...
This commit is contained in:
parent
0f6382b992
commit
c37c77e216
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Supabase 설정 (localhost - 같은 서버에서 실행 중)
|
||||||
|
SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzU4NTUxNjY2LCJleHAiOjQxMDI0NDQ4MDB9.jMCGL3Q-N2o_l7JQE_HrO7Uoct86CMgLsVxpabisG4I
|
||||||
|
|
||||||
|
# Kong Basic Auth (선택사항)
|
||||||
|
SUPABASE_BASIC_USER=
|
||||||
|
SUPABASE_BASIC_PASSWORD=
|
||||||
|
|
||||||
|
# Flask 설정
|
||||||
|
PORT=5000
|
||||||
|
# Flask 세션 보안 키 (프로덕션에서는 반드시 변경!)
|
||||||
|
SECRET_KEY=b62388775209d9176081ad7297f8cf68b8d0f98db0e84b317304255d414680be
|
||||||
|
|
||||||
|
# Web Push Notifications
|
||||||
|
VAPID_PUBLIC_KEY=BF5nMFp4VrnV4hHkG9Rmy2n-EwEH8Z9MP_Q2sPyBYdopg-J5y61XECovyD9s9lEKAgyl_a97ZD6CEV1qzMRHaHM
|
||||||
|
VAPID_PRIVATE_KEY=J6YKVvwLQ5FFI0nUQfF3KkwNGhXxxi-SGKCMkgfoNOI
|
||||||
|
VAPID_SUBJECT=mailto:example@humetrain.me
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
||||||
|
#Tue Oct 14 08:46:41 KST 2025
|
||||||
|
gradle.version=8.11.1
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,340 @@
|
||||||
|
# 회원 관리 및 인증 시스템 구축 완료 ✅
|
||||||
|
|
||||||
|
## 📝 작업 요약
|
||||||
|
|
||||||
|
부산교통공사 1호선 고장코드 시스템에 회원 관리 및 인증 기능을 성공적으로 추가했습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 구현된 기능
|
||||||
|
|
||||||
|
### 1. **이메일 인증 시스템**
|
||||||
|
- ✅ Supabase Auth를 활용한 이메일 기반 회원가입 및 로그인
|
||||||
|
- ✅ **humetro.busan.kr** 도메인만 허용하는 이메일 검증
|
||||||
|
- ✅ 비밀번호 암호화 및 안전한 인증 처리
|
||||||
|
|
||||||
|
### 2. **회원가입 기능**
|
||||||
|
- ✅ 사번, 이름, 소속부서, 이메일 정보 수집
|
||||||
|
- ✅ 부서 선택은 미리 정의된 부서 목록에서만 가능
|
||||||
|
- ✅ 이메일 도메인 자동 검증
|
||||||
|
- ✅ 비밀번호 확인 및 유효성 검사
|
||||||
|
|
||||||
|
### 3. **부서별 권한 관리 기반 구축**
|
||||||
|
- ✅ 부서 테이블 및 권한 테이블 설계
|
||||||
|
- ✅ 향후 부서별 조회/수정/삭제 권한 제어 가능
|
||||||
|
- ✅ RLS(Row Level Security) 정책 설정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 생성된 파일
|
||||||
|
|
||||||
|
### 1. **auth.py** - 인증 모듈
|
||||||
|
회원 관리 및 인증 로직을 담당하는 별도 모듈
|
||||||
|
|
||||||
|
**주요 클래스: AuthManager**
|
||||||
|
- `validate_email()`: 이메일 도메인 검증 (humetro.busan.kr)
|
||||||
|
- `get_departments()`: 가입 가능한 부서 목록 조회
|
||||||
|
- `signup_user()`: 회원가입 처리 (Supabase Auth + users 테이블)
|
||||||
|
- `login_user()`: 로그인 처리 및 세션 데이터 생성
|
||||||
|
- `logout_user()`: 로그아웃 처리
|
||||||
|
- `check_email_exists()`: 이메일 중복 체크
|
||||||
|
- `check_employee_id_exists()`: 사번 중복 체크
|
||||||
|
|
||||||
|
### 2. **database_schema.sql** - 데이터베이스 DDL
|
||||||
|
Supabase에서 실행할 데이터베이스 스키마 정의
|
||||||
|
|
||||||
|
**생성되는 테이블:**
|
||||||
|
|
||||||
|
#### 📋 departments (부서)
|
||||||
|
```sql
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- code (VARCHAR, UNIQUE) - 부서 코드
|
||||||
|
- name (VARCHAR) - 부서명
|
||||||
|
- description (TEXT) - 부서 설명
|
||||||
|
- is_active (BOOLEAN) - 활성화 여부
|
||||||
|
- created_at, updated_at (TIMESTAMPTZ)
|
||||||
|
```
|
||||||
|
|
||||||
|
**초기 부서 데이터:**
|
||||||
|
- SPC: 신평차량
|
||||||
|
- NPC: 노포차량
|
||||||
|
- VHD: 차량처
|
||||||
|
|
||||||
|
> **참고**: 부서 목록은 Supabase의 `public.departments` 테이블에서 동적으로 관리됩니다.
|
||||||
|
|
||||||
|
#### 👤 users (사용자)
|
||||||
|
```sql
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- auth_id (UUID, UNIQUE) - Supabase Auth ID
|
||||||
|
- email (VARCHAR, UNIQUE) - 이메일
|
||||||
|
- employee_id (VARCHAR, UNIQUE) - 사번
|
||||||
|
- name (VARCHAR) - 이름
|
||||||
|
- department_id (INTEGER, FK) - 부서 ID
|
||||||
|
- is_active (BOOLEAN) - 계정 활성화 여부
|
||||||
|
- last_login_at (TIMESTAMPTZ) - 마지막 로그인
|
||||||
|
- created_at, updated_at (TIMESTAMPTZ)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔐 department_permissions (부서별 권한)
|
||||||
|
```sql
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- department_id (INTEGER, FK)
|
||||||
|
- resource_type (VARCHAR) - 'fault_code', 'signal', 'mmi_code'
|
||||||
|
- can_read (BOOLEAN) - 조회 권한
|
||||||
|
- can_write (BOOLEAN) - 수정 권한
|
||||||
|
- can_delete (BOOLEAN) - 삭제 권한
|
||||||
|
- created_at, updated_at (TIMESTAMPTZ)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📊 audit_logs (감사 로그)
|
||||||
|
```sql
|
||||||
|
- id (SERIAL PRIMARY KEY)
|
||||||
|
- user_id (INTEGER, FK)
|
||||||
|
- action (VARCHAR) - 'login', 'logout', 'create', 'update', 'delete'
|
||||||
|
- resource_type (VARCHAR)
|
||||||
|
- resource_id (VARCHAR)
|
||||||
|
- details (JSONB)
|
||||||
|
- ip_address (VARCHAR)
|
||||||
|
- created_at (TIMESTAMPTZ)
|
||||||
|
```
|
||||||
|
|
||||||
|
**추가 기능:**
|
||||||
|
- ✅ RLS(Row Level Security) 정책 적용
|
||||||
|
- ✅ 트리거를 통한 updated_at 자동 업데이트
|
||||||
|
- ✅ 인덱스 최적화
|
||||||
|
- ✅ 뷰(users_with_department) 생성
|
||||||
|
|
||||||
|
### 3. **templates/login.html** - 로그인 페이지
|
||||||
|
- 이메일/비밀번호 입력 폼
|
||||||
|
- 회원가입 페이지 링크
|
||||||
|
- 에러/성공 메시지 표시
|
||||||
|
- 반응형 디자인 (Pico CSS)
|
||||||
|
|
||||||
|
### 4. **templates/signup.html** - 회원가입 페이지
|
||||||
|
- 사번, 이름, 부서, 이메일, 비밀번호 입력 폼
|
||||||
|
- 부서 선택 드롭다운 (Supabase에서 동적으로 로드)
|
||||||
|
- 이메일 도메인 검증 (humetro.busan.kr)
|
||||||
|
- 비밀번호 확인 검증
|
||||||
|
- 에러 메시지 표시
|
||||||
|
|
||||||
|
### 5. **app.py** - 수정된 메인 애플리케이션
|
||||||
|
**추가된 기능:**
|
||||||
|
- Flask 세션 설정 (SECRET_KEY, 쿠키 보안)
|
||||||
|
- 인증 라우트 추가:
|
||||||
|
- `/auth/login` (GET, POST): 로그인
|
||||||
|
- `/auth/signup` (GET, POST): 회원가입
|
||||||
|
- `/auth/logout`: 로그아웃
|
||||||
|
- `@login_required` 데코레이터: 로그인 필요 페이지 보호
|
||||||
|
- `get_current_user()` 함수: 현재 로그인 사용자 정보 조회
|
||||||
|
|
||||||
|
### 6. **templates/index.html** - 수정된 메인 페이지
|
||||||
|
- 헤더에 사용자 정보 표시 (이름, 사번)
|
||||||
|
- 로그아웃 버튼 추가
|
||||||
|
- 로그인된 사용자만 접근 가능
|
||||||
|
|
||||||
|
### 7. **AUTH_SETUP.md** - 설정 가이드
|
||||||
|
상세한 설정 및 사용 방법 문서
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 설정 방법 (빠른 시작)
|
||||||
|
|
||||||
|
### 1단계: 데이터베이스 스키마 적용
|
||||||
|
|
||||||
|
Supabase SQL 에디터에서 실행:
|
||||||
|
```bash
|
||||||
|
# database_schema.sql 파일 내용 복사 후 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: 환경 변수 설정
|
||||||
|
|
||||||
|
`.env` 파일에 추가:
|
||||||
|
```bash
|
||||||
|
# Supabase 설정
|
||||||
|
SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
|
|
||||||
|
# Flask 세션 보안 키 (반드시 변경!)
|
||||||
|
SECRET_KEY=your-secret-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3단계: 서버 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4단계: 회원가입 및 로그인
|
||||||
|
|
||||||
|
1. 브라우저에서 `http://localhost:5000` 접속
|
||||||
|
2. 자동으로 로그인 페이지로 리다이렉트
|
||||||
|
3. "회원가입" 클릭하여 계정 생성
|
||||||
|
4. humetro.busan.kr 도메인 이메일로 가입
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 주요 보안 기능
|
||||||
|
|
||||||
|
### 1. 이메일 도메인 제한 (애플리케이션 레벨)
|
||||||
|
|
||||||
|
> ⚠️ **Docker 기반 Supabase 환경**: 대시보드 Authentication 설정이 제한적이므로, **애플리케이션 레벨에서만** 도메인 검증을 수행합니다.
|
||||||
|
|
||||||
|
- **검증 위치**: `auth.py`의 `validate_email()` 함수
|
||||||
|
- **검증 패턴**: `@humetro.busan.kr`로 끝나는 이메일만 허용
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_email(self, email: str) -> tuple[bool, str]:
|
||||||
|
domain = email.split('@')[1]
|
||||||
|
if domain != self.ALLOWED_EMAIL_DOMAIN:
|
||||||
|
return False, f"{self.ALLOWED_EMAIL_DOMAIN} 도메인의 이메일만 사용 가능합니다."
|
||||||
|
return True, "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 중복 검사
|
||||||
|
- 이메일 중복 체크
|
||||||
|
- 사번 중복 체크
|
||||||
|
|
||||||
|
### 3. 세션 보안
|
||||||
|
- HTTPOnly 쿠키
|
||||||
|
- SameSite 설정
|
||||||
|
- SECRET_KEY 암호화
|
||||||
|
|
||||||
|
### 4. Row Level Security (RLS)
|
||||||
|
- 사용자는 본인 정보만 조회/수정 가능
|
||||||
|
- 부서 정보는 모든 인증된 사용자가 조회 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 데이터베이스 관계도
|
||||||
|
|
||||||
|
```
|
||||||
|
departments (부서)
|
||||||
|
↓ (1:N)
|
||||||
|
users (사용자) ← auth_id → Supabase Auth
|
||||||
|
↓
|
||||||
|
department_permissions (부서별 권한)
|
||||||
|
↓
|
||||||
|
audit_logs (감사 로그)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 사용자 흐름
|
||||||
|
|
||||||
|
### 회원가입 플로우
|
||||||
|
```
|
||||||
|
1. /auth/signup 접속
|
||||||
|
2. 부서 목록 로드 (Supabase departments 테이블)
|
||||||
|
3. 정보 입력 (사번, 이름, 부서, 이메일, 비밀번호)
|
||||||
|
4. 이메일 도메인 검증 (humetro.busan.kr)
|
||||||
|
5. Supabase Auth에 사용자 생성
|
||||||
|
6. users 테이블에 정보 저장
|
||||||
|
7. 로그인 페이지로 리다이렉트
|
||||||
|
```
|
||||||
|
|
||||||
|
### 로그인 플로우
|
||||||
|
```
|
||||||
|
1. /auth/login 접속
|
||||||
|
2. 이메일/비밀번호 입력
|
||||||
|
3. Supabase Auth 인증
|
||||||
|
4. users 테이블에서 사용자 정보 조회
|
||||||
|
5. 세션에 사용자 정보 저장
|
||||||
|
6. 메인 페이지로 리다이렉트
|
||||||
|
```
|
||||||
|
|
||||||
|
### 로그아웃 플로우
|
||||||
|
```
|
||||||
|
1. /auth/logout 접속
|
||||||
|
2. Supabase Auth 로그아웃
|
||||||
|
3. 세션 클리어
|
||||||
|
4. 로그인 페이지로 리다이렉트
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 API 엔드포인트
|
||||||
|
|
||||||
|
### 인증 관련
|
||||||
|
- `GET /auth/login` - 로그인 페이지
|
||||||
|
- `POST /auth/login` - 로그인 처리
|
||||||
|
- `GET /auth/signup` - 회원가입 페이지
|
||||||
|
- `POST /auth/signup` - 회원가입 처리
|
||||||
|
- `GET /auth/logout` - 로그아웃
|
||||||
|
|
||||||
|
### 메인
|
||||||
|
- `GET /` - 메인 페이지 (로그인 필요)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 향후 확장 가능 사항
|
||||||
|
|
||||||
|
### 1. 부서별 권한 적용
|
||||||
|
`department_permissions` 테이블을 활용하여:
|
||||||
|
- 고장코드 조회/수정 권한 제어
|
||||||
|
- TCMS 신호 조회/수정 권한 제어
|
||||||
|
- MMI 코드 조회/수정 권한 제어
|
||||||
|
|
||||||
|
### 2. 감사 로그 활용
|
||||||
|
`audit_logs` 테이블을 활용하여:
|
||||||
|
- 사용자 활동 추적
|
||||||
|
- 데이터 수정 이력 관리
|
||||||
|
- 보안 모니터링
|
||||||
|
|
||||||
|
### 3. 비밀번호 정책 강화
|
||||||
|
- 대문자, 숫자, 특수문자 포함 필수
|
||||||
|
- 비밀번호 변경 주기 설정
|
||||||
|
- 비밀번호 재사용 방지
|
||||||
|
|
||||||
|
### 4. 2단계 인증 (2FA)
|
||||||
|
- OTP 인증 추가
|
||||||
|
- SMS 인증 추가
|
||||||
|
|
||||||
|
### 5. 관리자 페이지
|
||||||
|
- 사용자 관리 (활성화/비활성화)
|
||||||
|
- 부서 관리
|
||||||
|
- 권한 관리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 체크리스트
|
||||||
|
|
||||||
|
### 설정 완료 확인
|
||||||
|
- [ ] `database_schema.sql` 실행
|
||||||
|
- [ ] `.env` 파일에 `SECRET_KEY` 설정
|
||||||
|
- [ ] Supabase Auth 이메일 인증 활성화
|
||||||
|
- [ ] 부서 데이터 확인 (`SELECT * FROM departments`)
|
||||||
|
- [ ] 테스트 회원가입 수행
|
||||||
|
- [ ] 테스트 로그인 수행
|
||||||
|
|
||||||
|
### 프로덕션 배포 전
|
||||||
|
- [ ] `SECRET_KEY` 변경 (강력한 랜덤 키)
|
||||||
|
- [ ] HTTPS 활성화
|
||||||
|
- [ ] CORS 설정 검토
|
||||||
|
- [ ] Supabase 프로덕션 환경 설정
|
||||||
|
- [ ] 이메일 템플릿 커스터마이징
|
||||||
|
- [ ] 로그 모니터링 설정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 관련 문서
|
||||||
|
|
||||||
|
- **AUTH_SETUP.md**: 상세 설정 가이드 및 트러블슈팅
|
||||||
|
- **database_schema.sql**: 데이터베이스 스키마 전체
|
||||||
|
- **auth.py**: 인증 모듈 소스 코드
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 기술 스택
|
||||||
|
|
||||||
|
- **Backend**: Flask (Python)
|
||||||
|
- **Authentication**: Supabase Auth
|
||||||
|
- **Database**: Supabase (PostgreSQL)
|
||||||
|
- **Frontend**: Pico CSS, HTMX
|
||||||
|
- **Session**: Flask Session (서버 사이드)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2024년 10월
|
||||||
|
**부산교통공사 차량처**
|
||||||
|
**1호선 고장코드 시스템 v2.0**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
# 회원 관리 및 인증 시스템 설정 가이드
|
||||||
|
|
||||||
|
## 📋 개요
|
||||||
|
|
||||||
|
부산교통공사 1호선 고장코드 시스템에 회원 관리 및 인증 기능이 추가되었습니다.
|
||||||
|
|
||||||
|
### 주요 기능
|
||||||
|
- ✅ **이메일 인증**: Supabase Auth를 사용한 이메일 기반 인증
|
||||||
|
- ✅ **도메인 제한**: humetro.busan.kr 도메인만 가입 가능
|
||||||
|
- ✅ **부서별 관리**: 부서 단위로 사용자 관리 및 권한 제어
|
||||||
|
- ✅ **세션 관리**: Flask 세션을 통한 로그인 상태 유지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ 1. 데이터베이스 설정
|
||||||
|
|
||||||
|
### 1.1 Supabase SQL 에디터에서 스키마 생성
|
||||||
|
|
||||||
|
`database_schema.sql` 파일의 내용을 Supabase SQL 에디터에서 실행하세요.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 파일 위치
|
||||||
|
./database_schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
이 스크립트는 다음 테이블을 생성합니다:
|
||||||
|
- **departments**: 부서 정보
|
||||||
|
- **users**: 사용자 정보
|
||||||
|
- **department_permissions**: 부서별 권한 (조회/수정/삭제)
|
||||||
|
- **audit_logs**: 감사 로그 (선택사항)
|
||||||
|
|
||||||
|
### 1.2 초기 부서 데이터
|
||||||
|
|
||||||
|
스크립트 실행 시 다음 부서가 자동으로 생성됩니다:
|
||||||
|
|
||||||
|
| 코드 | 부서명 | 설명 |
|
||||||
|
|------|--------|------|
|
||||||
|
| SPC | 신평차량 | 신평차량사업소 |
|
||||||
|
| NPC | 노포차량 | 노포차량사업소 |
|
||||||
|
| VHD | 차량처 | 차량처 |
|
||||||
|
|
||||||
|
> **참고**: 추가 부서가 필요한 경우 Supabase에서 직접 추가하거나 관리자 페이지를 통해 관리할 수 있습니다.
|
||||||
|
|
||||||
|
**추가 부서가 필요한 경우**:
|
||||||
|
```sql
|
||||||
|
INSERT INTO public.departments (code, name, description)
|
||||||
|
VALUES ('NEW001', '새로운부서', '부서 설명');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 2. Supabase Auth 설정
|
||||||
|
|
||||||
|
### 2.1 Docker 기반 Supabase 환경
|
||||||
|
|
||||||
|
> ⚠️ **중요**: Docker 기반 Supabase를 사용하는 경우, 대시보드의 Authentication 설정이 제한적입니다.
|
||||||
|
>
|
||||||
|
> 따라서 **애플리케이션 레벨에서만 이메일 도메인 검증**을 수행합니다.
|
||||||
|
|
||||||
|
### 2.2 이메일 도메인 검증 (애플리케이션 레벨)
|
||||||
|
|
||||||
|
`auth.py` 모듈에서 자동으로 처리됩니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# humetro.busan.kr 도메인만 허용
|
||||||
|
ALLOWED_EMAIL_DOMAIN = "humetro.busan.kr"
|
||||||
|
|
||||||
|
def validate_email(self, email: str) -> tuple[bool, str]:
|
||||||
|
domain = email.split('@')[1]
|
||||||
|
if domain != self.ALLOWED_EMAIL_DOMAIN:
|
||||||
|
return False, f"{self.ALLOWED_EMAIL_DOMAIN} 도메인의 이메일만 사용 가능합니다."
|
||||||
|
return True, "OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Supabase Auth 기본 설정 확인
|
||||||
|
|
||||||
|
Docker Supabase에서 확인할 사항:
|
||||||
|
- Supabase가 정상적으로 실행 중인지 확인
|
||||||
|
- Kong Gateway (기본 포트 8000)가 작동하는지 확인
|
||||||
|
- Auth 서비스가 활성화되어 있는지 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase 상태 확인
|
||||||
|
docker ps | grep supabase
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 3. 환경 변수 설정
|
||||||
|
|
||||||
|
### 3.1 Flask 백엔드 `.env` 파일 위치
|
||||||
|
|
||||||
|
> ⚠️ **중요**: `SECRET_KEY`는 **Flask 백엔드**의 `.env` 파일에 설정해야 합니다!
|
||||||
|
|
||||||
|
**파일 위치**: `/home/ckh08045/Tr_Code/.env` (app.py가 있는 디렉토리)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase 설정
|
||||||
|
SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
|
|
||||||
|
# Flask 세션 보안 키 (프로덕션에서는 반드시 변경!)
|
||||||
|
SECRET_KEY=your-secret-key-here-change-in-production
|
||||||
|
|
||||||
|
# Kong Basic Auth (선택사항)
|
||||||
|
SUPABASE_BASIC_USER=
|
||||||
|
SUPABASE_BASIC_PASSWORD=
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 SECRET_KEY 생성
|
||||||
|
|
||||||
|
Python으로 안전한 SECRET_KEY 생성:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import secrets
|
||||||
|
print(secrets.token_hex(32))
|
||||||
|
```
|
||||||
|
|
||||||
|
생성된 키를 **Flask 백엔드 `.env`** 파일의 `SECRET_KEY`에 설정하세요.
|
||||||
|
|
||||||
|
### 3.3 프론트엔드와 백엔드 .env 구분
|
||||||
|
|
||||||
|
**Flask 백엔드 .env** (`/home/ckh08045/Tr_Code/.env`):
|
||||||
|
- `SUPABASE_URL`
|
||||||
|
- `SUPABASE_ANON_KEY`
|
||||||
|
- `SECRET_KEY` ← **여기에 설정!**
|
||||||
|
- `SUPABASE_BASIC_USER`
|
||||||
|
- `SUPABASE_BASIC_PASSWORD`
|
||||||
|
|
||||||
|
**TWA 프론트엔드 .env** (`/home/ckh08045/Tr_Code/twa-frontend/.env`):
|
||||||
|
- Vue.js 관련 설정
|
||||||
|
- API 엔드포인트 등
|
||||||
|
- `SECRET_KEY`는 **필요 없음**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 4. 애플리케이션 실행
|
||||||
|
|
||||||
|
### 4.1 의존성 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 서버 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
서버가 `http://localhost:5000`에서 실행됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 5. 사용 방법
|
||||||
|
|
||||||
|
### 5.1 회원가입
|
||||||
|
|
||||||
|
1. 브라우저에서 `http://localhost:5000` 접속
|
||||||
|
2. 자동으로 로그인 페이지로 리다이렉트됨
|
||||||
|
3. **회원가입** 버튼 클릭
|
||||||
|
4. 다음 정보 입력:
|
||||||
|
- 사번
|
||||||
|
- 이름
|
||||||
|
- 소속 부서 (드롭다운에서 선택)
|
||||||
|
- 이메일 (humetro.busan.kr 도메인만)
|
||||||
|
- 비밀번호 (최소 8자)
|
||||||
|
|
||||||
|
5. 회원가입 완료 후 이메일 인증 (Supabase Auth 설정에 따라)
|
||||||
|
|
||||||
|
### 5.2 로그인
|
||||||
|
|
||||||
|
1. 등록한 이메일과 비밀번호로 로그인
|
||||||
|
2. 로그인 성공 시 메인 페이지로 이동
|
||||||
|
3. 헤더에 사용자 이름과 사번 표시
|
||||||
|
|
||||||
|
### 5.3 로그아웃
|
||||||
|
|
||||||
|
- 헤더의 **로그아웃** 버튼 클릭
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 6. 권한 관리 (향후 확장)
|
||||||
|
|
||||||
|
### 6.1 부서별 권한 조회
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT d.name, dp.resource_type, dp.can_read, dp.can_write, dp.can_delete
|
||||||
|
FROM public.department_permissions dp
|
||||||
|
JOIN public.departments d ON dp.department_id = d.id
|
||||||
|
WHERE d.code = 'SPC';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 권한 수정
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE public.department_permissions
|
||||||
|
SET can_write = true
|
||||||
|
WHERE department_id = 1 AND resource_type = 'fault_code';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 애플리케이션에서 권한 체크 (예시)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def check_permission(user, resource_type, action):
|
||||||
|
"""
|
||||||
|
user: 현재 사용자 정보
|
||||||
|
resource_type: 'fault_code', 'signal', 'mmi_code'
|
||||||
|
action: 'read', 'write', 'delete'
|
||||||
|
"""
|
||||||
|
with build_pg_client() as c:
|
||||||
|
r = c.get(
|
||||||
|
"/department_permissions",
|
||||||
|
params={
|
||||||
|
"department_id": f"eq.{user['department_id']}",
|
||||||
|
"resource_type": f"eq.{resource_type}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
perms = r.json()
|
||||||
|
if perms:
|
||||||
|
return perms[0].get(f"can_{action}", False)
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 7. 데이터베이스 스키마
|
||||||
|
|
||||||
|
### 7.1 테이블 구조
|
||||||
|
|
||||||
|
#### departments (부서)
|
||||||
|
- `id` (PK): 부서 ID
|
||||||
|
- `code`: 부서 코드 (UNIQUE)
|
||||||
|
- `name`: 부서명
|
||||||
|
- `description`: 설명
|
||||||
|
- `is_active`: 활성화 여부
|
||||||
|
- `created_at`, `updated_at`: 타임스탬프
|
||||||
|
|
||||||
|
#### users (사용자)
|
||||||
|
- `id` (PK): 사용자 ID
|
||||||
|
- `auth_id`: Supabase Auth 사용자 ID (UNIQUE)
|
||||||
|
- `email`: 이메일 (UNIQUE)
|
||||||
|
- `employee_id`: 사번 (UNIQUE)
|
||||||
|
- `name`: 이름
|
||||||
|
- `department_id` (FK): 부서 ID
|
||||||
|
- `is_active`: 계정 활성화 여부
|
||||||
|
- `last_login_at`: 마지막 로그인 시각
|
||||||
|
- `created_at`, `updated_at`: 타임스탬프
|
||||||
|
|
||||||
|
#### department_permissions (부서별 권한)
|
||||||
|
- `id` (PK): 권한 ID
|
||||||
|
- `department_id` (FK): 부서 ID
|
||||||
|
- `resource_type`: 리소스 타입 ('fault_code', 'signal', 'mmi_code')
|
||||||
|
- `can_read`: 조회 권한
|
||||||
|
- `can_write`: 수정 권한
|
||||||
|
- `can_delete`: 삭제 권한
|
||||||
|
- `created_at`, `updated_at`: 타임스탬프
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 8. 트러블슈팅
|
||||||
|
|
||||||
|
### 8.1 로그인 실패
|
||||||
|
|
||||||
|
**증상**: "이메일 또는 비밀번호가 올바르지 않습니다."
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
1. 이메일 주소가 `@humetro.busan.kr`로 끝나는지 확인
|
||||||
|
2. 비밀번호가 8자 이상인지 확인
|
||||||
|
3. Supabase Auth가 활성화되어 있는지 확인
|
||||||
|
4. `users` 테이블에 사용자가 존재하는지 확인:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM public.users WHERE email = 'user@humetro.busan.kr';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 회원가입 실패
|
||||||
|
|
||||||
|
**증상**: "회원가입에 실패했습니다."
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
1. Supabase Auth가 활성화되어 있는지 확인
|
||||||
|
2. 이메일이 이미 등록되어 있는지 확인
|
||||||
|
3. 사번이 중복되지 않는지 확인
|
||||||
|
4. `departments` 테이블에 부서가 존재하는지 확인
|
||||||
|
|
||||||
|
### 8.3 부서 목록이 표시되지 않음
|
||||||
|
|
||||||
|
**증상**: 회원가입 페이지에서 부서 선택 드롭다운이 비어있음
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
1. `database_schema.sql`이 정상적으로 실행되었는지 확인
|
||||||
|
2. `departments` 테이블에 데이터가 있는지 확인:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM public.departments WHERE is_active = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 세션이 유지되지 않음
|
||||||
|
|
||||||
|
**증상**: 페이지를 새로고침하면 로그인이 풀림
|
||||||
|
|
||||||
|
**해결 방법**:
|
||||||
|
1. `.env` 파일에 `SECRET_KEY`가 설정되어 있는지 확인
|
||||||
|
2. Flask 앱이 세션 쿠키를 설정할 수 있는지 확인 (HTTPS/HTTP 설정)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ 9. 보안 고려사항
|
||||||
|
|
||||||
|
### 9.1 프로덕션 체크리스트
|
||||||
|
|
||||||
|
- [ ] `SECRET_KEY` 변경 (절대 기본값 사용 금지)
|
||||||
|
- [ ] HTTPS 사용
|
||||||
|
- [ ] Supabase RLS(Row Level Security) 활성화
|
||||||
|
- [ ] 비밀번호 정책 강화 (대문자, 숫자, 특수문자 포함)
|
||||||
|
- [ ] 세션 타임아웃 설정
|
||||||
|
- [ ] CORS 설정 검토 (프로덕션에서는 특정 도메인만 허용)
|
||||||
|
- [ ] 로그 모니터링 (`audit_logs` 테이블 활용)
|
||||||
|
|
||||||
|
### 9.2 RLS 정책 (이미 적용됨)
|
||||||
|
|
||||||
|
`database_schema.sql`에 다음 RLS 정책이 포함되어 있습니다:
|
||||||
|
- 부서 목록은 모든 인증된 사용자가 조회 가능
|
||||||
|
- 사용자는 본인 정보만 조회/수정 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 10. 참고 자료
|
||||||
|
|
||||||
|
### 10.1 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
Tr_Code/
|
||||||
|
├── app.py # 메인 애플리케이션 (인증 라우트 포함)
|
||||||
|
├── auth.py # 인증 모듈 (AuthManager 클래스)
|
||||||
|
├── database_schema.sql # 데이터베이스 스키마
|
||||||
|
├── templates/
|
||||||
|
│ ├── index.html # 메인 페이지 (로그인 필요)
|
||||||
|
│ ├── login.html # 로그인 페이지
|
||||||
|
│ └── signup.html # 회원가입 페이지
|
||||||
|
└── .env # 환경 변수 설정
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 핵심 모듈
|
||||||
|
|
||||||
|
#### `auth.py` - AuthManager 클래스
|
||||||
|
- `validate_email()`: 이메일 도메인 검증
|
||||||
|
- `get_departments()`: 부서 목록 조회
|
||||||
|
- `signup_user()`: 회원가입 처리
|
||||||
|
- `login_user()`: 로그인 처리
|
||||||
|
- `logout_user()`: 로그아웃 처리
|
||||||
|
|
||||||
|
#### `app.py` - 인증 라우트
|
||||||
|
- `/auth/login`: 로그인 페이지 및 처리
|
||||||
|
- `/auth/signup`: 회원가입 페이지 및 처리
|
||||||
|
- `/auth/logout`: 로그아웃 처리
|
||||||
|
- `/`: 메인 페이지 (로그인 필요)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 11. 문의 및 지원
|
||||||
|
|
||||||
|
시스템 관련 문의사항은 차량처 시스템 관리자에게 연락하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2024년 10월
|
||||||
|
**버전**: 1.0
|
||||||
|
**부산교통공사 차량처**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
# 생체인증 가이드 (Credential Management API)
|
||||||
|
|
||||||
|
## 📱 개요
|
||||||
|
|
||||||
|
TWA(Trusted Web Activity) 환경에서 WebAuthn / Credential Management API를 활용한 생체인증 기능을 제공합니다.
|
||||||
|
|
||||||
|
## ✨ 주요 기능
|
||||||
|
|
||||||
|
### 1. 생체인증 등록
|
||||||
|
- 로그인 후 생체인증(지문, 얼굴 인식 등)을 등록할 수 있습니다
|
||||||
|
- 사용자의 사번과 이름으로 credential을 생성합니다
|
||||||
|
- 등록된 credential은 Supabase에 안전하게 저장됩니다
|
||||||
|
|
||||||
|
### 2. 생체인증 로그인
|
||||||
|
- 등록된 생체인증으로 빠르게 로그인할 수 있습니다
|
||||||
|
- 비밀번호 입력 없이 지문/얼굴 인식만으로 인증됩니다
|
||||||
|
- 플랫폼 인증 기능을 사용하여 보안성이 높습니다
|
||||||
|
|
||||||
|
### 3. 생체인증 해제
|
||||||
|
- 필요시 생체인증을 해제할 수 있습니다
|
||||||
|
- 해제 후 다시 등록할 수 있습니다
|
||||||
|
|
||||||
|
## 🏗️ 아키텍처
|
||||||
|
|
||||||
|
### Frontend (Vue.js/TypeScript)
|
||||||
|
|
||||||
|
#### 1. Composable: `useBiometric.ts`
|
||||||
|
```typescript
|
||||||
|
// 생체인증 지원 여부 확인
|
||||||
|
await biometric.checkSupport()
|
||||||
|
|
||||||
|
// 생체인증 등록
|
||||||
|
const result = await biometric.register(employeeId, userName)
|
||||||
|
|
||||||
|
// 생체인증 로그인
|
||||||
|
const result = await biometric.authenticate()
|
||||||
|
|
||||||
|
// 생체인증 해제
|
||||||
|
const result = await biometric.unregister()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 상태 관리
|
||||||
|
- `isSupported`: 브라우저가 WebAuthn을 지원하는지 여부
|
||||||
|
- `isAvailable`: 플랫폼 생체인증이 사용 가능한지 여부
|
||||||
|
- `isEnrolled`: 사용자가 생체인증을 등록했는지 여부
|
||||||
|
- `canUse`: 생체인증을 사용할 수 있는지 여부
|
||||||
|
- `canRegister`: 생체인증을 등록할 수 있는지 여부
|
||||||
|
- `canAuthenticate`: 생체인증으로 로그인할 수 있는지 여부
|
||||||
|
|
||||||
|
### Backend (Flask/Python)
|
||||||
|
|
||||||
|
#### 1. 생체인증 등록 Challenge
|
||||||
|
```
|
||||||
|
POST /api/biometric/register-challenge
|
||||||
|
{
|
||||||
|
"employeeId": "20240001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"challenge": "random_base64_string",
|
||||||
|
"userId": "20240001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Credential 등록
|
||||||
|
```
|
||||||
|
POST /api/biometric/register
|
||||||
|
{
|
||||||
|
"employeeId": "20240001",
|
||||||
|
"credential": {
|
||||||
|
"id": "credential_id",
|
||||||
|
"rawId": "base64_raw_id",
|
||||||
|
"response": {
|
||||||
|
"clientDataJSON": "base64_client_data",
|
||||||
|
"attestationObject": "base64_attestation"
|
||||||
|
},
|
||||||
|
"type": "public-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 로그인 Challenge
|
||||||
|
```
|
||||||
|
POST /api/biometric/login-challenge
|
||||||
|
{
|
||||||
|
"employeeId": "20240001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 생체인증 로그인
|
||||||
|
```
|
||||||
|
POST /api/biometric/login
|
||||||
|
{
|
||||||
|
"employeeId": "20240001",
|
||||||
|
"assertion": {
|
||||||
|
"id": "credential_id",
|
||||||
|
"rawId": "base64_raw_id",
|
||||||
|
"response": {
|
||||||
|
"clientDataJSON": "base64_client_data",
|
||||||
|
"authenticatorData": "base64_auth_data",
|
||||||
|
"signature": "base64_signature"
|
||||||
|
},
|
||||||
|
"type": "public-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"employee_id": "20240001",
|
||||||
|
"name": "홍길동",
|
||||||
|
"email": "hong@humetro.busan.kr",
|
||||||
|
"department_id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 생체인증 해제
|
||||||
|
```
|
||||||
|
POST /api/biometric/unregister
|
||||||
|
{
|
||||||
|
"employeeId": "20240001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗄️ 데이터베이스 스키마
|
||||||
|
|
||||||
|
### biometric_credentials 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS public.biometric_credentials (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
employee_id VARCHAR(50) NOT NULL REFERENCES public.users(employee_id) ON DELETE CASCADE,
|
||||||
|
credential_id VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
credential_data JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
UNIQUE(employee_id, credential_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_biometric_employee_id ON public.biometric_credentials(employee_id);
|
||||||
|
CREATE INDEX idx_biometric_credential_id ON public.biometric_credentials(credential_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**필드 설명:**
|
||||||
|
- `employee_id`: 사용자 사번 (외래키)
|
||||||
|
- `credential_id`: WebAuthn credential ID (고유값)
|
||||||
|
- `credential_data`: Credential 전체 데이터 (JSON)
|
||||||
|
- `created_at`: 등록 일시
|
||||||
|
- `last_used_at`: 마지막 사용 일시
|
||||||
|
|
||||||
|
## 🔐 보안 고려사항
|
||||||
|
|
||||||
|
### 1. Challenge-Response 인증
|
||||||
|
- 서버에서 생성한 랜덤 challenge를 사용합니다
|
||||||
|
- Challenge는 세션에 저장되고 10분간 유효합니다
|
||||||
|
- 각 인증 시도마다 새로운 challenge를 생성합니다
|
||||||
|
|
||||||
|
### 2. 플랫폼 인증
|
||||||
|
- `authenticatorAttachment: 'platform'`을 사용하여 기기 내장 생체인식만 허용합니다
|
||||||
|
- `userVerification: 'required'`로 사용자 검증을 필수로 합니다
|
||||||
|
|
||||||
|
### 3. Credential 검증
|
||||||
|
- 서버에 저장된 credential과 비교하여 검증합니다
|
||||||
|
- 실제 프로덕션에서는 공개키 암호화를 사용한 서명 검증이 필요합니다 (현재는 간소화된 버전)
|
||||||
|
|
||||||
|
### 4. 세션 관리
|
||||||
|
- 생체인증 성공 시 Flask 세션에 사용자 정보를 저장합니다
|
||||||
|
- 세션은 httponly 쿠키로 안전하게 관리됩니다
|
||||||
|
|
||||||
|
## 📱 TWA 환경에서의 동작
|
||||||
|
|
||||||
|
### 1. Android TWA
|
||||||
|
- Chrome의 WebAuthn API를 통해 Android 생체인식(지문, 얼굴)을 사용합니다
|
||||||
|
- Android Keystore에 안전하게 credential이 저장됩니다
|
||||||
|
- Google Play Services를 통한 FIDO2 인증을 지원합니다
|
||||||
|
|
||||||
|
### 2. 지원 여부 확인
|
||||||
|
```javascript
|
||||||
|
// PublicKeyCredential API 지원 확인
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
console.log('생체인증 미지원')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플랫폼 인증 가능 여부
|
||||||
|
const available = await PublicKeyCredential
|
||||||
|
.isUserVerifyingPlatformAuthenticatorAvailable()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 사용자 경험
|
||||||
|
1. 로그인 페이지에서 생체인증 가능 여부를 자동으로 확인합니다
|
||||||
|
2. 등록된 생체인증이 있으면 "생체인증으로 로그인" 버튼이 표시됩니다
|
||||||
|
3. 버튼 클릭 시 기기의 생체인증 UI가 표시됩니다
|
||||||
|
4. 인증 성공 시 자동으로 로그인됩니다
|
||||||
|
|
||||||
|
## 🚀 사용 방법
|
||||||
|
|
||||||
|
### 1. 생체인증 등록 (로그인 후)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useBiometric } from '@/composables/useBiometric'
|
||||||
|
|
||||||
|
const biometric = useBiometric()
|
||||||
|
|
||||||
|
// 지원 여부 확인
|
||||||
|
await biometric.checkSupport()
|
||||||
|
|
||||||
|
if (biometric.canRegister.value) {
|
||||||
|
// 등록
|
||||||
|
const result = await biometric.register(employeeId, userName)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('생체인증 등록 성공')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 생체인증 로그인
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (biometric.canAuthenticate.value) {
|
||||||
|
const result = await biometric.authenticate()
|
||||||
|
|
||||||
|
if (result.success && result.user) {
|
||||||
|
// 로그인 성공
|
||||||
|
authStore.setUser(result.user)
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. LoginView에서의 통합
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 생체인증 버튼 -->
|
||||||
|
<button
|
||||||
|
v-if="biometric.canAuthenticate.value"
|
||||||
|
@click="handleBiometricLogin"
|
||||||
|
:disabled="biometric.loading.value"
|
||||||
|
>
|
||||||
|
생체인증으로 로그인
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 등록 안내 -->
|
||||||
|
<div v-else-if="!biometric.isEnrolled.value && biometric.canUse.value">
|
||||||
|
로그인 후 생체인증을 등록할 수 있습니다.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useBiometric } from '@/composables/useBiometric'
|
||||||
|
|
||||||
|
const biometric = useBiometric()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await biometric.checkSupport()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleBiometricLogin() {
|
||||||
|
const result = await biometric.authenticate()
|
||||||
|
// 처리...
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 문제 해결
|
||||||
|
|
||||||
|
### 1. 생체인증이 표시되지 않음
|
||||||
|
- 브라우저가 WebAuthn을 지원하는지 확인
|
||||||
|
- HTTPS 환경에서만 동작 (localhost는 예외)
|
||||||
|
- 기기에 생체인증이 설정되어 있는지 확인
|
||||||
|
|
||||||
|
### 2. 등록 실패
|
||||||
|
- Challenge가 만료되었을 수 있음 (10분)
|
||||||
|
- 네트워크 연결 확인
|
||||||
|
- 콘솔 로그에서 에러 메시지 확인
|
||||||
|
|
||||||
|
### 3. 로그인 실패
|
||||||
|
- 등록된 credential이 삭제되었을 수 있음
|
||||||
|
- localStorage에서 `biometric_credential_id` 확인
|
||||||
|
- 데이터베이스에 credential이 존재하는지 확인
|
||||||
|
|
||||||
|
## 📝 TODO (향후 개선사항)
|
||||||
|
|
||||||
|
1. **서명 검증 구현**
|
||||||
|
- 현재는 credential_id만 비교하는 간소화된 버전
|
||||||
|
- 실제 공개키 암호화 서명 검증 추가 필요
|
||||||
|
|
||||||
|
2. **여러 Credential 지원**
|
||||||
|
- 한 사용자가 여러 기기에서 생체인증 등록 가능하도록
|
||||||
|
|
||||||
|
3. **Credential 관리 UI**
|
||||||
|
- 등록된 기기 목록 표시
|
||||||
|
- 개별 credential 삭제 기능
|
||||||
|
|
||||||
|
4. **마지막 사용 시간 업데이트**
|
||||||
|
- 로그인 시 `last_used_at` 필드 업데이트
|
||||||
|
|
||||||
|
5. **에러 로깅 개선**
|
||||||
|
- 생체인증 관련 에러를 audit_logs에 기록
|
||||||
|
|
||||||
|
## 🔗 참고 자료
|
||||||
|
|
||||||
|
- [WebAuthn Guide](https://webauthn.guide/)
|
||||||
|
- [MDN - Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
|
||||||
|
- [FIDO Alliance](https://fidoalliance.org/)
|
||||||
|
- [Chrome WebAuthn](https://developers.google.com/web/updates/2018/05/webauthn)
|
||||||
|
|
||||||
|
Can't render this file because it is too large.
|
|
Can't render this file because it is too large.
|
|
Can't render this file because it is too large.
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
# 📋 부서 관리 API 문서
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
회원가입 시 부서 선택을 위한 동적 부서 목록 API입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 엔드포인트
|
||||||
|
|
||||||
|
### GET `/api/departments`
|
||||||
|
|
||||||
|
부서 목록을 조회합니다.
|
||||||
|
|
||||||
|
**인증**: 불필요 (공개 API)
|
||||||
|
|
||||||
|
**요청**:
|
||||||
|
```http
|
||||||
|
GET /api/departments HTTP/1.1
|
||||||
|
Host: localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 (성공)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"departments": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"code": "SPC",
|
||||||
|
"name": "신평차량",
|
||||||
|
"description": "신평차량사업소",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"code": "NPC",
|
||||||
|
"name": "노포차량",
|
||||||
|
"description": "노포차량사업소",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"code": "VHD",
|
||||||
|
"name": "차량처",
|
||||||
|
"description": "차량처",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 (실패)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "오류 메시지"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프론트엔드 사용법
|
||||||
|
|
||||||
|
### Vue.js (TWA 프론트엔드)
|
||||||
|
|
||||||
|
`SignupView.vue`에서 부서 목록 로드:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 부서 목록 가져오기
|
||||||
|
async function fetchDepartments() {
|
||||||
|
loadingDepartments.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/departments')
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('부서 목록을 가져오는데 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
departments.value = data.departments || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('부서 목록 로딩 실패:', error)
|
||||||
|
errorMessage.value = '부서 목록을 불러오는데 실패했습니다.'
|
||||||
|
} finally {
|
||||||
|
loadingDepartments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 로드
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDepartments()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML 템플릿 사용
|
||||||
|
|
||||||
|
```html
|
||||||
|
<select v-model="formData.departmentId" required :disabled="loadingDepartments">
|
||||||
|
<option value="">
|
||||||
|
{{ loadingDepartments ? '부서 목록 로딩 중...' : '부서를 선택하세요' }}
|
||||||
|
</option>
|
||||||
|
<option v-for="dept in departments" :key="dept.id" :value="dept.id">
|
||||||
|
{{ dept.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 데이터베이스 구조
|
||||||
|
|
||||||
|
### departments 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE public.departments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
code VARCHAR(20) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 초기 데이터
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO public.departments (code, name, description) VALUES
|
||||||
|
('SPC', '신평차량', '신평차량사업소'),
|
||||||
|
('NPC', '노포차량', '노포차량사업소'),
|
||||||
|
('VHD', '차량처', '차량처')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부서 추가 방법
|
||||||
|
|
||||||
|
### 1. Supabase SQL 에디터에서 직접 추가
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO public.departments (code, name, description)
|
||||||
|
VALUES ('NEW001', '새로운부서', '부서 설명');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 관리자 페이지 (향후 구현)
|
||||||
|
|
||||||
|
관리자 권한이 있는 사용자가 웹 인터페이스를 통해 부서를 추가/수정/삭제할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 필터링
|
||||||
|
|
||||||
|
### 활성화된 부서만 조회
|
||||||
|
|
||||||
|
현재 API는 모든 부서를 반환합니다. 비활성화된 부서를 제외하려면:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.route("/api/departments")
|
||||||
|
def api_departments():
|
||||||
|
"""부서 목록 조회 API (활성화된 부서만)"""
|
||||||
|
try:
|
||||||
|
with build_pg_client() as c:
|
||||||
|
r = c.get(
|
||||||
|
"/departments",
|
||||||
|
params={
|
||||||
|
"select": "id,code,name,description,is_active",
|
||||||
|
"is_active": "eq.true", # 활성화된 부서만
|
||||||
|
"order": "name.asc"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
departments = r.json() or []
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"departments": departments
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}, 500
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 에러 처리
|
||||||
|
|
||||||
|
### 프론트엔드 에러 처리
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/departments')
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('부서 목록을 가져오는데 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || '알 수 없는 오류')
|
||||||
|
}
|
||||||
|
|
||||||
|
departments.value = data.departments || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('부서 목록 로딩 실패:', error)
|
||||||
|
errorMessage.value = '부서 목록을 불러오는데 실패했습니다.'
|
||||||
|
setTimeout(() => errorMessage.value = '', 3000)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 성능 최적화
|
||||||
|
|
||||||
|
### 캐싱
|
||||||
|
|
||||||
|
부서 목록은 자주 변경되지 않으므로 캐싱을 적용할 수 있습니다:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 간단한 메모리 캐시 (5분)
|
||||||
|
let cachedDepartments: Department[] | null = null
|
||||||
|
let cacheTime: number | null = null
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000 // 5분
|
||||||
|
|
||||||
|
async function fetchDepartments() {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// 캐시가 유효한 경우
|
||||||
|
if (cachedDepartments && cacheTime && now - cacheTime < CACHE_DURATION) {
|
||||||
|
departments.value = cachedDepartments
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
loadingDepartments.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/departments')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
cachedDepartments = data.departments
|
||||||
|
cacheTime = now
|
||||||
|
departments.value = data.departments
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingDepartments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스트
|
||||||
|
|
||||||
|
### cURL로 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:5000/api/departments
|
||||||
|
```
|
||||||
|
|
||||||
|
### 브라우저 콘솔에서 테스트
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
fetch('/api/departments')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => console.log(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 문제 해결
|
||||||
|
|
||||||
|
### 부서 목록이 비어있음
|
||||||
|
|
||||||
|
**원인**: `departments` 테이블에 데이터가 없음
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM public.departments;
|
||||||
|
|
||||||
|
-- 데이터가 없으면 초기 데이터 삽입
|
||||||
|
INSERT INTO public.departments (code, name, description) VALUES
|
||||||
|
('SPC', '신평차량', '신평차량사업소'),
|
||||||
|
('NPC', '노포차량', '노포차량사업소'),
|
||||||
|
('VHD', '차량처', '차량처')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 호출 실패
|
||||||
|
|
||||||
|
**원인**: Flask 서버가 실행되지 않음 또는 CORS 오류
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```bash
|
||||||
|
# 서버 실행 확인
|
||||||
|
python app.py
|
||||||
|
|
||||||
|
# CORS 설정 확인 (app.py)
|
||||||
|
CORS(app, origins=["*"], supports_credentials=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**부산교통공사 차량처**
|
||||||
|
**1호선 고장코드 시스템**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,490 @@
|
||||||
|
# 이메일 코드 인증 가이드
|
||||||
|
|
||||||
|
## 📧 개요
|
||||||
|
|
||||||
|
링크 클릭 방식 대신 **6자리 숫자 코드 입력 방식**의 이메일 인증을 사용합니다. 이는 스팸/피싱 의심을 줄이고 더 안전한 인증 경험을 제공합니다.
|
||||||
|
|
||||||
|
## ✨ 주요 기능
|
||||||
|
|
||||||
|
### 1. 회원가입 이메일 인증
|
||||||
|
- 회원가입 시 입력한 이메일로 6자리 인증 코드 발송
|
||||||
|
- 코드 입력 및 검증 후 회원가입 완료
|
||||||
|
- 코드 유효 시간: 5분
|
||||||
|
- 최대 5회 시도 가능
|
||||||
|
|
||||||
|
### 2. 비밀번호 재설정 이메일 인증
|
||||||
|
- 비밀번호 찾기 시 이메일로 6자리 인증 코드 발송
|
||||||
|
- 코드 검증 후 비밀번호 재설정 페이지로 이동
|
||||||
|
- 코드 유효 시간: 5분
|
||||||
|
- 재설정 페이지 접근 유효 시간: 10분
|
||||||
|
|
||||||
|
### 3. 코드 재전송
|
||||||
|
- 코드가 만료되거나 받지 못한 경우 재전송 가능
|
||||||
|
- 재전송은 첫 발송 후 30초 경과 시 가능
|
||||||
|
|
||||||
|
## 🎨 UI/UX 특징
|
||||||
|
|
||||||
|
### 1. 6자리 코드 입력
|
||||||
|
- 각 자리별 개별 입력 필드
|
||||||
|
- 자동으로 다음 칸으로 포커스 이동
|
||||||
|
- 붙여넣기 지원 (6자리 숫자 자동 분배)
|
||||||
|
- 백스페이스로 이전 칸으로 이동
|
||||||
|
|
||||||
|
### 2. 실시간 타이머
|
||||||
|
- 남은 시간을 시각적으로 표시 (분:초)
|
||||||
|
- 시간 만료 시 경고 메시지
|
||||||
|
- 재전송 버튼 활성화 타이밍 표시
|
||||||
|
|
||||||
|
### 3. 에러 처리
|
||||||
|
- 잘못된 코드 입력 시 shake 애니메이션
|
||||||
|
- 남은 시도 횟수 표시
|
||||||
|
- 명확한 에러 메시지
|
||||||
|
|
||||||
|
## 🏗️ 아키텍처
|
||||||
|
|
||||||
|
### Frontend Component: `EmailCodeVerification.vue`
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
```typescript
|
||||||
|
interface Props {
|
||||||
|
email: string // 인증 대상 이메일
|
||||||
|
title?: string // 제목 (기본값: '이메일 인증')
|
||||||
|
expirySeconds?: number // 만료 시간 (기본값: 300초 = 5분)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Events
|
||||||
|
```typescript
|
||||||
|
// 코드 검증 요청
|
||||||
|
emit('verify', code: string)
|
||||||
|
|
||||||
|
// 코드 재전송 요청
|
||||||
|
emit('resend')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 사용 예시
|
||||||
|
```vue
|
||||||
|
<EmailCodeVerification
|
||||||
|
:email="formData.email"
|
||||||
|
title="회원가입 이메일 인증"
|
||||||
|
@verify="handleCodeVerify"
|
||||||
|
@resend="handleCodeResend"
|
||||||
|
ref="codeVerificationRef"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend API
|
||||||
|
|
||||||
|
#### 1. 코드 발송
|
||||||
|
```
|
||||||
|
POST /api/email/send-code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@humetro.busan.kr",
|
||||||
|
"type": "signup" // 또는 "password_reset"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "인증 코드를 이메일로 전송했습니다.",
|
||||||
|
"debug_code": "123456" // 개발 환경에서만
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 코드 검증
|
||||||
|
```
|
||||||
|
POST /api/email/verify-code
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@humetro.busan.kr",
|
||||||
|
"code": "123456",
|
||||||
|
"type": "signup" // 또는 "password_reset"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (성공):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "인증이 완료되었습니다."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (실패):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "인증 코드가 올바르지 않습니다. (남은 시도: 3회)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 보안 기능
|
||||||
|
|
||||||
|
### 1. 코드 생성
|
||||||
|
- 6자리 무작위 숫자 생성 (`random.choices(string.digits, k=6)`)
|
||||||
|
- 각 이메일/타입별 고유 세션 키 사용
|
||||||
|
- 코드는 세션에 암호화되어 저장
|
||||||
|
|
||||||
|
### 2. 검증 제한
|
||||||
|
- **시도 횟수 제한**: 5회까지만 시도 가능
|
||||||
|
- **시간 제한**: 5분 후 자동 만료
|
||||||
|
- **일회성**: 한 번 검증되면 코드 즉시 삭제
|
||||||
|
|
||||||
|
### 3. 도메인 제한
|
||||||
|
- `@humetro.busan.kr` 도메인만 허용
|
||||||
|
- 백엔드에서 이메일 도메인 검증
|
||||||
|
|
||||||
|
### 4. 세션 관리
|
||||||
|
```python
|
||||||
|
# 코드 저장 (5분간 유효)
|
||||||
|
session[f"email_code_{email}_{type}"] = {
|
||||||
|
"code": "123456",
|
||||||
|
"expiry": timestamp + 300,
|
||||||
|
"attempts": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 검증 완료 표시 (10분간 유효)
|
||||||
|
session[f"email_verified_{email}_{type}"] = {
|
||||||
|
"verified_at": timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 사용 흐름
|
||||||
|
|
||||||
|
### 회원가입 흐름
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant F as Frontend
|
||||||
|
participant B as Backend
|
||||||
|
participant E as Email
|
||||||
|
|
||||||
|
U->>F: 회원가입 정보 입력
|
||||||
|
F->>B: POST /api/email/send-code
|
||||||
|
B->>B: 6자리 코드 생성
|
||||||
|
B->>E: 이메일 발송 (TODO)
|
||||||
|
B->>F: {success: true}
|
||||||
|
F->>U: 코드 입력 화면 표시
|
||||||
|
U->>F: 코드 입력
|
||||||
|
F->>B: POST /api/email/verify-code
|
||||||
|
B->>B: 코드 검증
|
||||||
|
B->>F: {success: true}
|
||||||
|
F->>B: POST /auth/signup
|
||||||
|
B->>F: 회원가입 완료
|
||||||
|
F->>U: 로그인 페이지로 이동
|
||||||
|
```
|
||||||
|
|
||||||
|
### 비밀번호 재설정 흐름
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant F as Frontend
|
||||||
|
participant B as Backend
|
||||||
|
|
||||||
|
U->>F: 이메일 입력
|
||||||
|
F->>B: POST /api/email/send-code (type: password_reset)
|
||||||
|
B->>F: {success: true}
|
||||||
|
F->>U: 코드 입력 화면
|
||||||
|
U->>F: 코드 입력
|
||||||
|
F->>B: POST /api/email/verify-code
|
||||||
|
B->>B: 세션에 인증 완료 표시
|
||||||
|
B->>F: {success: true}
|
||||||
|
F->>F: /reset-password?email=...&verified=true
|
||||||
|
U->>F: 새 비밀번호 입력
|
||||||
|
F->>B: POST /api/auth/reset-password
|
||||||
|
B->>B: 세션에서 인증 확인
|
||||||
|
B->>B: 비밀번호 업데이트
|
||||||
|
B->>F: {success: true}
|
||||||
|
F->>U: 로그인 페이지로 이동
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 회원가입 통합
|
||||||
|
|
||||||
|
### SignupView.vue
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 이메일 코드 인증 단계 -->
|
||||||
|
<EmailCodeVerification
|
||||||
|
v-if="showCodeVerification"
|
||||||
|
:email="formData.email"
|
||||||
|
title="회원가입 이메일 인증"
|
||||||
|
@verify="handleCodeVerify"
|
||||||
|
@resend="handleCodeResend"
|
||||||
|
ref="codeVerificationRef"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 회원가입 폼 -->
|
||||||
|
<form v-else @submit.prevent="handleSignup">
|
||||||
|
<!-- 폼 필드들... -->
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const showCodeVerification = ref(false)
|
||||||
|
|
||||||
|
async function handleSignup() {
|
||||||
|
// 유효성 검증...
|
||||||
|
|
||||||
|
// 이메일 코드 발송
|
||||||
|
const sent = await sendEmailCode()
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
// 코드 인증 화면으로 전환
|
||||||
|
showCodeVerification.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCodeVerify(code: string) {
|
||||||
|
const response = await fetch('/api/email/verify-code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: formData.value.email,
|
||||||
|
code,
|
||||||
|
type: 'signup'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 회원가입 완료
|
||||||
|
await completeSignup()
|
||||||
|
} else {
|
||||||
|
// 에러 표시
|
||||||
|
codeVerificationRef.value?.setError(data.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 비밀번호 재설정 통합
|
||||||
|
|
||||||
|
### ForgotPasswordView.vue → ResetPasswordView.vue
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ForgotPasswordView.vue -->
|
||||||
|
<template>
|
||||||
|
<EmailCodeVerification
|
||||||
|
v-if="showCodeVerification"
|
||||||
|
:email="email"
|
||||||
|
title="비밀번호 재설정 인증"
|
||||||
|
@verify="handleCodeVerify"
|
||||||
|
@resend="handleCodeResend"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form v-else @submit.prevent="handleSubmit">
|
||||||
|
<input v-model="email" type="email" />
|
||||||
|
<button type="submit">코드 전송</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
async function handleCodeVerify(code: string) {
|
||||||
|
const response = await fetch('/api/email/verify-code', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value,
|
||||||
|
code,
|
||||||
|
type: 'password_reset'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 비밀번호 재설정 페이지로 이동
|
||||||
|
router.push({
|
||||||
|
path: '/reset-password',
|
||||||
|
query: { email: email.value, verified: 'true' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ResetPasswordView.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
onMounted(() => {
|
||||||
|
const queryEmail = route.query.email as string
|
||||||
|
const verified = route.query.verified as string
|
||||||
|
|
||||||
|
if (!queryEmail || verified !== 'true') {
|
||||||
|
// 인증되지 않은 접근 차단
|
||||||
|
errorMessage.value = '잘못된 접근입니다.'
|
||||||
|
router.push('/forgot-password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const response = await fetch('/api/auth/reset-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email.value,
|
||||||
|
password: password.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 백엔드에서 세션의 인증 여부를 확인
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 개발 환경 설정
|
||||||
|
|
||||||
|
### 1. 이메일 발송 (TODO)
|
||||||
|
|
||||||
|
현재는 콘솔에만 코드를 출력하고, 개발 환경에서는 API 응답에 코드를 포함시킵니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 개발 환경에서만
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "인증 코드를 이메일로 전송했습니다.",
|
||||||
|
"debug_code": code if app.debug else None
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
프로덕션에서는 실제 이메일 발송 서비스 연동 필요:
|
||||||
|
- SMTP 서버
|
||||||
|
- SendGrid / AWS SES
|
||||||
|
- Supabase Email (Docker 환경에서는 제한적)
|
||||||
|
|
||||||
|
### 2. 콘솔에서 코드 확인
|
||||||
|
|
||||||
|
백엔드 콘솔:
|
||||||
|
```
|
||||||
|
=== 이메일 인증 코드 ===
|
||||||
|
To: user@humetro.busan.kr
|
||||||
|
Code: 123456
|
||||||
|
Type: signup
|
||||||
|
Expiry: 2025-10-14 15:30:00
|
||||||
|
=======================
|
||||||
|
```
|
||||||
|
|
||||||
|
프론트엔드 콘솔:
|
||||||
|
```
|
||||||
|
=== 개발 환경 인증 코드 ===
|
||||||
|
코드: 123456
|
||||||
|
=======================
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 에러 처리
|
||||||
|
|
||||||
|
### Frontend 에러 메시지
|
||||||
|
|
||||||
|
| 상황 | 메시지 |
|
||||||
|
|------|--------|
|
||||||
|
| 잘못된 코드 | "인증 코드가 올바르지 않습니다. (남은 시도: N회)" |
|
||||||
|
| 시도 횟수 초과 | "인증 시도 횟수를 초과했습니다. 코드를 재전송해주세요." |
|
||||||
|
| 코드 만료 | "인증 코드가 만료되었습니다. 코드를 재전송해주세요." |
|
||||||
|
| 코드 미존재 | "인증 코드가 존재하지 않거나 만료되었습니다." |
|
||||||
|
|
||||||
|
### Backend 에러 응답
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 시도 횟수 초과
|
||||||
|
if stored_data["attempts"] >= 5:
|
||||||
|
session.pop(session_key, None)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "인증 시도 횟수를 초과했습니다. 코드를 재전송해주세요."
|
||||||
|
}, 400
|
||||||
|
|
||||||
|
# 만료
|
||||||
|
if datetime.now().timestamp() > stored_data["expiry"]:
|
||||||
|
session.pop(session_key, None)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "인증 코드가 만료되었습니다. 코드를 재전송해주세요."
|
||||||
|
}, 400
|
||||||
|
|
||||||
|
# 불일치
|
||||||
|
if stored_data["code"] != code:
|
||||||
|
stored_data["attempts"] += 1
|
||||||
|
session[session_key] = stored_data
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"인증 코드가 올바르지 않습니다. (남은 시도: {5 - stored_data['attempts']}회)"
|
||||||
|
}, 400
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI 커스터마이징
|
||||||
|
|
||||||
|
### 다크모드 지원
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root.dark .code-digit {
|
||||||
|
background: #1a202c;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #f7fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .timer {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 애니메이션
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Shake 애니메이션 (에러 시) */
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-5px); }
|
||||||
|
75% { transform: translateX(5px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-digit.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
animation: shake 0.3s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 TODO (향후 개선사항)
|
||||||
|
|
||||||
|
### 1. 이메일 발송 구현
|
||||||
|
```python
|
||||||
|
# SMTP 또는 이메일 서비스 연동
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
def send_verification_email(email, code):
|
||||||
|
msg = MIMEText(f"인증 코드: {code}")
|
||||||
|
msg['Subject'] = '부산교통공사 1호선 - 이메일 인증'
|
||||||
|
msg['From'] = 'noreply@humetro.busan.kr'
|
||||||
|
msg['To'] = email
|
||||||
|
|
||||||
|
# SMTP 발송...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 이메일 템플릿
|
||||||
|
- HTML 이메일 템플릿 디자인
|
||||||
|
- 회사 로고 및 브랜딩 추가
|
||||||
|
- 다국어 지원
|
||||||
|
|
||||||
|
### 3. 보안 강화
|
||||||
|
- Rate limiting (IP별 요청 제한)
|
||||||
|
- Captcha 추가 (무차별 대입 공격 방지)
|
||||||
|
- 로그 기록 (audit_logs)
|
||||||
|
|
||||||
|
### 4. 사용자 경험 개선
|
||||||
|
- 이메일 자동 완성
|
||||||
|
- 코드 자동 감지 (SMS/Email OTP)
|
||||||
|
- 음성 안내 (접근성)
|
||||||
|
|
||||||
|
## 🔗 참고 자료
|
||||||
|
|
||||||
|
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||||
|
- [Email OTP Best Practices](https://www.twilio.com/docs/verify/email)
|
||||||
|
- [Flask Session Management](https://flask.palletsprojects.com/en/2.3.x/quickstart/#sessions)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
# 🔧 환경 변수 설정 가이드
|
||||||
|
|
||||||
|
## 📍 Flask 백엔드 `.env` 파일 설정
|
||||||
|
|
||||||
|
### ⚠️ 중요사항
|
||||||
|
|
||||||
|
**`SECRET_KEY`는 Flask 백엔드의 `.env` 파일에 설정합니다!**
|
||||||
|
|
||||||
|
```
|
||||||
|
Flask 백엔드: /home/ckh08045/Tr_Code/.env ← SECRET_KEY 여기에!
|
||||||
|
TWA 프론트엔드: /home/ckh08045/Tr_Code/twa-frontend/.env ← SECRET_KEY 불필요
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 빠른 설정 (3단계)
|
||||||
|
|
||||||
|
### 1단계: .env 파일 생성
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
cp env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: SECRET_KEY 생성
|
||||||
|
|
||||||
|
터미널에서 실행:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
출력 예시:
|
||||||
|
```
|
||||||
|
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3단계: .env 파일 수정
|
||||||
|
|
||||||
|
생성한 `.env` 파일을 열어서 `SECRET_KEY` 값을 변경:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 변경 전
|
||||||
|
SECRET_KEY=dev-secret-key-change-in-production
|
||||||
|
|
||||||
|
# 변경 후 (위에서 생성한 키 사용)
|
||||||
|
SECRET_KEY=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 .env 파일 전체 예시
|
||||||
|
|
||||||
|
**파일 위치**: `/home/ckh08045/Tr_Code/.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase 설정 (Docker 기반)
|
||||||
|
SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzU4NTUxNjY2LCJleHAiOjQxMDI0NDQ4MDB9.jMCGL3Q-N2o_l7JQE_HrO7Uoct86CMgLsVxpabisG4I
|
||||||
|
|
||||||
|
# Flask 세션 보안 키 (반드시 변경!)
|
||||||
|
SECRET_KEY=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345
|
||||||
|
|
||||||
|
# Kong Basic Auth (선택사항 - 사용하지 않으면 비워두기)
|
||||||
|
SUPABASE_BASIC_USER=
|
||||||
|
SUPABASE_BASIC_PASSWORD=
|
||||||
|
|
||||||
|
# Flask 서버 포트
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 설정 확인
|
||||||
|
|
||||||
|
### 1. .env 파일 존재 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /home/ckh08045/Tr_Code/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SECRET_KEY 설정 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep SECRET_KEY /home/ckh08045/Tr_Code/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
**올바른 설정**:
|
||||||
|
```
|
||||||
|
SECRET_KEY=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345
|
||||||
|
```
|
||||||
|
|
||||||
|
**잘못된 설정** (기본값 그대로):
|
||||||
|
```
|
||||||
|
SECRET_KEY=dev-secret-key-change-in-production ← 변경 필요!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Flask 앱 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
정상 실행 시 출력:
|
||||||
|
```
|
||||||
|
* Running on http://0.0.0.0:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 보안 체크리스트
|
||||||
|
|
||||||
|
### 개발 환경
|
||||||
|
- [x] `.env` 파일 생성
|
||||||
|
- [x] `SECRET_KEY` 값 변경 (기본값 사용 금지)
|
||||||
|
- [x] `SUPABASE_URL` 확인 (Docker: `http://localhost:8000`)
|
||||||
|
- [x] `.env` 파일이 `.gitignore`에 포함되어 있는지 확인
|
||||||
|
|
||||||
|
### 프로덕션 환경
|
||||||
|
- [ ] `SECRET_KEY`를 강력한 랜덤 키로 변경
|
||||||
|
- [ ] `SUPABASE_URL`을 프로덕션 URL로 변경
|
||||||
|
- [ ] `SUPABASE_ANON_KEY`를 프로덕션 키로 변경
|
||||||
|
- [ ] `.env` 파일 권한 설정 (`chmod 600 .env`)
|
||||||
|
- [ ] 환경 변수를 시스템 환경 변수로 설정 (선택사항)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 트러블슈팅
|
||||||
|
|
||||||
|
### 문제: "세션이 유지되지 않음"
|
||||||
|
|
||||||
|
**원인**: `SECRET_KEY`가 설정되지 않았거나 기본값 사용
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```bash
|
||||||
|
# 1. SECRET_KEY 확인
|
||||||
|
grep SECRET_KEY /home/ckh08045/Tr_Code/.env
|
||||||
|
|
||||||
|
# 2. 새 키 생성
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
|
||||||
|
# 3. .env 파일에 설정
|
||||||
|
nano /home/ckh08045/Tr_Code/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제: "Supabase 연결 실패"
|
||||||
|
|
||||||
|
**원인**: `SUPABASE_URL` 또는 `SUPABASE_ANON_KEY` 오류
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```bash
|
||||||
|
# 1. Docker Supabase 상태 확인
|
||||||
|
docker ps | grep supabase
|
||||||
|
|
||||||
|
# 2. Kong Gateway 포트 확인 (기본 8000)
|
||||||
|
curl http://localhost:8000/rest/v1/
|
||||||
|
|
||||||
|
# 3. .env 파일에서 URL 확인
|
||||||
|
grep SUPABASE_URL /home/ckh08045/Tr_Code/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제: ".env 파일을 찾을 수 없음"
|
||||||
|
|
||||||
|
**원인**: 잘못된 위치에 .env 파일 생성
|
||||||
|
|
||||||
|
**해결**:
|
||||||
|
```bash
|
||||||
|
# Flask 백엔드 디렉토리로 이동
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
|
||||||
|
# .env 파일 생성
|
||||||
|
cp env.example .env
|
||||||
|
|
||||||
|
# 파일 위치 확인 (app.py와 같은 디렉토리에 있어야 함)
|
||||||
|
ls -la | grep -E "(app.py|.env)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 참고
|
||||||
|
|
||||||
|
### Flask와 TWA 프론트엔드의 환경 변수 비교
|
||||||
|
|
||||||
|
| 항목 | Flask 백엔드 | TWA 프론트엔드 |
|
||||||
|
|------|-------------|---------------|
|
||||||
|
| 파일 위치 | `/home/ckh08045/Tr_Code/.env` | `/home/ckh08045/Tr_Code/twa-frontend/.env` |
|
||||||
|
| SECRET_KEY | ✅ **필수** | ❌ 불필요 |
|
||||||
|
| SUPABASE_URL | ✅ 필수 | ✅ 필요 (다를 수 있음) |
|
||||||
|
| SUPABASE_ANON_KEY | ✅ 필수 | ✅ 필요 |
|
||||||
|
| PORT | ✅ 선택 | ❌ 불필요 |
|
||||||
|
|
||||||
|
### 환경 변수 로드 방식
|
||||||
|
|
||||||
|
Flask (`app.py`):
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv() # .env 파일을 자동으로 로드
|
||||||
|
|
||||||
|
# 환경 변수 사용
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY", "default-key")
|
||||||
|
```
|
||||||
|
|
||||||
|
Vue.js (`twa-frontend`):
|
||||||
|
```javascript
|
||||||
|
// .env 파일의 VUE_APP_* 변수만 자동 로드
|
||||||
|
const apiUrl = process.env.VUE_APP_API_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 문의
|
||||||
|
|
||||||
|
환경 변수 설정 관련 문의는 시스템 관리자에게 연락하세요.
|
||||||
|
|
||||||
|
**부산교통공사 차량처**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
# 구현 완료 요약
|
||||||
|
|
||||||
|
## ✅ 완료된 작업
|
||||||
|
|
||||||
|
### 1. 생체인증 (Credential Management API)
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **`useBiometric.ts`** Composable 생성
|
||||||
|
- WebAuthn API 통합
|
||||||
|
- PublicKeyCredential 생성 및 검증
|
||||||
|
- Base64 URL 인코딩/디코딩 헬퍼 함수
|
||||||
|
- 상태 관리 (isSupported, isAvailable, isEnrolled)
|
||||||
|
- Challenge-Response 인증 플로우
|
||||||
|
|
||||||
|
- **LoginView.vue** 업데이트
|
||||||
|
- 생체인증 버튼 추가
|
||||||
|
- 지원 여부에 따른 조건부 렌더링
|
||||||
|
- 로그인 성공 시 생체인증 등록 제안
|
||||||
|
- 다크모드 스타일 지원
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- **생체인증 API 엔드포인트** 추가
|
||||||
|
- `/api/biometric/register-challenge` - Challenge 생성
|
||||||
|
- `/api/biometric/register` - Credential 등록
|
||||||
|
- `/api/biometric/login-challenge` - 로그인 Challenge
|
||||||
|
- `/api/biometric/login` - 생체인증 로그인
|
||||||
|
- `/api/biometric/unregister` - 생체인증 해제
|
||||||
|
|
||||||
|
- **database_schema.sql** 업데이트
|
||||||
|
- `biometric_credentials` 테이블 추가
|
||||||
|
- Credential ID, 데이터, 생성/사용 시간 관리
|
||||||
|
- 인덱스 최적화
|
||||||
|
|
||||||
|
#### 문서
|
||||||
|
- **BIOMETRIC_AUTH_GUIDE.md** 작성
|
||||||
|
- 아키텍처 설명
|
||||||
|
- API 문서
|
||||||
|
- 사용 방법
|
||||||
|
- TWA 환경 가이드
|
||||||
|
- 문제 해결 가이드
|
||||||
|
|
||||||
|
### 2. 이메일 코드 인증
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **`EmailCodeVerification.vue`** 컴포넌트 생성
|
||||||
|
- 6자리 코드 입력 UI
|
||||||
|
- 자동 포커스 이동
|
||||||
|
- 붙여넣기 지원
|
||||||
|
- 실시간 타이머 (5분)
|
||||||
|
- Shake 애니메이션 (에러 시)
|
||||||
|
- 다크모드 지원
|
||||||
|
|
||||||
|
- **SignupView.vue** 통합
|
||||||
|
- 회원가입 폼 → 코드 인증 → 회원가입 완료 플로우
|
||||||
|
- `sendEmailCode()` 함수 추가
|
||||||
|
- `handleCodeVerify()` 코드 검증
|
||||||
|
- `completeSignup()` 회원가입 완료
|
||||||
|
|
||||||
|
- **ForgotPasswordView.vue** 통합
|
||||||
|
- 이메일 입력 → 코드 인증 → 비밀번호 재설정 플로우
|
||||||
|
- 코드 검증 후 ResetPasswordView로 이동
|
||||||
|
|
||||||
|
- **ResetPasswordView.vue** 수정
|
||||||
|
- 이메일 인증 확인 (쿼리 파라미터)
|
||||||
|
- 인증되지 않은 접근 차단
|
||||||
|
- 비밀번호 재설정 API 호출
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- **이메일 코드 API 엔드포인트** 추가
|
||||||
|
- `/api/email/send-code` - 코드 발송
|
||||||
|
- `/api/email/verify-code` - 코드 검증
|
||||||
|
- `/api/auth/reset-password` - 비밀번호 재설정
|
||||||
|
|
||||||
|
- **보안 기능**
|
||||||
|
- 6자리 랜덤 숫자 코드 생성
|
||||||
|
- 세션 기반 코드 저장 (5분 유효)
|
||||||
|
- 시도 횟수 제한 (5회)
|
||||||
|
- 이메일 도메인 검증 (`@humetro.busan.kr`)
|
||||||
|
- 검증 완료 후 세션 표시 (10분 유효)
|
||||||
|
|
||||||
|
#### 문서
|
||||||
|
- **EMAIL_CODE_AUTH_GUIDE.md** 작성
|
||||||
|
- UI/UX 특징 설명
|
||||||
|
- 아키텍처 및 플로우 다이어그램
|
||||||
|
- API 문서
|
||||||
|
- 회원가입/비밀번호 재설정 통합 가이드
|
||||||
|
- 개발 환경 설정
|
||||||
|
- 에러 처리 가이드
|
||||||
|
|
||||||
|
### 3. 문서 업데이트
|
||||||
|
|
||||||
|
- **README.md** 업데이트
|
||||||
|
- 새로운 인증 기능 추가
|
||||||
|
- 문서 링크 정리
|
||||||
|
|
||||||
|
- **database_schema.sql** 업데이트
|
||||||
|
- `biometric_credentials` 테이블 추가
|
||||||
|
|
||||||
|
## 📁 생성/수정된 파일
|
||||||
|
|
||||||
|
### Frontend (Vue.js/TypeScript)
|
||||||
|
```
|
||||||
|
twa-frontend/src/
|
||||||
|
├── composables/
|
||||||
|
│ └── useBiometric.ts # ✨ NEW
|
||||||
|
├── components/
|
||||||
|
│ └── EmailCodeVerification.vue # ✨ NEW
|
||||||
|
└── views/
|
||||||
|
├── LoginView.vue # ✏️ MODIFIED
|
||||||
|
├── SignupView.vue # ✏️ MODIFIED
|
||||||
|
├── ForgotPasswordView.vue # ✏️ MODIFIED
|
||||||
|
└── ResetPasswordView.vue # ✏️ MODIFIED
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (Flask/Python)
|
||||||
|
```
|
||||||
|
/home/ckh08045/Tr_Code/
|
||||||
|
├── app.py # ✏️ MODIFIED
|
||||||
|
│ ├── 이메일 코드 API (+3 endpoints)
|
||||||
|
│ ├── 생체인증 API (+5 endpoints)
|
||||||
|
│ └── 비밀번호 재설정 API (+1 endpoint)
|
||||||
|
└── database_schema.sql # ✏️ MODIFIED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문서
|
||||||
|
```
|
||||||
|
/home/ckh08045/Tr_Code/
|
||||||
|
├── README.md # ✏️ MODIFIED
|
||||||
|
├── BIOMETRIC_AUTH_GUIDE.md # ✨ NEW
|
||||||
|
├── EMAIL_CODE_AUTH_GUIDE.md # ✨ NEW
|
||||||
|
└── IMPLEMENTATION_SUMMARY.md # ✨ NEW (this file)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 주요 기능
|
||||||
|
|
||||||
|
### 생체인증 (Biometric Authentication)
|
||||||
|
- ✅ WebAuthn/Credential Management API 활용
|
||||||
|
- ✅ 플랫폼 인증 (지문, 얼굴 인식)
|
||||||
|
- ✅ Challenge-Response 인증
|
||||||
|
- ✅ TWA 환경 지원
|
||||||
|
- ✅ 로그인 후 등록 제안
|
||||||
|
- ✅ 빠른 생체인증 로그인
|
||||||
|
|
||||||
|
### 이메일 코드 인증
|
||||||
|
- ✅ 6자리 숫자 코드 방식
|
||||||
|
- ✅ 스팸/피싱 의심 방지
|
||||||
|
- ✅ 5분 유효 시간
|
||||||
|
- ✅ 5회 시도 제한
|
||||||
|
- ✅ 실시간 타이머 UI
|
||||||
|
- ✅ 코드 재전송 기능
|
||||||
|
- ✅ 회원가입 및 비밀번호 재설정 통합
|
||||||
|
|
||||||
|
### 비밀번호 재설정
|
||||||
|
- ✅ 이메일 코드 인증
|
||||||
|
- ✅ 인증된 사용자만 재설정 가능
|
||||||
|
- ✅ 10분 세션 유효 시간
|
||||||
|
- ✅ 비밀번호 강도 표시
|
||||||
|
|
||||||
|
## 🔐 보안 강화
|
||||||
|
|
||||||
|
### 1. 생체인증
|
||||||
|
- Challenge는 세션에 저장 (10분 유효)
|
||||||
|
- Credential ID 검증 (향후 서명 검증 추가 필요)
|
||||||
|
- 플랫폼 인증 강제 (`authenticatorAttachment: 'platform'`)
|
||||||
|
- 사용자 검증 필수 (`userVerification: 'required'`)
|
||||||
|
|
||||||
|
### 2. 이메일 코드 인증
|
||||||
|
- 무작위 6자리 숫자 생성
|
||||||
|
- 세션 기반 저장 (5분 유효)
|
||||||
|
- 시도 횟수 제한 (5회)
|
||||||
|
- 도메인 검증 (`@humetro.busan.kr`)
|
||||||
|
- 일회성 코드 (검증 후 즉시 삭제)
|
||||||
|
|
||||||
|
### 3. 비밀번호 재설정
|
||||||
|
- 이메일 인증 필수
|
||||||
|
- 세션에서 인증 여부 확인
|
||||||
|
- 10분 내 재설정 완료 필요
|
||||||
|
- 쿼리 파라미터 검증
|
||||||
|
|
||||||
|
## 🎯 사용 흐름
|
||||||
|
|
||||||
|
### 신규 사용자
|
||||||
|
1. 회원가입 버튼 클릭
|
||||||
|
2. 정보 입력 (사번, 이름, 부서, 이메일, 비밀번호)
|
||||||
|
3. 이메일로 6자리 코드 수신
|
||||||
|
4. 코드 입력 및 검증
|
||||||
|
5. 회원가입 완료
|
||||||
|
6. 로그인
|
||||||
|
7. (선택) 생체인증 등록
|
||||||
|
|
||||||
|
### 기존 사용자 (생체인증 등록)
|
||||||
|
1. 로그인
|
||||||
|
2. "생체인증을 등록하시겠습니까?" 확인
|
||||||
|
3. 생체인증 실행 (지문/얼굴)
|
||||||
|
4. 등록 완료
|
||||||
|
5. 다음 로그인부터 생체인증 사용
|
||||||
|
|
||||||
|
### 비밀번호 재설정
|
||||||
|
1. "비밀번호 찾기" 클릭
|
||||||
|
2. 이메일 입력
|
||||||
|
3. 6자리 코드 수신
|
||||||
|
4. 코드 입력 및 검증
|
||||||
|
5. 새 비밀번호 입력
|
||||||
|
6. 재설정 완료
|
||||||
|
|
||||||
|
## 📊 API 엔드포인트 추가
|
||||||
|
|
||||||
|
### 이메일 코드 인증
|
||||||
|
```
|
||||||
|
POST /api/email/send-code
|
||||||
|
POST /api/email/verify-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### 생체인증
|
||||||
|
```
|
||||||
|
POST /api/biometric/register-challenge
|
||||||
|
POST /api/biometric/register
|
||||||
|
POST /api/biometric/login-challenge
|
||||||
|
POST /api/biometric/login
|
||||||
|
POST /api/biometric/unregister
|
||||||
|
```
|
||||||
|
|
||||||
|
### 비밀번호 재설정
|
||||||
|
```
|
||||||
|
POST /api/auth/reset-password
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗄️ 데이터베이스 변경
|
||||||
|
|
||||||
|
### 새 테이블: `biometric_credentials`
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS public.biometric_credentials (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
employee_id VARCHAR(50) NOT NULL,
|
||||||
|
credential_id VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
credential_data JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
UNIQUE(employee_id, credential_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 배포 가이드
|
||||||
|
|
||||||
|
### 1. 데이터베이스 마이그레이션
|
||||||
|
```bash
|
||||||
|
# Supabase SQL 에디터에서 실행
|
||||||
|
# database_schema.sql의 biometric_credentials 테이블 부분 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 환경 변수 확인
|
||||||
|
```bash
|
||||||
|
# .env 파일에 SECRET_KEY 설정 확인
|
||||||
|
SECRET_KEY=your_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 프론트엔드 빌드
|
||||||
|
```bash
|
||||||
|
cd twa-frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 서버 재시작
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart Tr_Code
|
||||||
|
sudo systemctl status Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 테스트
|
||||||
|
1. 로그인 페이지에서 생체인증 버튼 확인
|
||||||
|
2. 회원가입에서 이메일 코드 인증 테스트
|
||||||
|
3. 비밀번호 재설정 플로우 테스트
|
||||||
|
4. 개발 환경에서 콘솔로 인증 코드 확인
|
||||||
|
|
||||||
|
## 📝 TODO (향후 개선사항)
|
||||||
|
|
||||||
|
### 이메일 발송
|
||||||
|
- [ ] 실제 SMTP/이메일 서비스 연동
|
||||||
|
- [ ] HTML 이메일 템플릿 작성
|
||||||
|
- [ ] 이메일 발송 로그 기록
|
||||||
|
|
||||||
|
### 생체인증
|
||||||
|
- [ ] 서명 검증 구현 (공개키 암호화)
|
||||||
|
- [ ] 여러 기기 지원 (다중 credential)
|
||||||
|
- [ ] Credential 관리 UI
|
||||||
|
- [ ] 마지막 사용 시간 업데이트
|
||||||
|
|
||||||
|
### 보안 강화
|
||||||
|
- [ ] Rate limiting (IP별 요청 제한)
|
||||||
|
- [ ] Captcha 추가
|
||||||
|
- [ ] 감사 로그 기록 (audit_logs)
|
||||||
|
- [ ] 2FA (Two-Factor Authentication) 옵션
|
||||||
|
|
||||||
|
### UX 개선
|
||||||
|
- [ ] 이메일 자동 완성
|
||||||
|
- [ ] SMS OTP 대안
|
||||||
|
- [ ] 생체인증 실패 시 대체 방법
|
||||||
|
- [ ] 접근성 개선 (음성 안내)
|
||||||
|
|
||||||
|
## 🎉 완료!
|
||||||
|
|
||||||
|
모든 요청사항이 성공적으로 구현되었습니다:
|
||||||
|
|
||||||
|
1. ✅ **Credential Management API로 생체인증 활성화**
|
||||||
|
- WebAuthn 기반 구현
|
||||||
|
- TWA 환경 지원
|
||||||
|
- 등록/로그인/해제 기능 완료
|
||||||
|
|
||||||
|
2. ✅ **이메일 인증을 링크 클릭에서 코드 입력으로 변경**
|
||||||
|
- 6자리 숫자 코드
|
||||||
|
- 회원가입 통합
|
||||||
|
- 비밀번호 재설정 통합
|
||||||
|
- 스팸/피싱 의심 방지
|
||||||
|
|
||||||
|
## 📚 관련 문서
|
||||||
|
|
||||||
|
- [BIOMETRIC_AUTH_GUIDE.md](./BIOMETRIC_AUTH_GUIDE.md) - 생체인증 상세 가이드
|
||||||
|
- [EMAIL_CODE_AUTH_GUIDE.md](./EMAIL_CODE_AUTH_GUIDE.md) - 이메일 코드 인증 상세 가이드
|
||||||
|
- [README.md](./README.md) - 프로젝트 메인 문서
|
||||||
|
- [AUTH_SETUP.md](./AUTH_SETUP.md) - 인증 시스템 설정
|
||||||
|
- [ENV_SETUP_GUIDE.md](./ENV_SETUP_GUIDE.md) - 환경 설정
|
||||||
|
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
# NPM (Nginx Proxy Manager) 설정 가이드
|
||||||
|
|
||||||
|
M1T 서버의 Tr_Code 애플리케이션을 외부에서 접근할 수 있도록 NPM을 설정하는 방법입니다.
|
||||||
|
|
||||||
|
## 📋 현재 서버 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
외부 인터넷
|
||||||
|
↓
|
||||||
|
R3 공유기 1번 포트
|
||||||
|
↓
|
||||||
|
Proxmox 서버
|
||||||
|
↓
|
||||||
|
NPM 컨테이너 (:80, :443)
|
||||||
|
↓ 프록시 패스
|
||||||
|
M1T 서버 (192.168.0.180:80)
|
||||||
|
↓ nginx 리버스 프록시
|
||||||
|
↓ Unix Socket
|
||||||
|
Gunicorn (Tr_Code.sock)
|
||||||
|
↓
|
||||||
|
Flask App
|
||||||
|
↓ localhost:8000
|
||||||
|
Supabase (Docker)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ M1T 서버 현재 설정
|
||||||
|
|
||||||
|
### 1. Gunicorn (WSGI 서버)
|
||||||
|
- **방식**: Unix Socket
|
||||||
|
- **위치**: `/home/ckh08045/Tr_Code/Tr_Code.sock`
|
||||||
|
- **Workers**: 3개
|
||||||
|
- **상태**: ✅ 실행 중
|
||||||
|
|
||||||
|
### 2. nginx (리버스 프록시)
|
||||||
|
- **포트**: 80 (HTTP)
|
||||||
|
- **설정**: `/etc/nginx/sites-enabled/tr_code`
|
||||||
|
- **소켓 연결**: `unix:/home/ckh08045/Tr_Code/Tr_Code.sock`
|
||||||
|
- **상태**: ✅ 실행 중
|
||||||
|
|
||||||
|
### 3. 내부 IP
|
||||||
|
- **IP**: `192.168.0.180`
|
||||||
|
- **포트**: `80`
|
||||||
|
|
||||||
|
## 🔧 NPM (Proxmox) 설정 방법
|
||||||
|
|
||||||
|
### 1. NPM 웹 인터페이스 접속
|
||||||
|
|
||||||
|
일반적으로 NPM은 다음 주소로 접속합니다:
|
||||||
|
```
|
||||||
|
http://proxmox-ip:81
|
||||||
|
```
|
||||||
|
|
||||||
|
기본 로그인:
|
||||||
|
- Email: `admin@example.com`
|
||||||
|
- Password: `changeme` (첫 로그인 후 변경)
|
||||||
|
|
||||||
|
### 2. Proxy Host 추가
|
||||||
|
|
||||||
|
**Dashboard** → **Hosts** → **Proxy Hosts** → **Add Proxy Host**
|
||||||
|
|
||||||
|
#### Details 탭
|
||||||
|
|
||||||
|
```
|
||||||
|
Domain Names:
|
||||||
|
- trcode.yourdomain.com
|
||||||
|
(또는 원하는 도메인/서브도메인)
|
||||||
|
|
||||||
|
Scheme: http
|
||||||
|
Forward Hostname / IP: 192.168.0.180
|
||||||
|
Forward Port: 80
|
||||||
|
|
||||||
|
☐ Cache Assets
|
||||||
|
☑ Block Common Exploits
|
||||||
|
☑ Websockets Support (선택사항, 필요시 체크)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSL 탭
|
||||||
|
|
||||||
|
```
|
||||||
|
SSL Certificate:
|
||||||
|
- Request a new SSL Certificate (Let's Encrypt)
|
||||||
|
또는
|
||||||
|
- 기존 인증서 선택
|
||||||
|
|
||||||
|
☑ Force SSL (권장)
|
||||||
|
☑ HTTP/2 Support (권장)
|
||||||
|
☑ HSTS Enabled (권장)
|
||||||
|
☑ HSTS Subdomains (선택사항)
|
||||||
|
|
||||||
|
이메일 입력:
|
||||||
|
- your-email@example.com
|
||||||
|
|
||||||
|
☑ I Agree to the Let's Encrypt Terms of Service
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Advanced 탭 (선택사항)
|
||||||
|
|
||||||
|
추가 nginx 설정이 필요한 경우:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# 클라이언트 업로드 크기 제한
|
||||||
|
client_max_body_size 10M;
|
||||||
|
|
||||||
|
# 타임아웃 설정 (필요시)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# 실제 클라이언트 IP 전달 (이미 기본 설정되어 있음)
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 저장 및 확인
|
||||||
|
|
||||||
|
**Save** 버튼 클릭 → SSL 인증서 자동 발급 (Let's Encrypt 사용 시)
|
||||||
|
|
||||||
|
## 🌐 접속 방법
|
||||||
|
|
||||||
|
### 외부 접속 (인터넷에서)
|
||||||
|
|
||||||
|
```
|
||||||
|
https://trcode.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 내부 네트워크 접속
|
||||||
|
|
||||||
|
#### 방법 1: M1T 서버 IP 직접 접근 (같은 네트워크)
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.0.180
|
||||||
|
```
|
||||||
|
|
||||||
|
- ✅ 빠름 (라우터 거치지 않음)
|
||||||
|
- ✅ SSL 없음 (내부망이므로 안전)
|
||||||
|
- ⚠️ M1T와 같은 네트워크에서만 가능
|
||||||
|
|
||||||
|
#### 방법 2: NPM을 통한 접근 (모든 내부망)
|
||||||
|
|
||||||
|
```
|
||||||
|
https://trcode.yourdomain.com
|
||||||
|
또는
|
||||||
|
http://npm-server-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
- ✅ 도메인 사용
|
||||||
|
- ✅ SSL 적용
|
||||||
|
- ⚠️ 약간의 오버헤드 (NPM 경유)
|
||||||
|
|
||||||
|
#### 방법 3: hosts 파일 수정 (개발용)
|
||||||
|
|
||||||
|
내부 네트워크에서 도메인 이름으로 접근하려면:
|
||||||
|
|
||||||
|
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
|
||||||
|
**Linux/Mac**: `/etc/hosts`
|
||||||
|
|
||||||
|
```
|
||||||
|
192.168.0.180 trcode.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
그 후:
|
||||||
|
```
|
||||||
|
http://trcode.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 테스트
|
||||||
|
|
||||||
|
### 1. 내부 직접 접근 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# M1T 서버에서
|
||||||
|
curl http://localhost/health
|
||||||
|
|
||||||
|
# 같은 네트워크 다른 컴퓨터에서
|
||||||
|
curl http://192.168.0.180/health
|
||||||
|
|
||||||
|
# 응답 예시
|
||||||
|
{"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. NPM을 통한 접근 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 외부/내부 어디서든
|
||||||
|
curl https://trcode.yourdomain.com/health
|
||||||
|
|
||||||
|
# 응답 예시
|
||||||
|
{"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Supabase 연결 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://192.168.0.180/sb/health
|
||||||
|
|
||||||
|
# 응답 예시
|
||||||
|
{"sb":"ok","url":"http://localhost:8000"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛡️ 보안 설정 (권장)
|
||||||
|
|
||||||
|
### 1. 방화벽 설정 (M1T 서버)
|
||||||
|
|
||||||
|
외부에서 M1T 80 포트 직접 접근을 차단하고, NPM만 허용:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ufw 방화벽 사용 시
|
||||||
|
sudo ufw status
|
||||||
|
|
||||||
|
# NPM IP만 허용 (예: 192.168.0.100)
|
||||||
|
sudo ufw allow from 192.168.0.100 to any port 80
|
||||||
|
|
||||||
|
# 또는 같은 서브넷 전체 허용
|
||||||
|
sudo ufw allow from 192.168.0.0/24 to any port 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. nginx 추가 보안 설정
|
||||||
|
|
||||||
|
`/etc/nginx/sites-available/tr_code`에 추가:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# 특정 IP만 허용 (선택사항)
|
||||||
|
# allow 192.168.0.0/24;
|
||||||
|
# deny all;
|
||||||
|
|
||||||
|
# 보안 헤더
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 적용
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 모바일/PWA 접속
|
||||||
|
|
||||||
|
### Android/iOS 브라우저
|
||||||
|
|
||||||
|
```
|
||||||
|
https://trcode.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### PWA 설치 (홈 화면에 추가)
|
||||||
|
|
||||||
|
**Android (Chrome)**:
|
||||||
|
1. 사이트 접속
|
||||||
|
2. 메뉴 (⋮) → "홈 화면에 추가"
|
||||||
|
3. 이름 입력 → 추가
|
||||||
|
|
||||||
|
**iOS (Safari)**:
|
||||||
|
1. 사이트 접속
|
||||||
|
2. 공유 버튼 (□↑) → "홈 화면에 추가"
|
||||||
|
3. 이름 입력 → 추가
|
||||||
|
|
||||||
|
## 🔧 문제 해결
|
||||||
|
|
||||||
|
### 502 Bad Gateway (NPM에서)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# M1T에서 nginx 확인
|
||||||
|
sudo systemctl status nginx
|
||||||
|
curl http://localhost/health
|
||||||
|
|
||||||
|
# 방화벽 확인
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 503 Service Unavailable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# M1T에서 Tr_Code 서비스 확인
|
||||||
|
sudo systemctl status Tr_Code
|
||||||
|
|
||||||
|
# 재시작
|
||||||
|
sudo systemctl restart Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL 인증서 발급 실패 (NPM)
|
||||||
|
|
||||||
|
1. **도메인이 NPM IP를 가리키는지 확인**
|
||||||
|
```bash
|
||||||
|
nslookup trcode.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **80 포트가 열려있는지 확인**
|
||||||
|
- Let's Encrypt는 80 포트로 인증
|
||||||
|
|
||||||
|
3. **NPM 로그 확인**
|
||||||
|
- Dashboard → System → Logs
|
||||||
|
|
||||||
|
### 내부 IP 접근 안됨
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# M1T에서 nginx 포트 확인
|
||||||
|
sudo netstat -tlnp | grep :80
|
||||||
|
|
||||||
|
# 방화벽 확인
|
||||||
|
sudo ufw status
|
||||||
|
|
||||||
|
# ping 테스트
|
||||||
|
ping 192.168.0.180
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 성능 비교
|
||||||
|
|
||||||
|
| 접속 방법 | 속도 | SSL | 외부 접근 | 추천 |
|
||||||
|
|----------|------|-----|----------|------|
|
||||||
|
| 내부 직접 (192.168.0.180) | ⚡ 가장 빠름 | ❌ | ❌ | 내부 개발/테스트 |
|
||||||
|
| NPM 경유 (도메인) | 🔥 빠름 | ✅ | ✅ | 운영 환경 |
|
||||||
|
| localhost (M1T에서) | ⚡⚡ 즉시 | ❌ | ❌ | 서버 직접 작업 |
|
||||||
|
|
||||||
|
## 🎯 권장 설정
|
||||||
|
|
||||||
|
### 개발/테스트 환경
|
||||||
|
```
|
||||||
|
내부 네트워크: http://192.168.0.180
|
||||||
|
M1T 서버: http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### 운영 환경
|
||||||
|
```
|
||||||
|
모든 접속: https://trcode.yourdomain.com (NPM 경유)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 NPM 추가 설정 예시
|
||||||
|
|
||||||
|
### 여러 서브도메인 설정
|
||||||
|
|
||||||
|
```
|
||||||
|
trcode.yourdomain.com → 192.168.0.180:80 (Tr_Code)
|
||||||
|
supabase.yourdomain.com → 192.168.0.180:8000 (Supabase)
|
||||||
|
api.yourdomain.com → 192.168.0.180:3000 (다른 API)
|
||||||
|
```
|
||||||
|
|
||||||
|
각각 별도의 Proxy Host로 추가하면 됩니다.
|
||||||
|
|
||||||
|
### 로드 밸런싱 (미래 확장)
|
||||||
|
|
||||||
|
M1T 서버가 여러 대가 될 경우:
|
||||||
|
```
|
||||||
|
trcode.yourdomain.com →
|
||||||
|
- 192.168.0.180:80 (M1T-1)
|
||||||
|
- 192.168.0.181:80 (M1T-2)
|
||||||
|
- 192.168.0.182:80 (M1T-3)
|
||||||
|
```
|
||||||
|
|
||||||
|
NPM에서 자동 로드 밸런싱 지원합니다.
|
||||||
|
|
||||||
|
## 💡 팁
|
||||||
|
|
||||||
|
### 1. 도메인이 없는 경우
|
||||||
|
|
||||||
|
무료 도메인 서비스 사용:
|
||||||
|
- Duck DNS (https://www.duckdns.org)
|
||||||
|
- No-IP (https://www.noip.com)
|
||||||
|
- FreeDNS (https://freedns.afraid.org)
|
||||||
|
|
||||||
|
예: `trcode.duckdns.org`
|
||||||
|
|
||||||
|
### 2. 동적 IP 문제
|
||||||
|
|
||||||
|
가정용 인터넷은 IP가 변경될 수 있으므로:
|
||||||
|
- DDNS (Dynamic DNS) 사용
|
||||||
|
- Duck DNS, No-IP 등에서 자동 업데이트 클라이언트 제공
|
||||||
|
|
||||||
|
### 3. 포트 포워딩 (R3 공유기)
|
||||||
|
|
||||||
|
외부 접근을 위해 R3 공유기 설정:
|
||||||
|
```
|
||||||
|
외부 포트 80 → Proxmox IP:80
|
||||||
|
외부 포트 443 → Proxmox IP:443
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 관련 문서
|
||||||
|
|
||||||
|
- [README.md](README.md) - 전체 설치 가이드
|
||||||
|
- [PWA_README.md](PWA_README.md) - 안드로이드 앱 변환 가이드
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-10-13
|
||||||
|
**M1T 서버 IP**: 192.168.0.180
|
||||||
|
**현재 포트**: 80 (nginx) → Unix Socket (gunicorn)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,586 @@
|
||||||
|
# PWA (Progressive Web App) 가이드
|
||||||
|
|
||||||
|
이 문서는 Tr_Code 웹 애플리케이션을 안드로이드 앱으로 패키징하여 Google Play Store에 배포하는 방법을 설명합니다.
|
||||||
|
|
||||||
|
## 📋 목차
|
||||||
|
|
||||||
|
- [PWA란?](#pwa란)
|
||||||
|
- [TWA (Trusted Web Activity)](#twa-trusted-web-activity)
|
||||||
|
- [준비사항](#준비사항)
|
||||||
|
- [Android Studio 설정](#android-studio-설정)
|
||||||
|
- [앱 빌드](#앱-빌드)
|
||||||
|
- [Play Store 배포](#play-store-배포)
|
||||||
|
- [오프라인 기능](#오프라인-기능)
|
||||||
|
- [FAQ](#faq)
|
||||||
|
|
||||||
|
## 🌐 PWA란?
|
||||||
|
|
||||||
|
**Progressive Web App(PWA)**는 웹 기술로 만들어졌지만 네이티브 앱처럼 동작하는 애플리케이션입니다.
|
||||||
|
|
||||||
|
### PWA의 장점
|
||||||
|
|
||||||
|
✅ **설치 가능** - 홈 화면에 아이콘 추가
|
||||||
|
✅ **오프라인 작동** - 네트워크 없이도 사용 가능
|
||||||
|
✅ **빠른 로딩** - 캐시를 통한 빠른 시작
|
||||||
|
✅ **푸시 알림** - 사용자에게 알림 전송
|
||||||
|
✅ **자동 업데이트** - 별도 앱 업데이트 불필요
|
||||||
|
✅ **크로스 플랫폼** - Android, iOS, Desktop 모두 지원
|
||||||
|
|
||||||
|
### Tr_Code PWA 특징
|
||||||
|
|
||||||
|
- **완전한 오프라인 지원**: 한 번 방문 후 네트워크 없이도 사용 가능
|
||||||
|
- **빠른 응답속도**: HTMX 기반 부분 렌더링
|
||||||
|
- **모바일 최적화**: 반응형 디자인
|
||||||
|
- **다크모드**: 자동 다크모드 지원
|
||||||
|
|
||||||
|
## 📱 TWA (Trusted Web Activity)
|
||||||
|
|
||||||
|
**TWA**는 PWA를 안드로이드 네이티브 앱으로 패키징하는 기술입니다.
|
||||||
|
|
||||||
|
### TWA vs WebView
|
||||||
|
|
||||||
|
| 특징 | TWA | WebView |
|
||||||
|
|------|-----|---------|
|
||||||
|
| 성능 | Chrome 브라우저 엔진 사용 (빠름) | 앱 내장 (느림) |
|
||||||
|
| 업데이트 | 자동 (서버만 업데이트) | 앱 재배포 필요 |
|
||||||
|
| 보안 | Chrome 보안 정책 적용 | 개발자 구현 필요 |
|
||||||
|
| 캐시 | Service Worker | 수동 구현 |
|
||||||
|
| 크기 | 작음 (~2MB) | 큼 (WebView 포함) |
|
||||||
|
|
||||||
|
## 🛠️ 준비사항
|
||||||
|
|
||||||
|
### 1. 도메인 및 HTTPS
|
||||||
|
|
||||||
|
PWA는 HTTPS가 **필수**입니다 (localhost 제외).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 현재 구조
|
||||||
|
http://your-domain.com → NPM (Proxmox) → M1T nginx → App
|
||||||
|
```
|
||||||
|
|
||||||
|
NPM(Nginx Proxy Manager)에서 SSL 인증서를 설정하세요:
|
||||||
|
- Let's Encrypt 자동 발급
|
||||||
|
- 또는 기존 인증서 업로드
|
||||||
|
|
||||||
|
### 2. Android Studio
|
||||||
|
|
||||||
|
[Android Studio 다운로드](https://developer.android.com/studio)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 시스템 요구사항
|
||||||
|
- OS: Windows 10/11, macOS, Linux
|
||||||
|
- RAM: 8GB 이상 권장
|
||||||
|
- 저장공간: 10GB 이상
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Java Development Kit (JDK)
|
||||||
|
|
||||||
|
Android Studio에 포함되어 있지만, 별도 설치도 가능:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Linux
|
||||||
|
sudo apt install openjdk-17-jdk
|
||||||
|
|
||||||
|
# 확인
|
||||||
|
java -version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Google Play Console 계정
|
||||||
|
|
||||||
|
- [Google Play Console](https://play.google.com/console) 가입
|
||||||
|
- **일회성 등록 비용**: $25 (평생 사용)
|
||||||
|
- 개발자 계정 승인까지 1-2일 소요
|
||||||
|
|
||||||
|
## 🏗️ Android Studio 설정
|
||||||
|
|
||||||
|
### 1. 프로젝트 생성
|
||||||
|
|
||||||
|
이미 `build.gradle` 파일이 있으므로 다음 단계를 진행합니다.
|
||||||
|
|
||||||
|
### 2. 기존 프로젝트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
Tr_Code/
|
||||||
|
├── app/ # Android 앱 소스
|
||||||
|
│ ├── src/
|
||||||
|
│ │ └── main/
|
||||||
|
│ │ ├── AndroidManifest.xml
|
||||||
|
│ │ ├── res/ # 리소스 (아이콘, 문자열 등)
|
||||||
|
│ │ └── java/ # Java/Kotlin 코드
|
||||||
|
├── build.gradle # 프로젝트 빌드 설정
|
||||||
|
├── settings.gradle # 프로젝트 설정
|
||||||
|
└── twa-manifest.json # TWA 설정
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. twa-manifest.json 설정
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageId": "com.trcode.app",
|
||||||
|
"host": "your-domain.com",
|
||||||
|
"name": "1호선 고장코드",
|
||||||
|
"launcherName": "고장코드",
|
||||||
|
"display": "standalone",
|
||||||
|
"themeColor": "#1f2937",
|
||||||
|
"backgroundColor": "#111827",
|
||||||
|
"enableNotifications": false,
|
||||||
|
"startUrl": "/",
|
||||||
|
"iconUrl": "https://your-domain.com/static/icon.png",
|
||||||
|
"maskableIconUrl": "https://your-domain.com/static/icon-maskable.png",
|
||||||
|
"splashScreenFadeOutDuration": 300,
|
||||||
|
"signingKey": {
|
||||||
|
"path": "android.keystore",
|
||||||
|
"alias": "trcode"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"locationDelegation": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"playBilling": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. AndroidManifest.xml 주요 설정
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.trcode.app">
|
||||||
|
|
||||||
|
<!-- 권한 -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="1호선 고장코드"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:theme="@style/Theme.TrCode">
|
||||||
|
|
||||||
|
<!-- TWA Activity -->
|
||||||
|
<activity
|
||||||
|
android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Digital Asset Links (중요!) -->
|
||||||
|
<meta-data
|
||||||
|
android:name="asset_statements"
|
||||||
|
android:resource="@string/asset_statements" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Digital Asset Links 설정
|
||||||
|
|
||||||
|
TWA가 작동하려면 **도메인과 앱을 연결**해야 합니다.
|
||||||
|
|
||||||
|
#### 서버 측: `.well-known/assetlinks.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# M1T 서버에 파일 생성
|
||||||
|
sudo mkdir -p /home/ckh08045/Tr_Code/static/.well-known
|
||||||
|
sudo nano /home/ckh08045/Tr_Code/static/.well-known/assetlinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "com.trcode.app",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"YOUR_SHA256_FINGERPRINT_HERE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
**SHA256 Fingerprint 얻는 방법:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 키스토어에서 추출
|
||||||
|
keytool -list -v -keystore android.keystore -alias trcode
|
||||||
|
|
||||||
|
# 출력에서 SHA256 찾기
|
||||||
|
# 예: AA:BB:CC:DD:EE:FF:...
|
||||||
|
# 콜론(:) 제거하고 입력
|
||||||
|
```
|
||||||
|
|
||||||
|
#### nginx 설정 추가
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/tr_code에 추가
|
||||||
|
location /.well-known/assetlinks.json {
|
||||||
|
alias /home/ckh08045/Tr_Code/static/.well-known/assetlinks.json;
|
||||||
|
default_type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# nginx 재시작
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
# 테스트
|
||||||
|
curl https://your-domain.com/.well-known/assetlinks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔨 앱 빌드
|
||||||
|
|
||||||
|
### 1. 키스토어 생성 (처음 한 번만)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
|
||||||
|
# 키스토어 생성
|
||||||
|
keytool -genkey -v -keystore android.keystore \
|
||||||
|
-alias trcode \
|
||||||
|
-keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
|
||||||
|
# 정보 입력
|
||||||
|
# - 비밀번호 (잘 기억하세요!)
|
||||||
|
# - 이름, 조직, 도시, 국가 등
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 중요**: `android.keystore` 파일과 비밀번호를 **안전하게 백업**하세요!
|
||||||
|
|
||||||
|
### 2. Gradle 빌드 설정
|
||||||
|
|
||||||
|
`build.gradle` 파일 확인:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
android {
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.trcode.app"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 33
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
storeFile file('android.keystore')
|
||||||
|
storePassword 'your_password'
|
||||||
|
keyAlias 'trcode'
|
||||||
|
keyPassword 'your_password'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 빌드 실행
|
||||||
|
|
||||||
|
**Android Studio에서:**
|
||||||
|
|
||||||
|
1. `Build` → `Select Build Variant` → `release` 선택
|
||||||
|
2. `Build` → `Build Bundle(s) / APK(s)` → `Build Bundle(s)`
|
||||||
|
3. 완료되면: `app/build/outputs/bundle/release/app-release.aab`
|
||||||
|
|
||||||
|
**명령줄에서:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
|
||||||
|
# Clean
|
||||||
|
./gradlew clean
|
||||||
|
|
||||||
|
# AAB (Play Store용) 빌드
|
||||||
|
./gradlew bundleRelease
|
||||||
|
|
||||||
|
# 또는 APK (테스트용) 빌드
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
# 출력 위치
|
||||||
|
# AAB: app/build/outputs/bundle/release/app-release.aab
|
||||||
|
# APK: app/build/outputs/apk/release/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# APK 설치 (USB 디버깅 활성화된 안드로이드 기기)
|
||||||
|
adb install app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
|
# 또는 Android Studio에서 Run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Play Store 배포
|
||||||
|
|
||||||
|
### 1. Google Play Console 준비
|
||||||
|
|
||||||
|
1. [Google Play Console](https://play.google.com/console) 로그인
|
||||||
|
2. `모든 앱` → `앱 만들기`
|
||||||
|
3. 앱 세부정보 입력:
|
||||||
|
- 앱 이름: `1호선 고장코드`
|
||||||
|
- 기본 언어: `한국어`
|
||||||
|
- 앱/게임: `앱`
|
||||||
|
- 무료/유료: `무료`
|
||||||
|
|
||||||
|
### 2. 앱 콘텐츠 작성
|
||||||
|
|
||||||
|
**개인정보처리방침**
|
||||||
|
- URL 제공 (필수)
|
||||||
|
- 예: `https://your-domain.com/privacy-policy`
|
||||||
|
|
||||||
|
**앱 카테고리**
|
||||||
|
- 카테고리: `비즈니스` 또는 `생산성`
|
||||||
|
- 콘텐츠 등급: 설문조사 진행
|
||||||
|
|
||||||
|
**광고**
|
||||||
|
- 광고 포함 여부 선택
|
||||||
|
|
||||||
|
### 3. 프로덕션 릴리스 생성
|
||||||
|
|
||||||
|
1. `프로덕션` → `새 릴리스 만들기`
|
||||||
|
2. `app-release.aab` 업로드
|
||||||
|
3. 출시 노트 작성:
|
||||||
|
|
||||||
|
```
|
||||||
|
첫 번째 릴리스
|
||||||
|
- 고장코드 조회
|
||||||
|
- TCMS 신호 조회
|
||||||
|
- MMI 코드 조회
|
||||||
|
- 오프라인 지원
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 검토 제출
|
||||||
|
|
||||||
|
### 4. 스토어 등록정보
|
||||||
|
|
||||||
|
**스크린샷** (필수)
|
||||||
|
- 휴대전화: 2개 이상 (16:9 비율)
|
||||||
|
- 7인치 태블릿: 선택사항
|
||||||
|
- 10인치 태블릿: 선택사항
|
||||||
|
|
||||||
|
**그래픽 애셋**
|
||||||
|
- 앱 아이콘: 512x512 PNG
|
||||||
|
- 기능 그래픽: 1024x500 PNG
|
||||||
|
|
||||||
|
**설명**
|
||||||
|
```
|
||||||
|
1호선 철도 차량 고장코드 및 TCMS 신호 조회 애플리케이션
|
||||||
|
|
||||||
|
주요 기능:
|
||||||
|
✓ 제조사별 고장코드 검색
|
||||||
|
✓ TCMS 신호 조회
|
||||||
|
✓ MMI 코드 데이터베이스
|
||||||
|
✓ 오프라인 지원
|
||||||
|
✓ 빠른 검색 및 필터링
|
||||||
|
|
||||||
|
철도 유지보수 및 정비 업무를 위한 필수 도구입니다.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 심사 및 배포
|
||||||
|
|
||||||
|
- 제출 후 **1-7일** 심사 기간
|
||||||
|
- 승인되면 자동으로 Play Store에 배포
|
||||||
|
- 거부 시 피드백 확인 후 수정하여 재제출
|
||||||
|
|
||||||
|
## 💾 오프라인 기능
|
||||||
|
|
||||||
|
### Service Worker
|
||||||
|
|
||||||
|
PWA의 오프라인 기능은 **Service Worker**로 구현됩니다.
|
||||||
|
|
||||||
|
`static/sw.js` 예시:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CACHE_NAME = 'tr-code-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/static/css/main.css',
|
||||||
|
'/static/js/main.js',
|
||||||
|
'/static/icon.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 설치
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(cache => cache.addAll(urlsToCache))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 요청 가로채기
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => response || fetch(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 업데이트
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map(cacheName => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### manifest.json
|
||||||
|
|
||||||
|
`static/manifest.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "1호선 고장코드",
|
||||||
|
"short_name": "고장코드",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#111827",
|
||||||
|
"theme_color": "#1f2937",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML에서 등록
|
||||||
|
|
||||||
|
`templates/base.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<head>
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
<meta name="theme-color" content="#1f2937">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icon.png">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- ... -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/static/sw.js')
|
||||||
|
.then(reg => console.log('SW registered', reg))
|
||||||
|
.catch(err => console.log('SW registration failed', err));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
### Q1: localhost로 테스트할 수 있나요?
|
||||||
|
|
||||||
|
**A:** 네! PWA는 `localhost`에서 HTTPS 없이도 작동합니다. 하지만 TWA는 실제 도메인이 필요합니다.
|
||||||
|
|
||||||
|
### Q2: 앱 업데이트는 어떻게 하나요?
|
||||||
|
|
||||||
|
**A:** 서버만 업데이트하면 됩니다! 앱을 다시 빌드/배포할 필요가 없습니다. 사용자가 앱을 열면 자동으로 최신 버전을 로드합니다.
|
||||||
|
|
||||||
|
단, 앱 아이콘이나 이름 변경 시에는 Play Store에 새 버전을 제출해야 합니다.
|
||||||
|
|
||||||
|
### Q3: iOS는 지원되나요?
|
||||||
|
|
||||||
|
**A:** PWA 자체는 iOS에서도 작동합니다 (Safari). 하지만 TWA는 Android 전용이므로, iOS 앱을 만들려면:
|
||||||
|
- **웹 앱 추가**: Safari에서 "홈 화면에 추가"
|
||||||
|
- **App Store**: Apple Developer Program ($99/년) 가입 후 네이티브 앱 개발 필요
|
||||||
|
|
||||||
|
### Q4: 서버가 다운되면 앱도 작동하지 않나요?
|
||||||
|
|
||||||
|
**A:** Service Worker로 캐시를 구현하면 오프라인에서도 기본 기능은 사용할 수 있습니다. 단, 새 데이터는 서버 연결이 필요합니다.
|
||||||
|
|
||||||
|
### Q5: 데이터는 어디에 저장되나요?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- **서버**: Supabase (PostgreSQL)
|
||||||
|
- **클라이언트**: 브라우저 캐시 (Service Worker)
|
||||||
|
- **앱 내부**: 없음 (모든 데이터는 웹에서 로드)
|
||||||
|
|
||||||
|
### Q6: 보안은 어떻게 되나요?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- HTTPS 필수 (데이터 암호화)
|
||||||
|
- Digital Asset Links (앱-도메인 인증)
|
||||||
|
- Chrome 브라우저의 보안 정책 적용
|
||||||
|
- Supabase Row Level Security
|
||||||
|
|
||||||
|
### Q7: 앱 크기는 얼마나 되나요?
|
||||||
|
|
||||||
|
**A:** TWA 앱은 매우 작습니다:
|
||||||
|
- **APK/AAB**: 약 2-5MB
|
||||||
|
- **설치 후**: 10-20MB (캐시 포함)
|
||||||
|
|
||||||
|
비교: 일반 네이티브 앱은 20-100MB 이상
|
||||||
|
|
||||||
|
### Q8: Play Store 심사에서 거부될 수 있나요?
|
||||||
|
|
||||||
|
**A:** 가능성 있는 거부 사유:
|
||||||
|
- Digital Asset Links 미설정
|
||||||
|
- 개인정보처리방침 누락
|
||||||
|
- 앱 콘텐츠 설명 불충분
|
||||||
|
- 스크린샷 부족
|
||||||
|
|
||||||
|
모두 수정 후 재제출 가능합니다.
|
||||||
|
|
||||||
|
### Q9: 현재 서버 구조에서 PWA가 잘 작동하나요?
|
||||||
|
|
||||||
|
**A:** 네! 현재 구조는 PWA에 최적화되어 있습니다:
|
||||||
|
- ✅ nginx 리버스 프록시
|
||||||
|
- ✅ HTMX (빠른 로딩)
|
||||||
|
- ✅ 반응형 디자인
|
||||||
|
- ✅ localhost Supabase (빠른 응답)
|
||||||
|
|
||||||
|
NPM(Proxmox)에서 HTTPS만 설정하면 완벽합니다!
|
||||||
|
|
||||||
|
### Q10: 비용은 얼마나 드나요?
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
- Google Play Console: **$25** (평생)
|
||||||
|
- 도메인: 연간 $10-20
|
||||||
|
- SSL 인증서: **무료** (Let's Encrypt)
|
||||||
|
- 서버: 기존 사용 중
|
||||||
|
|
||||||
|
**총 초기 비용: 약 $25-50**
|
||||||
|
|
||||||
|
## 📚 추가 자료
|
||||||
|
|
||||||
|
- [Google TWA 문서](https://developer.chrome.com/docs/android/trusted-web-activity/)
|
||||||
|
- [PWA 완벽 가이드](https://web.dev/progressive-web-apps/)
|
||||||
|
- [Android Studio 가이드](https://developer.android.com/studio/intro)
|
||||||
|
- [Play Console 도움말](https://support.google.com/googleplay/android-developer/)
|
||||||
|
|
||||||
|
## 🆘 도움이 필요하신가요?
|
||||||
|
|
||||||
|
문제가 발생하거나 질문이 있으면 시스템 관리자에게 문의하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-10-13
|
||||||
|
**버전**: 1.0
|
||||||
|
|
||||||
483
README.md
483
README.md
|
|
@ -1,37 +1,484 @@
|
||||||
1호선 고장코드 (Flask + HTMX, Supabase)
|
# 1호선 고장코드 (Tr_Code)
|
||||||
|
|
||||||
실행 방법
|
Flask + HTMX 기반의 철도 차량 고장코드 및 TCMS 신호 조회 웹 애플리케이션입니다.
|
||||||
|
|
||||||
1) 의존성 설치 (venv 권장)
|
## 📋 목차
|
||||||
|
|
||||||
|
- [주요 기능](#주요-기능)
|
||||||
|
- [시스템 아키텍처](#시스템-아키텍처)
|
||||||
|
- [설치 및 배포](#설치-및-배포)
|
||||||
|
- [환경 설정](#환경-설정)
|
||||||
|
- [회원 인증 시스템](#회원-인증-시스템) ⭐ NEW!
|
||||||
|
- [서비스 관리](#서비스-관리)
|
||||||
|
- [개발 환경](#개발-환경)
|
||||||
|
- [API 엔드포인트](#api-엔드포인트)
|
||||||
|
- [문제 해결](#문제-해결)
|
||||||
|
|
||||||
|
## 🎯 주요 기능
|
||||||
|
|
||||||
|
### 🔐 회원 관리 및 인증 (NEW!)
|
||||||
|
- **이메일 코드 인증** - 6자리 숫자 코드 입력 (링크 클릭 방식 대신)
|
||||||
|
- **생체인증** - WebAuthn API 활용 (지문, 얼굴 인식)
|
||||||
|
- **비밀번호 재설정** - 이메일 코드 인증 후 재설정
|
||||||
|
- **humetro.busan.kr 도메인 제한** (회사 이메일만 가입 가능)
|
||||||
|
- 부서별 사용자 관리 (신평차량, 노포차량, 차량처)
|
||||||
|
- 사번 기반 사용자 식별
|
||||||
|
- 동적 부서 목록 로드 (Supabase departments 테이블)
|
||||||
|
- 부서별 권한 관리 기반 구축 (조회/수정 권한 제어 가능)
|
||||||
|
- 세션 기반 로그인 상태 유지
|
||||||
|
|
||||||
|
### 고장코드 조회
|
||||||
|
- 제조사별 필터링 (우진, 로템 등)
|
||||||
|
- 장치별, 차량 타입별 검색
|
||||||
|
- 실시간 검색 (HTMX 기반)
|
||||||
|
- 코드 그룹핑 옵션
|
||||||
|
- 상세 정보 모달 표시
|
||||||
|
|
||||||
|
### TCMS 신호 조회
|
||||||
|
- 신호 분류별 필터링
|
||||||
|
- 제조사/차량별 검색
|
||||||
|
- 신호 상태값 확인
|
||||||
|
|
||||||
|
### MMI 코드 조회
|
||||||
|
- MMI 코드 데이터베이스 검색
|
||||||
|
- 차량분류별 필터링
|
||||||
|
|
||||||
|
### UI/UX
|
||||||
|
- 반응형 디자인 (모바일 최적화)
|
||||||
|
- 다크모드 지원
|
||||||
|
- PWA 지원 (오프라인 사용 가능)
|
||||||
|
- 빠른 페이지 전환 (HTMX)
|
||||||
|
- 로그인 사용자 정보 표시
|
||||||
|
|
||||||
|
## 🏗️ 시스템 아키텍처
|
||||||
|
|
||||||
```
|
```
|
||||||
|
외부 클라이언트 (브라우저/PWA)
|
||||||
|
↓ HTTPS/HTTP
|
||||||
|
NPM 리버스 프록시 (Proxmox, 선택적)
|
||||||
|
↓ HTTP
|
||||||
|
M1T 서버 nginx (:80)
|
||||||
|
↓ Unix Socket
|
||||||
|
gunicorn (Tr_Code.sock)
|
||||||
|
↓
|
||||||
|
Flask App (app.py)
|
||||||
|
↓ localhost:8000
|
||||||
|
Supabase (Docker)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 기술 스택
|
||||||
|
|
||||||
|
**백엔드**
|
||||||
|
- Flask 3.0.3 - 웹 프레임워크
|
||||||
|
- Gunicorn 23.0.0 - WSGI 서버
|
||||||
|
- httpx - 비동기 HTTP 클라이언트
|
||||||
|
|
||||||
|
**프론트엔드**
|
||||||
|
- HTMX - 동적 UI
|
||||||
|
- TailwindCSS - 스타일링
|
||||||
|
- Jinja2 템플릿
|
||||||
|
|
||||||
|
**데이터베이스**
|
||||||
|
- Supabase (PostgreSQL)
|
||||||
|
- PostgREST API
|
||||||
|
|
||||||
|
**인프라**
|
||||||
|
- nginx - 리버스 프록시
|
||||||
|
- systemd - 프로세스 관리
|
||||||
|
- Ubuntu 22.04 LTS
|
||||||
|
|
||||||
|
## 🚀 설치 및 배포
|
||||||
|
|
||||||
|
### 1. 시스템 요구사항
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OS
|
||||||
|
Ubuntu 22.04 LTS 이상
|
||||||
|
|
||||||
|
# Python
|
||||||
|
Python 3.10 이상
|
||||||
|
|
||||||
|
# 필수 패키지
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y python3 python3-venv python3-pip nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 프로젝트 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 프로젝트 클론 또는 복사
|
||||||
|
cd /home/ckh08045
|
||||||
|
git clone <repository> Tr_Code
|
||||||
|
cd Tr_Code
|
||||||
|
|
||||||
|
# 가상환경 생성
|
||||||
|
python3 -m venv .
|
||||||
|
|
||||||
|
# 가상환경 활성화
|
||||||
|
source bin/activate
|
||||||
|
|
||||||
|
# 의존성 설치
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
2) 환경변수 설정
|
### 3. 환경 설정
|
||||||
|
|
||||||
```
|
#### 3.1 데이터베이스 스키마 생성
|
||||||
# Windows PowerShell 예시
|
|
||||||
$env:SUPABASE_URL="http://192.168.0.180:54321" # 로컬 도커 기본 포트
|
회원 관리 시스템을 사용하려면 먼저 데이터베이스 스키마를 생성해야 합니다:
|
||||||
$env:SUPABASE_ANON_KEY="<ANON_KEY>"
|
|
||||||
|
```bash
|
||||||
|
# database_schema.sql 파일의 내용을 Supabase SQL 에디터에서 실행
|
||||||
|
# 부서, 사용자, 권한 테이블이 자동으로 생성됩니다
|
||||||
```
|
```
|
||||||
|
|
||||||
3) 서버 실행
|
#### 3.2 `.env` 파일 생성
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# env.example 파일을 .env로 복사
|
||||||
|
cp env.example .env
|
||||||
|
|
||||||
|
# SECRET_KEY 생성
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
|
||||||
|
# .env 파일 편집
|
||||||
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`.env` 파일 내용:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase 설정 (localhost - 같은 서버에서 실행 중)
|
||||||
|
SUPABASE_URL=http://localhost:8000
|
||||||
|
SUPABASE_ANON_KEY=your_anon_key_here
|
||||||
|
|
||||||
|
# Flask 세션 보안 키 (반드시 변경!)
|
||||||
|
SECRET_KEY=위에서-생성한-랜덤-키-입력
|
||||||
|
|
||||||
|
# Kong Basic Auth (선택사항)
|
||||||
|
SUPABASE_BASIC_USER=
|
||||||
|
SUPABASE_BASIC_PASSWORD=
|
||||||
|
|
||||||
|
# Flask 설정
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 회원 인증 시스템
|
||||||
|
|
||||||
|
### 빠른 시작
|
||||||
|
|
||||||
|
회원 관리 및 인증 시스템 설정을 위한 문서:
|
||||||
|
|
||||||
|
📖 **[ENV_SETUP_GUIDE.md](ENV_SETUP_GUIDE.md)** - 환경 변수 설정 가이드
|
||||||
|
📖 **[AUTH_SETUP.md](AUTH_SETUP.md)** - 상세 인증 시스템 설정
|
||||||
|
📖 **[EMAIL_CODE_AUTH_GUIDE.md](EMAIL_CODE_AUTH_GUIDE.md)** - 이메일 코드 인증 가이드
|
||||||
|
📖 **[BIOMETRIC_AUTH_GUIDE.md](BIOMETRIC_AUTH_GUIDE.md)** - 생체인증 가이드
|
||||||
|
📖 **[AUTHENTICATION_SUMMARY.md](AUTHENTICATION_SUMMARY.md)** - 기술 문서
|
||||||
|
📖 **[DEPARTMENTS_API.md](DEPARTMENTS_API.md)** - 부서 API 문서
|
||||||
|
|
||||||
|
### 주요 기능
|
||||||
|
|
||||||
|
- ✅ **이메일 코드 인증** - 6자리 숫자 코드 입력 방식 (스팸/피싱 의심 방지)
|
||||||
|
- ✅ **생체인증** - WebAuthn/Credential Management API (지문, 얼굴 인식)
|
||||||
|
- ✅ **비밀번호 재설정** - 이메일 코드 인증 후 재설정
|
||||||
|
- ✅ **humetro.busan.kr 도메인만 가입 가능**
|
||||||
|
- ✅ 부서별 사용자 관리 (동적 부서 목록)
|
||||||
|
- ✅ 사번 기반 사용자 식별
|
||||||
|
- ✅ 부서별 권한 관리 (조회/수정 권한 제어 가능)
|
||||||
|
|
||||||
|
### Docker 기반 Supabase 환경
|
||||||
|
|
||||||
|
> ⚠️ **중요**: Docker 기반 Supabase는 대시보드 Authentication 설정이 제한적입니다.
|
||||||
|
> 이메일 도메인 검증은 **애플리케이션 레벨에서만** 처리됩니다.
|
||||||
|
|
||||||
|
### 3단계 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 데이터베이스 스키마 생성 (Supabase SQL 에디터)
|
||||||
|
# database_schema.sql 실행
|
||||||
|
|
||||||
|
# 2. .env 파일 설정
|
||||||
|
cp env.example .env
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
# 출력된 키를 .env의 SECRET_KEY에 설정
|
||||||
|
|
||||||
|
# 3. 서버 실행
|
||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
브라우저에서 http://localhost:5000 접속
|
### 사용 방법
|
||||||
|
|
||||||
기능
|
1. `http://localhost:5000` 접속 → 로그인 페이지
|
||||||
- Supabase의 `public.Falut_Code_Table`에서 필터/검색
|
2. "회원가입" 클릭
|
||||||
- 항목 클릭 시 모달 상세 표시
|
3. 정보 입력 (humetro.busan.kr 이메일 사용)
|
||||||
- 다크모드 토글
|
4. 로그인
|
||||||
|
|
||||||
비고
|
### 4. Systemd 서비스 설정
|
||||||
- 로컬 SQLite 및 `/api/meta`, `/api/db` 기반 동기화는 제거됨
|
|
||||||
- Supabase 접속 실패 시 안내 메시지를 화면에 표시
|
`/etc/systemd/system/Tr_Code.service`:
|
||||||
- 사내망 주소/포트 방화벽 허용 필요 (예: 54321, 8000 등 구성에 따라 상이)
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Gunicorn instance to serve Tr_Code
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=ckh08045
|
||||||
|
Group=ckh08045
|
||||||
|
WorkingDirectory=/home/ckh08045/Tr_Code
|
||||||
|
Environment="PATH=/home/ckh08045/Tr_Code/bin"
|
||||||
|
ExecStart=/home/ckh08045/Tr_Code/bin/gunicorn --workers 3 --bind unix:Tr_Code.sock -m 007 app:app
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. nginx 설정
|
||||||
|
|
||||||
|
`/etc/nginx/sites-available/tr_code`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
client_max_body_size 10M;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /home/ckh08045/Tr_Code/static/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://unix:/home/ckh08045/Tr_Code/Tr_Code.sock;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**nginx 활성화:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 심볼릭 링크 생성
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/tr_code /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# 기본 사이트 비활성화
|
||||||
|
sudo rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# nginx 사용자를 애플리케이션 그룹에 추가 (Unix 소켓 접근 권한)
|
||||||
|
sudo usermod -a -G ckh08045 www-data
|
||||||
|
|
||||||
|
# 설정 테스트
|
||||||
|
sudo nginx -t
|
||||||
|
|
||||||
|
# nginx 재시작
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 서비스 시작
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# systemd 설정 리로드
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# 서비스 활성화 (부팅 시 자동 시작)
|
||||||
|
sudo systemctl enable Tr_Code
|
||||||
|
|
||||||
|
# 서비스 시작
|
||||||
|
sudo systemctl start Tr_Code
|
||||||
|
|
||||||
|
# 상태 확인
|
||||||
|
sudo systemctl status Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 환경 설정
|
||||||
|
|
||||||
|
### 환경변수 설명
|
||||||
|
|
||||||
|
| 변수 | 설명 | 기본값 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `SUPABASE_URL` | Supabase 서버 주소 | `http://localhost:8000` |
|
||||||
|
| `SUPABASE_ANON_KEY` | Supabase Anonymous Key | - |
|
||||||
|
| `SUPABASE_BASIC_USER` | Kong Basic Auth 사용자명 (선택) | - |
|
||||||
|
| `SUPABASE_BASIC_PASSWORD` | Kong Basic Auth 비밀번호 (선택) | - |
|
||||||
|
| `PORT` | Flask 개발 서버 포트 | `5000` |
|
||||||
|
|
||||||
|
### Supabase 연결
|
||||||
|
|
||||||
|
- **같은 서버**: `http://localhost:8000` 사용 (최적 성능)
|
||||||
|
- **다른 서버**: `http://ip:port` 형식으로 지정
|
||||||
|
- **Kong 프록시**: Basic Auth 정보 추가
|
||||||
|
|
||||||
|
## 🔧 서비스 관리
|
||||||
|
|
||||||
|
### 서비스 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 시작
|
||||||
|
sudo systemctl start Tr_Code
|
||||||
|
|
||||||
|
# 중지
|
||||||
|
sudo systemctl stop Tr_Code
|
||||||
|
|
||||||
|
# 재시작
|
||||||
|
sudo systemctl restart Tr_Code
|
||||||
|
|
||||||
|
# 상태 확인
|
||||||
|
sudo systemctl status Tr_Code
|
||||||
|
|
||||||
|
# 로그 확인
|
||||||
|
sudo journalctl -u Tr_Code -f
|
||||||
|
|
||||||
|
# 부팅 시 자동 시작 활성화
|
||||||
|
sudo systemctl enable Tr_Code
|
||||||
|
|
||||||
|
# 부팅 시 자동 시작 비활성화
|
||||||
|
sudo systemctl disable Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### nginx 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 설정 테스트
|
||||||
|
sudo nginx -t
|
||||||
|
|
||||||
|
# 재시작
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
|
||||||
|
# 리로드 (무중단)
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
# 에러 로그 확인
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
|
||||||
|
# 액세스 로그 확인
|
||||||
|
sudo tail -f /var/log/nginx/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 개발 환경
|
||||||
|
|
||||||
|
### 로컬 개발 서버
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 가상환경 활성화
|
||||||
|
source bin/activate
|
||||||
|
|
||||||
|
# 개발 모드로 실행 (디버그 활성화)
|
||||||
|
python app.py
|
||||||
|
|
||||||
|
# 접속
|
||||||
|
# http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 코드 수정 후
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 서비스 재시작
|
||||||
|
sudo systemctl restart Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 API 엔드포인트
|
||||||
|
|
||||||
|
### 헬스체크
|
||||||
|
- `GET /health` - 애플리케이션 상태
|
||||||
|
- `GET /sb/health` - Supabase 연결 상태
|
||||||
|
|
||||||
|
### 고장코드
|
||||||
|
- `GET /sb` - 메인 페이지
|
||||||
|
- `GET /sb/faults/list` - 고장코드 목록
|
||||||
|
- `GET /sb/faults/<code>` - 고장코드 상세
|
||||||
|
|
||||||
|
### TCMS 신호
|
||||||
|
- `GET /sb/signals/list` - 신호 목록
|
||||||
|
- `GET /sb/signals/<id>` - 신호 상세
|
||||||
|
|
||||||
|
### MMI 코드
|
||||||
|
- `GET /sb?section=mmicode` - MMI 코드 목록
|
||||||
|
|
||||||
|
### 디버그
|
||||||
|
- `GET /sb/signals/test` - Signals 테이블 테스트
|
||||||
|
- `GET /sb/signals/debug` - 상세 디버그 정보
|
||||||
|
|
||||||
|
## 🐛 문제 해결
|
||||||
|
|
||||||
|
### 서비스가 시작되지 않을 때
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 로그 확인
|
||||||
|
sudo journalctl -u Tr_Code -n 50
|
||||||
|
|
||||||
|
# 가상환경 경로 확인
|
||||||
|
ls -la /home/ckh08045/Tr_Code/bin/gunicorn
|
||||||
|
|
||||||
|
# 권한 확인
|
||||||
|
ls -la /home/ckh08045/Tr_Code/
|
||||||
|
```
|
||||||
|
|
||||||
|
### nginx Permission Denied
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# www-data 사용자가 소켓에 접근할 수 있는지 확인
|
||||||
|
sudo usermod -a -G ckh08045 www-data
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supabase 연결 실패
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supabase 컨테이너 상태 확인
|
||||||
|
docker ps | grep supabase
|
||||||
|
|
||||||
|
# 포트 확인
|
||||||
|
sudo netstat -tlnp | grep 8000
|
||||||
|
|
||||||
|
# .env 파일 확인
|
||||||
|
cat .env
|
||||||
|
|
||||||
|
# 연결 테스트
|
||||||
|
curl http://localhost:8000/rest/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# gunicorn 서비스 상태 확인
|
||||||
|
sudo systemctl status Tr_Code
|
||||||
|
|
||||||
|
# 소켓 파일 확인
|
||||||
|
ls -la /home/ckh08045/Tr_Code/Tr_Code.sock
|
||||||
|
|
||||||
|
# nginx 에러 로그
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 PWA 지원
|
||||||
|
|
||||||
|
PWA(Progressive Web App)로 변환하여 모바일 앱처럼 사용할 수 있습니다.
|
||||||
|
|
||||||
|
자세한 내용은 [PWA_README.md](PWA_README.md)를 참조하세요.
|
||||||
|
|
||||||
|
## 📄 라이선스
|
||||||
|
|
||||||
|
이 프로젝트는 내부 사용을 위한 것입니다.
|
||||||
|
|
||||||
|
## 🤝 기여
|
||||||
|
|
||||||
|
문제가 발생하거나 개선 사항이 있으면 이슈를 등록해주세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**문의**: 시스템 관리자
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,527 @@
|
||||||
|
# SMTP 이메일 발신 시스템 설정 가이드
|
||||||
|
|
||||||
|
이메일 인증 및 비밀번호 재설정을 위한 SMTP 서버 설정 방법입니다.
|
||||||
|
|
||||||
|
## 📋 목차
|
||||||
|
|
||||||
|
- [방법 1: Python Flask-Mail (권장)](#방법-1-python-flask-mail-권장)
|
||||||
|
- [방법 2: Docker MailHog (개발용)](#방법-2-docker-mailhog-개발용)
|
||||||
|
- [방법 3: Docker Postfix (운영용)](#방법-3-docker-postfix-운영용)
|
||||||
|
- [방법 4: 외부 SMTP 서비스](#방법-4-외부-smtp-서비스)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 방법 1: Python Flask-Mail (권장) ⭐
|
||||||
|
|
||||||
|
Flask 앱에 직접 통합하는 가장 간단한 방법입니다.
|
||||||
|
|
||||||
|
### 1-1. 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
source bin/activate
|
||||||
|
pip install Flask-Mail
|
||||||
|
pip freeze > requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1-2. .env 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SMTP 설정 추가
|
||||||
|
MAIL_SERVER=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USE_SSL=False
|
||||||
|
MAIL_USERNAME=your-email@gmail.com
|
||||||
|
MAIL_PASSWORD=your-app-password
|
||||||
|
MAIL_DEFAULT_SENDER=your-email@gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gmail 앱 비밀번호 생성:**
|
||||||
|
1. Google 계정 → 보안
|
||||||
|
2. 2단계 인증 활성화
|
||||||
|
3. 앱 비밀번호 생성
|
||||||
|
4. `.env`에 입력
|
||||||
|
|
||||||
|
### 1-3. app.py에 추가
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask_mail import Mail, Message
|
||||||
|
|
||||||
|
# Flask-Mail 설정
|
||||||
|
app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.gmail.com')
|
||||||
|
app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587))
|
||||||
|
app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'True').lower() == 'true'
|
||||||
|
app.config['MAIL_USE_SSL'] = os.environ.get('MAIL_USE_SSL', 'False').lower() == 'true'
|
||||||
|
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
|
||||||
|
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
|
||||||
|
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER')
|
||||||
|
|
||||||
|
mail = Mail(app)
|
||||||
|
|
||||||
|
def send_reset_email(to_email, reset_token, employee_id):
|
||||||
|
"""비밀번호 재설정 이메일 전송"""
|
||||||
|
reset_url = url_for('reset_password', token=reset_token, _external=True)
|
||||||
|
|
||||||
|
msg = Message(
|
||||||
|
subject='[1호선 고장코드] 비밀번호 재설정',
|
||||||
|
recipients=[to_email]
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.body = f"""
|
||||||
|
안녕하세요,
|
||||||
|
|
||||||
|
비밀번호 재설정을 요청하셨습니다.
|
||||||
|
|
||||||
|
사번: {employee_id}
|
||||||
|
|
||||||
|
아래 링크를 클릭하여 비밀번호를 재설정하세요:
|
||||||
|
{reset_url}
|
||||||
|
|
||||||
|
이 링크는 1시간 동안 유효합니다.
|
||||||
|
|
||||||
|
본인이 요청하지 않았다면 이 이메일을 무시하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
부산교통공사 1호선 차량 고장코드 시스템
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg.html = f"""
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px;">
|
||||||
|
<h2 style="color: #0066cc;">비밀번호 재설정</h2>
|
||||||
|
<p>안녕하세요,</p>
|
||||||
|
<p>비밀번호 재설정을 요청하셨습니다.</p>
|
||||||
|
<p><strong>사번:</strong> {employee_id}</p>
|
||||||
|
<p>아래 버튼을 클릭하여 비밀번호를 재설정하세요:</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{reset_url}"
|
||||||
|
style="background-color: #0066cc; color: white; padding: 12px 30px;
|
||||||
|
text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||||
|
비밀번호 재설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666; font-size: 0.9em;">
|
||||||
|
이 링크는 <strong>1시간</strong> 동안 유효합니다.
|
||||||
|
</p>
|
||||||
|
<p style="color: #666; font-size: 0.9em;">
|
||||||
|
본인이 요청하지 않았다면 이 이메일을 무시하세요.
|
||||||
|
</p>
|
||||||
|
<hr style="margin: 30px 0; border: none; border-top: 1px solid #eee;">
|
||||||
|
<p style="color: #999; font-size: 0.8em; text-align: center;">
|
||||||
|
부산교통공사 1호선 차량 고장코드 시스템
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail.send(msg)
|
||||||
|
return True, "이메일이 전송되었습니다."
|
||||||
|
except Exception as e:
|
||||||
|
print(f"이메일 전송 실패: {str(e)}")
|
||||||
|
return False, f"이메일 전송에 실패했습니다: {str(e)}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1-4. forgot_password 라우트 수정
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 재설정 토큰 생성 후
|
||||||
|
reset_token = secrets.token_urlsafe(32)
|
||||||
|
session[f"reset_token_{reset_token}"] = {
|
||||||
|
"employee_id": employee_id,
|
||||||
|
"email": user["email"],
|
||||||
|
"expires": (datetime.now() + timedelta(hours=1)).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 이메일 전송
|
||||||
|
success, message = send_reset_email(user["email"], reset_token, employee_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return render_template(
|
||||||
|
"forgot_password.html",
|
||||||
|
app_name=APP_NAME,
|
||||||
|
success="등록된 이메일로 비밀번호 재설정 링크를 전송했습니다."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return render_template(
|
||||||
|
"forgot_password.html",
|
||||||
|
app_name=APP_NAME,
|
||||||
|
error=message
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1-5. 장단점
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- ✅ 설치/설정 간단
|
||||||
|
- ✅ Flask 앱과 완벽 통합
|
||||||
|
- ✅ 추가 서버 불필요
|
||||||
|
- ✅ Gmail, Naver 등 기존 계정 사용 가능
|
||||||
|
|
||||||
|
**단점:**
|
||||||
|
- ❌ 메일 서버 의존성
|
||||||
|
- ❌ 발신 제한 (Gmail: 하루 500통)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 방법 2: Docker MailHog (개발용) 🧪
|
||||||
|
|
||||||
|
개발/테스트 환경에서 실제 이메일 전송 없이 테스트하는 방법입니다.
|
||||||
|
|
||||||
|
### 2-1. Docker Compose 설정
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml (기존 Supabase에 추가)
|
||||||
|
services:
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog:latest
|
||||||
|
container_name: mailhog
|
||||||
|
ports:
|
||||||
|
- "1025:1025" # SMTP
|
||||||
|
- "8025:8025" # Web UI
|
||||||
|
networks:
|
||||||
|
- tr_code_network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tr_code_network:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-2. 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d mailhog
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-3. .env 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 개발 환경
|
||||||
|
MAIL_SERVER=localhost
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USE_TLS=False
|
||||||
|
MAIL_USE_SSL=False
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_DEFAULT_SENDER=noreply@tr-code.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2-4. 이메일 확인
|
||||||
|
|
||||||
|
브라우저에서 `http://192.168.0.180:8025` 접속
|
||||||
|
|
||||||
|
- 전송된 모든 이메일 확인 가능
|
||||||
|
- 실제 전송은 안 됨 (개발용)
|
||||||
|
|
||||||
|
### 2-5. 장단점
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- ✅ 실제 이메일 전송 없이 테스트
|
||||||
|
- ✅ 웹 UI로 쉽게 확인
|
||||||
|
- ✅ 설정 간단
|
||||||
|
|
||||||
|
**단점:**
|
||||||
|
- ❌ 개발용으로만 사용 가능
|
||||||
|
- ❌ 실제 이메일 전송 안 됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 방법 3: Docker Postfix (운영용) 🚀
|
||||||
|
|
||||||
|
자체 SMTP 서버를 운영하는 방법입니다.
|
||||||
|
|
||||||
|
### 3-1. Docker Compose 설정
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
postfix:
|
||||||
|
image: boky/postfix:latest
|
||||||
|
container_name: postfix
|
||||||
|
environment:
|
||||||
|
- ALLOWED_SENDER_DOMAINS=humetro.busan.kr
|
||||||
|
- HOSTNAME=mail.tr-code.local
|
||||||
|
ports:
|
||||||
|
- "25:25" # SMTP
|
||||||
|
- "587:587" # SMTP with TLS
|
||||||
|
volumes:
|
||||||
|
- ./postfix-data:/var/spool/postfix
|
||||||
|
networks:
|
||||||
|
- tr_code_network
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-2. .env 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MAIL_SERVER=localhost
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USE_SSL=False
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_DEFAULT_SENDER=noreply@humetro.busan.kr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-3. DNS 설정 (필수)
|
||||||
|
|
||||||
|
도메인 DNS에 다음 레코드 추가:
|
||||||
|
|
||||||
|
```
|
||||||
|
MX @ 10 mail.yourdomain.com
|
||||||
|
A mail 192.168.0.180
|
||||||
|
TXT @ "v=spf1 ip4:192.168.0.180 ~all"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3-4. 장단점
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- ✅ 완전한 제어
|
||||||
|
- ✅ 발신 제한 없음
|
||||||
|
- ✅ 커스터마이징 가능
|
||||||
|
|
||||||
|
**단점:**
|
||||||
|
- ❌ 설정 복잡
|
||||||
|
- ❌ 스팸 필터링 이슈
|
||||||
|
- ❌ DNS/IP 평판 관리 필요
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 방법 4: 외부 SMTP 서비스 📧
|
||||||
|
|
||||||
|
전문 이메일 서비스를 사용하는 방법입니다.
|
||||||
|
|
||||||
|
### 4-1. SendGrid (추천)
|
||||||
|
|
||||||
|
**가격:** 무료 티어 100통/일
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MAIL_SERVER=smtp.sendgrid.net
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USERNAME=apikey
|
||||||
|
MAIL_PASSWORD=your_sendgrid_api_key
|
||||||
|
MAIL_DEFAULT_SENDER=noreply@yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- ✅ 높은 전달률
|
||||||
|
- ✅ 통계/분석 제공
|
||||||
|
- ✅ 무료 티어 제공
|
||||||
|
|
||||||
|
### 4-2. Gmail
|
||||||
|
|
||||||
|
**가격:** 무료 (500통/일 제한)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MAIL_SERVER=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USERNAME=your-email@gmail.com
|
||||||
|
MAIL_PASSWORD=your-app-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-3. Naver
|
||||||
|
|
||||||
|
**가격:** 무료 (제한 있음)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MAIL_SERVER=smtp.naver.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USERNAME=your-id@naver.com
|
||||||
|
MAIL_PASSWORD=your-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4-4. AWS SES
|
||||||
|
|
||||||
|
**가격:** 0.10USD / 1000통
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
MAIL_SERVER=email-smtp.ap-northeast-2.amazonaws.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USERNAME=your_access_key
|
||||||
|
MAIL_PASSWORD=your_secret_key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 권장 사항
|
||||||
|
|
||||||
|
### 개발/테스트 환경
|
||||||
|
```
|
||||||
|
MailHog (Docker) → 가장 간단
|
||||||
|
```
|
||||||
|
|
||||||
|
### 운영 환경 (소규모)
|
||||||
|
```
|
||||||
|
Gmail + Flask-Mail → 빠른 구축
|
||||||
|
```
|
||||||
|
|
||||||
|
### 운영 환경 (대규모)
|
||||||
|
```
|
||||||
|
SendGrid 또는 AWS SES → 안정성
|
||||||
|
```
|
||||||
|
|
||||||
|
### 내부망 전용
|
||||||
|
```
|
||||||
|
Postfix (Docker) → 완전한 제어
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 설치 스크립트
|
||||||
|
|
||||||
|
### Flask-Mail 설치 및 설정
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
source bin/activate
|
||||||
|
pip install Flask-Mail
|
||||||
|
pip freeze > requirements.txt
|
||||||
|
|
||||||
|
# .env에 설정 추가
|
||||||
|
cat >> .env << 'EOF'
|
||||||
|
|
||||||
|
# SMTP 설정 (Gmail 예시)
|
||||||
|
MAIL_SERVER=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USE_TLS=True
|
||||||
|
MAIL_USE_SSL=False
|
||||||
|
MAIL_USERNAME=your-email@gmail.com
|
||||||
|
MAIL_PASSWORD=your-app-password
|
||||||
|
MAIL_DEFAULT_SENDER=your-email@gmail.com
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 서비스 재시작
|
||||||
|
sudo systemctl restart Tr_Code
|
||||||
|
```
|
||||||
|
|
||||||
|
### MailHog (Docker) 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ckh08045/Tr_Code
|
||||||
|
|
||||||
|
# docker-compose.yml 생성 또는 수정
|
||||||
|
docker-compose up -d mailhog
|
||||||
|
|
||||||
|
# 웹 UI 접속
|
||||||
|
echo "MailHog UI: http://192.168.0.180:8025"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 이메일 템플릿 예시
|
||||||
|
|
||||||
|
### 비밀번호 재설정
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.button {
|
||||||
|
background-color: #0066cc;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>비밀번호 재설정</h2>
|
||||||
|
<p>안녕하세요, {{ name }}님</p>
|
||||||
|
<p>비밀번호 재설정을 요청하셨습니다.</p>
|
||||||
|
<a href="{{ reset_url }}" class="button">비밀번호 재설정</a>
|
||||||
|
<p>이 링크는 1시간 동안 유효합니다.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 회원가입 인증
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="container">
|
||||||
|
<h2>회원가입 인증</h2>
|
||||||
|
<p>환영합니다, {{ name }}님!</p>
|
||||||
|
<p>아래 버튼을 클릭하여 이메일을 인증하세요:</p>
|
||||||
|
<a href="{{ verify_url }}" class="button">이메일 인증</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 문제 해결
|
||||||
|
|
||||||
|
### Gmail 전송 실패
|
||||||
|
|
||||||
|
**원인:** 앱 비밀번호 미설정
|
||||||
|
**해결:**
|
||||||
|
1. Google 계정 → 보안
|
||||||
|
2. 2단계 인증 활성화
|
||||||
|
3. 앱 비밀번호 생성
|
||||||
|
|
||||||
|
### SMTP 연결 실패
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 포트 확인
|
||||||
|
telnet smtp.gmail.com 587
|
||||||
|
|
||||||
|
# 방화벽 확인
|
||||||
|
sudo ufw allow 587/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### MailHog 접속 안됨
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 컨테이너 상태 확인
|
||||||
|
docker ps | grep mailhog
|
||||||
|
|
||||||
|
# 로그 확인
|
||||||
|
docker logs mailhog
|
||||||
|
|
||||||
|
# 재시작
|
||||||
|
docker restart mailhog
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 보안 고려사항
|
||||||
|
|
||||||
|
1. **환경변수 사용**: 비밀번호를 코드에 하드코딩하지 마세요
|
||||||
|
2. **TLS 사용**: 암호화된 연결 사용
|
||||||
|
3. **Rate Limiting**: 과도한 이메일 전송 방지
|
||||||
|
4. **토큰 만료**: 재설정 링크에 시간 제한 설정
|
||||||
|
5. **.env 보호**: `.gitignore`에 추가
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 비교표
|
||||||
|
|
||||||
|
| 방법 | 설정 난이도 | 비용 | 전달률 | 제어 | 추천 |
|
||||||
|
|------|-----------|------|--------|------|------|
|
||||||
|
| Flask-Mail + Gmail | ⭐ 쉬움 | 무료 | 높음 | 낮음 | 소규모 |
|
||||||
|
| MailHog | ⭐ 쉬움 | 무료 | N/A | 높음 | 개발 |
|
||||||
|
| Postfix | ⭐⭐⭐ 어려움 | 무료 | 낮음 | 높음 | 내부망 |
|
||||||
|
| SendGrid | ⭐⭐ 보통 | 무료/유료 | 높음 | 보통 | 운영 |
|
||||||
|
| AWS SES | ⭐⭐ 보통 | 유료 | 높음 | 보통 | 대규모 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-10-15
|
||||||
|
**권장 방법**: Flask-Mail + Gmail (개발), SendGrid (운영)
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -21,11 +21,11 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
def twaManifest = [
|
def twaManifest = [
|
||||||
applicationId: 'cc.m1tcloud.tr.twa',
|
applicationId: 'me.humetrain.tr.twa',
|
||||||
hostName: 'tr.m1tcloud.cc', // The domain being opened in the TWA.
|
hostName: 'humetrain.me', // The domain being opened in the TWA.
|
||||||
launchUrl: '/', // The start path for the TWA. Must be relative to the domain.
|
launchUrl: '/', // The start path for the TWA. Must be relative to the domain.
|
||||||
name: 'FaultCode_Line1', // The application name.
|
name: 'Humetrain_1', // The application name.
|
||||||
launcherName: 'F_Code', // The name shown on the Android Launcher.
|
launcherName: 'Humetrain_1', // The name shown on the Android Launcher.
|
||||||
themeColor: '#0F172A', // The color used for the status bar.
|
themeColor: '#0F172A', // The color used for the status bar.
|
||||||
themeColorDark: '#000000', // The color used for the dark status bar.
|
themeColorDark: '#000000', // The color used for the dark status bar.
|
||||||
navigationColor: '#000000', // The color used for the navigation bar.
|
navigationColor: '#000000', // The color used for the navigation bar.
|
||||||
|
|
@ -52,13 +52,13 @@ def twaManifest = [
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 36
|
compileSdkVersion 36
|
||||||
namespace "cc.m1tcloud.tr.twa"
|
namespace "me.humetrain.tr.twa"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.m1tcloud.tr.twa"
|
applicationId "me.humetrain.tr.twa"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 35
|
targetSdkVersion 35
|
||||||
versionCode 1
|
versionCode 5
|
||||||
versionName "1"
|
versionName "5"
|
||||||
|
|
||||||
// The name for the application
|
// The name for the application
|
||||||
resValue "string", "appName", twaManifest.name
|
resValue "string", "appName", twaManifest.name
|
||||||
|
|
@ -76,12 +76,12 @@ android {
|
||||||
// The URL the Web Manifest for the Progressive Web App that the TWA points to. This
|
// The URL the Web Manifest for the Progressive Web App that the TWA points to. This
|
||||||
// is used by Chrome OS and Meta Quest to open the Web version of the PWA instead of
|
// is used by Chrome OS and Meta Quest to open the Web version of the PWA instead of
|
||||||
// the TWA, as it will probably give a better user experience for non-mobile devices.
|
// the TWA, as it will probably give a better user experience for non-mobile devices.
|
||||||
resValue "string", "webManifestUrl", 'https://tr.m1tcloud.cc/manifest.webmanifest'
|
resValue "string", "webManifestUrl", 'http://localhost:9999/static/manifest.webmanifest'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// This is used by Meta Quest.
|
// This is used by Meta Quest.
|
||||||
resValue "string", "fullScopeUrl", 'https://tr.m1tcloud.cc/'
|
resValue "string", "fullScopeUrl", 'https://humetrain.me/'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Automatically generated file. DO NOT MODIFY -->
|
||||||
|
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<bool name="enableNotification">true</bool>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<bool name="enableSiteSettingsShortcut">true</bool>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="backgroundColor">#0F172A</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="colorPrimary">#0F172A</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="colorPrimaryDark">#000000</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="navigationColor">#000000</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="navigationColorDark">#000000</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="navigationDividerColor">#000000</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<color name="navigationDividerColorDark">#000000</color>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<integer name="splashScreenFadeOutDuration">300</integer>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="appName" translatable="false">Humetrain_1</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="fallbackType" translatable="false">customtabs</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="fullScopeUrl" translatable="false">https://humetrain.me/</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="generatorApp" translatable="false">bubblewrap-cli</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="hostName" translatable="false">humetrain.me</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="launchUrl" translatable="false">https://humetrain.me/</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="launcherName" translatable="false">Humetrain_1</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="orientation" translatable="false">default</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="providerAuthority" translatable="false">me.humetrain.tr.twa.fileprovider</string>
|
||||||
|
<!-- Value from default config. -->
|
||||||
|
<string name="webManifestUrl" translatable="false">http://localhost:9999/static/manifest.webmanifest</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
-keep class androidx.core.app.CoreComponentFactory { <init>(); }
|
||||||
|
-keep class androidx.core.content.FileProvider { <init>(); }
|
||||||
|
-keep class androidx.profileinstaller.ProfileInstallReceiver { <init>(); }
|
||||||
|
-keep class androidx.startup.InitializationProvider { <init>(); }
|
||||||
|
-keep class com.google.android.gms.common.api.GoogleApiActivity { <init>(); }
|
||||||
|
-keep class com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity { <init>(); }
|
||||||
|
-keep class com.google.androidbrowserhelper.trusted.FocusActivity { <init>(); }
|
||||||
|
-keep class com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity { <init>(); }
|
||||||
|
-keep class com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity { <init>(); }
|
||||||
|
-keep class com.google.androidbrowserhelper.trusted.WebViewFallbackActivity { <init>(); }
|
||||||
|
-keep class me.humetrain.tr.twa.Application { <init>(); }
|
||||||
|
-keep class me.humetrain.tr.twa.DelegationService { <init>(); }
|
||||||
|
-keep class me.humetrain.tr.twa.LauncherActivity { <init>(); }
|
||||||
|
-keep class android.widget.Space { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.app.AlertController$RecycleListView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.view.menu.ActionMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.view.menu.ExpandedMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.view.menu.ListMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ActionBarContainer { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ActionBarContextView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ActionBarOverlayLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ActionMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ActivityChooserView$InnerLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.AlertDialogLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ButtonBarLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ContentFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.DialogTitle { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.FitWindowsFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.FitWindowsLinearLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.SearchView$SearchAutoComplete { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.Toolbar { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.appcompat.widget.ViewStubCompat { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.browser.browseractions.BrowserActionsFallbackMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
-keep class androidx.core.widget.NestedScrollView { <init>(android.content.Context, android.util.AttributeSet); }
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#- File Locator -
|
||||||
|
listingFile=../../../../outputs/apk/release/output-metadata.json
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
appMetadataVersion=1.1
|
||||||
|
androidGradlePluginVersion=8.9.1
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "BUNDLE",
|
||||||
|
"kind": "RegularFile"
|
||||||
|
},
|
||||||
|
"applicationId": "me.humetrain.tr.twa",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"outputFile": "../../../../outputs/bundle/release/app-release.aab"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#- File Locator -
|
||||||
|
listingFile=../../../bundle_ide_model/release/produceReleaseBundleIdeListingFile/output-metadata.json
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2019 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
The "package" attribute is rewritten by the Gradle build with the value of applicationId.
|
||||||
|
It is still required here, as it is used to derive paths, for instance when referring
|
||||||
|
to an Activity by ".MyActivity" instead of the full name. If more Activities are added to the
|
||||||
|
application, the package attribute will need to reflect the correct path in order to use
|
||||||
|
the abbreviated format.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="me.humetrain.tr.twa"
|
||||||
|
android:versionCode="5"
|
||||||
|
android:versionName="5" >
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="21"
|
||||||
|
android:targetSdkVersion="35" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="me.humetrain.tr.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
|
<uses-permission android:name="me.humetrain.tr.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name="me.humetrain.tr.twa.Application"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/appName"
|
||||||
|
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" >
|
||||||
|
<meta-data
|
||||||
|
android:name="asset_statements"
|
||||||
|
android:resource="@string/assetStatements" />
|
||||||
|
<meta-data
|
||||||
|
android:name="web_manifest_url"
|
||||||
|
android:value="@string/webManifestUrl" />
|
||||||
|
<meta-data
|
||||||
|
android:name="twa_generator"
|
||||||
|
android:value="@string/generatorApp" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
|
||||||
|
android:enabled="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
|
||||||
|
android:value="@string/launchUrl" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="me.humetrain.tr.twa.LauncherActivity"
|
||||||
|
android:alwaysRetainTaskState="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/launcherName" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.DEFAULT_URL"
|
||||||
|
android:value="@string/launchUrl" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
|
||||||
|
android:resource="@color/colorPrimary" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
|
||||||
|
android:resource="@color/colorPrimaryDark" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
|
||||||
|
android:resource="@color/navigationColor" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
|
||||||
|
android:resource="@color/navigationColorDark" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR"
|
||||||
|
android:resource="@color/navigationDividerColor" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR_DARK"
|
||||||
|
android:resource="@color/navigationDividerColorDark" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
|
||||||
|
android:resource="@drawable/splash" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
|
||||||
|
android:resource="@color/backgroundColor" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
|
||||||
|
android:value="@integer/splashScreenFadeOutDuration" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
|
||||||
|
android:value="@string/providerAuthority" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
|
||||||
|
android:value="@string/fallbackType" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
|
||||||
|
android:value="@string/orientation" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true" >
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="@string/hostName"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
|
||||||
|
android:configChanges="orientation|screenSize" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="@string/providerAuthority"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/filepaths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="me.humetrain.tr.twa.DelegationService"
|
||||||
|
android:enabled="@bool/enableNotification"
|
||||||
|
android:exported="@bool/enableNotification" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SMALL_ICON"
|
||||||
|
android:resource="@drawable/ic_notification_icon" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity android:name="com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity" />
|
||||||
|
<activity android:name="com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.common.api.GoogleApiActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.version"
|
||||||
|
android:value="@integer/google_play_services_version" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="me.humetrain.tr.twa.androidx-startup"
|
||||||
|
android:exported="false" >
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
||||||
|
android:value="androidx.startup" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
|
||||||
|
android:value="androidx.startup" />
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
|
||||||
|
android:value="androidx.startup" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||||
|
android:directBootAware="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.DUMP" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.profileinstaller.action.SKIP_FILE" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,873 @@
|
||||||
|
Landroidx/activity/a;
|
||||||
|
Landroidx/activity/ComponentActivity$4;
|
||||||
|
Landroidx/lifecycle/j;
|
||||||
|
Landroidx/lifecycle/k;
|
||||||
|
HSPLandroidx/activity/ComponentActivity$4;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
||||||
|
Landroidx/activity/b;
|
||||||
|
Landroidx/core/app/b;
|
||||||
|
Landroidx/lifecycle/l;
|
||||||
|
Landroidx/lifecycle/E;
|
||||||
|
Landroidx/lifecycle/f;
|
||||||
|
LI/d;
|
||||||
|
Landroidx/activity/c;
|
||||||
|
HSPLandroidx/activity/c;-><init>(Z)V
|
||||||
|
HSPLandroidx/activity/c;->a()V
|
||||||
|
PLandroidx/activity/c;->b(Landroidx/activity/a;)V
|
||||||
|
HSPLandroidx/activity/c;->c(Z)V
|
||||||
|
Landroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;
|
||||||
|
PLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->d()V
|
||||||
|
HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
||||||
|
Landroidx/activity/OnBackPressedDispatcher;
|
||||||
|
HSPLandroidx/activity/OnBackPressedDispatcher;->a(Landroidx/activity/c;)Landroidx/activity/a;
|
||||||
|
Lc/a;
|
||||||
|
Lc/d;
|
||||||
|
Lc/e;
|
||||||
|
Lc/f;
|
||||||
|
Lc/g;
|
||||||
|
Lc/h;
|
||||||
|
Lc/i;
|
||||||
|
HSPLc/i;-><clinit>()V
|
||||||
|
Ld/a;
|
||||||
|
HSPLd/a;-><init>(II)V
|
||||||
|
Le/a;
|
||||||
|
Lg/b;
|
||||||
|
Lh/a;
|
||||||
|
HSPLh/a;-><init>(Landroid/content/Context;)V
|
||||||
|
HSPLh/a;->a(Landroid/content/Context;)Lh/a;
|
||||||
|
HSPLh/a;->b()I
|
||||||
|
HSPLh/a;->c()I
|
||||||
|
HSPLh/a;->d()Z
|
||||||
|
Lh/b;
|
||||||
|
HSPLh/b;-><clinit>()V
|
||||||
|
HSPLh/b;-><init>(Landroid/content/Context;)V
|
||||||
|
Li/a;
|
||||||
|
Ls/b;
|
||||||
|
HSPLi/a;-><init>(Landroid/content/Context;IIIILjava/lang/CharSequence;)V
|
||||||
|
Landroidx/appcompat/view/menu/a;
|
||||||
|
Landroidx/appcompat/view/menu/h;
|
||||||
|
HSPLandroidx/appcompat/view/menu/a;-><init>(Landroid/content/Context;II)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/a;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
||||||
|
PLandroidx/appcompat/view/menu/a;->a(Landroidx/appcompat/view/menu/d;Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/a;->f(Landroidx/appcompat/view/menu/h$a;)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/a;->j(Z)V
|
||||||
|
Landroidx/appcompat/view/menu/d$a;
|
||||||
|
Landroidx/appcompat/view/menu/d$b;
|
||||||
|
Landroidx/appcompat/view/menu/d;
|
||||||
|
Ls/a;
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;-><init>(Landroid/content/Context;)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->b(Landroidx/appcompat/view/menu/h;Landroid/content/Context;)V
|
||||||
|
PLandroidx/appcompat/view/menu/d;->close()V
|
||||||
|
PLandroidx/appcompat/view/menu/d;->d(Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->h(Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->q()V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->r()Ljava/util/ArrayList;
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->v()Ljava/util/ArrayList;
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->A()Ljava/util/ArrayList;
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->hasVisibleItems()Z
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->H(Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->M(Landroidx/appcompat/view/menu/d$a;)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->setQwertyMode(Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->U(Z)V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->size()I
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->V()V
|
||||||
|
HSPLandroidx/appcompat/view/menu/d;->W()V
|
||||||
|
Landroidx/appcompat/view/menu/h$a;
|
||||||
|
Landroidx/appcompat/view/menu/i;
|
||||||
|
Landroidx/appcompat/widget/a$a;
|
||||||
|
HSPLandroidx/appcompat/widget/a$a;-><init>(Landroidx/appcompat/widget/a;)V
|
||||||
|
Landroidx/appcompat/widget/a;
|
||||||
|
HSPLandroidx/appcompat/widget/a;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
Landroidx/appcompat/widget/b;
|
||||||
|
HSPLandroidx/appcompat/widget/b;-><init>(Landroidx/appcompat/widget/ActionBarContainer;)V
|
||||||
|
HSPLandroidx/appcompat/widget/b;->draw(Landroid/graphics/Canvas;)V
|
||||||
|
HSPLandroidx/appcompat/widget/b;->getOpacity()I
|
||||||
|
HSPLandroidx/appcompat/widget/b;->getOutline(Landroid/graphics/Outline;)V
|
||||||
|
Landroidx/appcompat/widget/ActionBarContainer;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->drawableStateChanged()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->jumpDrawablesToCurrentState()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->onFinishInflate()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContainer;->setTabContainer(Landroidx/appcompat/widget/G;)V
|
||||||
|
PLandroidx/appcompat/widget/ActionBarContainer;->verifyDrawable(Landroid/graphics/drawable/Drawable;)Z
|
||||||
|
Landroidx/appcompat/widget/ActionBarContextView;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
PLandroidx/appcompat/widget/ActionBarContextView;->onDetachedFromWindow()V
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout$a;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$a;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout$b;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$b;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout$c;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$c;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout$d;
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$e;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
Landroidx/appcompat/widget/ActionBarOverlayLayout;
|
||||||
|
Landroidx/core/view/o;
|
||||||
|
Landroidx/core/view/p;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->h(Landroid/view/View;Landroid/graphics/Rect;ZZZZ)Z
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->k(Landroid/util/AttributeSet;)Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->l(Landroid/view/View;)Landroidx/appcompat/widget/u;
|
||||||
|
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->m()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->n(Landroid/content/Context;)V
|
||||||
|
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->onDetachedFromWindow()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onStartNestedScroll(Landroid/view/View;Landroid/view/View;I)Z
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->f(Landroid/view/View;Landroid/view/View;II)Z
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onWindowVisibilityChanged(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->q()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setActionBarVisibilityCallback(Landroidx/appcompat/widget/ActionBarOverlayLayout$d;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setHasNonEmbeddedTabs(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowCallback(Landroid/view/Window$Callback;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowTitle(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->shouldDelayChildPressedState()Z
|
||||||
|
Landroidx/appcompat/widget/c$d$a;
|
||||||
|
Landroidx/appcompat/widget/y;
|
||||||
|
HSPLandroidx/appcompat/widget/c$d$a;-><init>(Landroidx/appcompat/widget/c$d;Landroid/view/View;Landroidx/appcompat/widget/c;)V
|
||||||
|
Landroidx/appcompat/widget/c$d;
|
||||||
|
Landroidx/appcompat/widget/l;
|
||||||
|
Landroidx/appcompat/widget/ActionMenuView$a;
|
||||||
|
HSPLandroidx/appcompat/widget/c$d;-><init>(Landroidx/appcompat/widget/c;Landroid/content/Context;)V
|
||||||
|
Landroidx/appcompat/widget/c$f;
|
||||||
|
HSPLandroidx/appcompat/widget/c$f;-><init>(Landroidx/appcompat/widget/c;)V
|
||||||
|
Landroidx/appcompat/widget/c;
|
||||||
|
HSPLandroidx/appcompat/widget/c;-><init>(Landroid/content/Context;)V
|
||||||
|
PLandroidx/appcompat/widget/c;->w()Z
|
||||||
|
HSPLandroidx/appcompat/widget/c;->h()Z
|
||||||
|
PLandroidx/appcompat/widget/c;->z()Z
|
||||||
|
PLandroidx/appcompat/widget/c;->A()Z
|
||||||
|
HSPLandroidx/appcompat/widget/c;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
||||||
|
PLandroidx/appcompat/widget/c;->a(Landroidx/appcompat/view/menu/d;Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/c;->D(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/c;->E(Landroidx/appcompat/widget/ActionMenuView;)V
|
||||||
|
HSPLandroidx/appcompat/widget/c;->j(Z)V
|
||||||
|
Landroidx/appcompat/widget/ActionMenuView$e;
|
||||||
|
Landroidx/appcompat/widget/ActionMenuView;
|
||||||
|
Landroidx/appcompat/widget/z;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
PLandroidx/appcompat/widget/ActionMenuView;->w()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->C(Landroidx/appcompat/view/menu/d;)V
|
||||||
|
PLandroidx/appcompat/widget/ActionMenuView;->onDetachedFromWindow()V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->G()Landroidx/appcompat/view/menu/d;
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->H(Landroidx/appcompat/view/menu/h$a;Landroidx/appcompat/view/menu/d$a;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->setOnMenuItemClickListener(Landroidx/appcompat/widget/ActionMenuView$e;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->setOverflowReserved(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->setPopupTheme(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/ActionMenuView;->setPresenter(Landroidx/appcompat/widget/c;)V
|
||||||
|
Landroidx/appcompat/widget/e;
|
||||||
|
HSPLandroidx/appcompat/widget/e;-><init>(Landroid/view/View;)V
|
||||||
|
HSPLandroidx/appcompat/widget/e;->b()V
|
||||||
|
HSPLandroidx/appcompat/widget/e;->e(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/e;->k()Z
|
||||||
|
Landroidx/appcompat/widget/f$a;
|
||||||
|
Landroidx/appcompat/widget/D$f;
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;-><init>()V
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;->f([II)Z
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;->a(Landroidx/appcompat/widget/D;Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;->b(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;->d(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
|
||||||
|
HSPLandroidx/appcompat/widget/f$a;->c(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
|
||||||
|
Landroidx/appcompat/widget/f;
|
||||||
|
HSPLandroidx/appcompat/widget/f;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/f;-><init>()V
|
||||||
|
HSPLandroidx/appcompat/widget/f;->a()Landroid/graphics/PorterDuff$Mode;
|
||||||
|
HSPLandroidx/appcompat/widget/f;->b()Landroidx/appcompat/widget/f;
|
||||||
|
HSPLandroidx/appcompat/widget/f;->e(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
|
||||||
|
HSPLandroidx/appcompat/widget/f;->f()V
|
||||||
|
Landroidx/appcompat/widget/g;
|
||||||
|
HSPLandroidx/appcompat/widget/g;-><init>(Landroid/widget/EditText;)V
|
||||||
|
HSPLandroidx/appcompat/widget/g;->a(Landroid/text/method/KeyListener;)Landroid/text/method/KeyListener;
|
||||||
|
HSPLandroidx/appcompat/widget/g;->c(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/g;->e(Z)V
|
||||||
|
Landroidx/appcompat/widget/h;
|
||||||
|
HSPLandroidx/appcompat/widget/h;-><init>(Landroid/widget/TextView;)V
|
||||||
|
HSPLandroidx/appcompat/widget/h;->a([Landroid/text/InputFilter;)[Landroid/text/InputFilter;
|
||||||
|
HSPLandroidx/appcompat/widget/h;->b(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/h;->d(Z)V
|
||||||
|
Landroidx/appcompat/widget/j;
|
||||||
|
HSPLandroidx/appcompat/widget/j;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/j;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/j;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
Landroidx/appcompat/widget/k;
|
||||||
|
HSPLandroidx/appcompat/widget/k;-><init>(Landroid/widget/ImageView;)V
|
||||||
|
HSPLandroidx/appcompat/widget/k;->b()V
|
||||||
|
HSPLandroidx/appcompat/widget/k;->c()V
|
||||||
|
HSPLandroidx/appcompat/widget/k;->g(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/l;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/l;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/l;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
Landroidx/appcompat/widget/n;
|
||||||
|
HSPLandroidx/appcompat/widget/n;-><init>(Landroid/widget/TextView;)V
|
||||||
|
Landroidx/appcompat/widget/o$a;
|
||||||
|
Landroidx/core/content/res/h$e;
|
||||||
|
HSPLandroidx/appcompat/widget/o$a;-><init>(Landroidx/appcompat/widget/o;IILjava/lang/ref/WeakReference;)V
|
||||||
|
HSPLandroidx/appcompat/widget/o$a;->f(I)V
|
||||||
|
Landroidx/appcompat/widget/o;
|
||||||
|
HSPLandroidx/appcompat/widget/o;-><init>(Landroid/widget/TextView;)V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->b()V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->d(Landroid/content/Context;Landroidx/appcompat/widget/f;I)Landroidx/appcompat/widget/J;
|
||||||
|
HSPLandroidx/appcompat/widget/o;->m(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->o(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->q(Landroid/content/Context;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->y(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/o;->C(Landroid/content/Context;Landroidx/appcompat/widget/L;)V
|
||||||
|
Landroidx/appcompat/widget/p;
|
||||||
|
Landroidx/core/widget/h;
|
||||||
|
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->r()V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->drawableStateChanged()V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->getEmojiTextViewHelper()Landroidx/appcompat/widget/h;
|
||||||
|
HSPLandroidx/appcompat/widget/p;->getText()Ljava/lang/CharSequence;
|
||||||
|
HSPLandroidx/appcompat/widget/p;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->onTextChanged(Ljava/lang/CharSequence;III)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->setCompoundDrawables(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->setCompoundDrawablesWithIntrinsicBounds(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->setFilters([Landroid/text/InputFilter;)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->setTextAppearance(Landroid/content/Context;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/p;->setTypeface(Landroid/graphics/Typeface;I)V
|
||||||
|
Landroidx/appcompat/widget/q$b;
|
||||||
|
Landroidx/appcompat/widget/q$d;
|
||||||
|
HSPLandroidx/appcompat/widget/q$b;-><init>()V
|
||||||
|
Landroidx/appcompat/widget/q$c;
|
||||||
|
HSPLandroidx/appcompat/widget/q$c;-><init>()V
|
||||||
|
HSPLandroidx/appcompat/widget/q$d;-><init>()V
|
||||||
|
Landroidx/appcompat/widget/q;
|
||||||
|
HSPLandroidx/appcompat/widget/q;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/q;-><init>(Landroid/widget/TextView;)V
|
||||||
|
HSPLandroidx/appcompat/widget/q;->j()I
|
||||||
|
HSPLandroidx/appcompat/widget/q;->o(Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/q;->y()Z
|
||||||
|
Landroidx/appcompat/widget/ContentFrameLayout$a;
|
||||||
|
Landroidx/appcompat/widget/ContentFrameLayout;
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMajor()Landroid/util/TypedValue;
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMinor()Landroid/util/TypedValue;
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onAttachedToWindow()V
|
||||||
|
PLandroidx/appcompat/widget/ContentFrameLayout;->onDetachedFromWindow()V
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/ContentFrameLayout;->setAttachListener(Landroidx/appcompat/widget/ContentFrameLayout$a;)V
|
||||||
|
Landroidx/appcompat/widget/u;
|
||||||
|
Landroidx/appcompat/widget/v;
|
||||||
|
HSPLandroidx/appcompat/widget/y;-><init>(Landroid/view/View;)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->getVirtualChildCount()I
|
||||||
|
HSPLandroidx/appcompat/widget/z;->p(IIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->s(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->onInitializeAccessibilityNodeInfo(Landroid/view/accessibility/AccessibilityNodeInfo;)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->setBaselineAligned(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/z;->setDividerDrawable(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
Landroidx/appcompat/widget/D$c;
|
||||||
|
Lp/h;
|
||||||
|
Landroidx/appcompat/widget/D;
|
||||||
|
Landroidx/appcompat/widget/E;
|
||||||
|
Landroidx/appcompat/widget/F;
|
||||||
|
HSPLandroidx/appcompat/widget/F;-><init>()V
|
||||||
|
HSPLandroidx/appcompat/widget/F;->a()I
|
||||||
|
HSPLandroidx/appcompat/widget/F;->d()I
|
||||||
|
HSPLandroidx/appcompat/widget/F;->e(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/F;->f(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/F;->g(II)V
|
||||||
|
Landroidx/appcompat/widget/H;
|
||||||
|
HSPLandroidx/appcompat/widget/H;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/H;->a(Landroid/view/View;Landroid/content/Context;)V
|
||||||
|
Landroidx/appcompat/widget/I;
|
||||||
|
HSPLandroidx/appcompat/widget/I;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/I;->a(Landroid/content/Context;)Z
|
||||||
|
HSPLandroidx/appcompat/widget/I;->b(Landroid/content/Context;)Landroid/content/Context;
|
||||||
|
Landroidx/appcompat/widget/K;
|
||||||
|
Landroidx/appcompat/widget/L;
|
||||||
|
HSPLandroidx/appcompat/widget/L;-><init>(Landroid/content/Context;Landroid/content/res/TypedArray;)V
|
||||||
|
HSPLandroidx/appcompat/widget/L;->a(IZ)Z
|
||||||
|
HSPLandroidx/appcompat/widget/L;->b(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->c(I)Landroid/content/res/ColorStateList;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->d(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->e(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->f(I)Landroid/graphics/drawable/Drawable;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->g(IF)F
|
||||||
|
HSPLandroidx/appcompat/widget/L;->h(IILandroidx/core/content/res/h$e;)Landroid/graphics/Typeface;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->i(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->j(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->k(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->l(II)I
|
||||||
|
HSPLandroidx/appcompat/widget/L;->m(I)Ljava/lang/String;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->n(I)Ljava/lang/CharSequence;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->o()Landroid/content/res/TypedArray;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->p(I)Z
|
||||||
|
HSPLandroidx/appcompat/widget/L;->q(Landroid/content/Context;I[I)Landroidx/appcompat/widget/L;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->r(Landroid/content/Context;Landroid/util/AttributeSet;[I)Landroidx/appcompat/widget/L;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->s(Landroid/content/Context;Landroid/util/AttributeSet;[III)Landroidx/appcompat/widget/L;
|
||||||
|
HSPLandroidx/appcompat/widget/L;->u()V
|
||||||
|
Landroidx/appcompat/widget/Toolbar$a;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$a;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
||||||
|
Landroidx/appcompat/widget/Toolbar$b;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$b;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
||||||
|
Landroidx/appcompat/widget/Toolbar$f;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$f;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$f;->h()Z
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$f;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
||||||
|
PLandroidx/appcompat/widget/Toolbar$f;->a(Landroidx/appcompat/view/menu/d;Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$f;->j(Z)V
|
||||||
|
Landroidx/appcompat/widget/Toolbar$g;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar$g;-><init>(II)V
|
||||||
|
Landroidx/appcompat/widget/Toolbar;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->b(Ljava/util/List;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->c(Landroid/view/View;Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->f()V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->i()V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->j()V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->k()Landroidx/appcompat/widget/Toolbar$g;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->o(Landroid/view/View;I)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->p(I)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetEnd()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetStart()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetEnd()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetLeft()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetRight()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetStart()I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->q(Landroid/view/View;)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationContentDescription()Ljava/lang/CharSequence;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationIcon()Landroid/graphics/drawable/Drawable;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getSubtitle()Ljava/lang/CharSequence;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getTitle()Ljava/lang/CharSequence;
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->r(Landroid/view/View;)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->s(Ljava/util/List;[I)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->getWrapper()Landroidx/appcompat/widget/u;
|
||||||
|
PLandroidx/appcompat/widget/Toolbar;->t()Z
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->w(Landroid/view/View;)Z
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->z(Landroid/view/View;I[II)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->A(Landroid/view/View;IIII[I)I
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->B(Landroid/view/View;IIIII)V
|
||||||
|
PLandroidx/appcompat/widget/Toolbar;->onDetachedFromWindow()V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->onLayout(ZIIII)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->onMeasure(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->onRtlPropertiesChanged(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setCollapsible(Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->F(II)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationContentDescription(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationIcon(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationOnClickListener(Landroid/view/View$OnClickListener;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setPopupTheme(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setSubtitle(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->G(Landroid/content/Context;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->setTitle(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->H(Landroid/content/Context;I)V
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->I()Z
|
||||||
|
HSPLandroidx/appcompat/widget/Toolbar;->J(Landroid/view/View;)Z
|
||||||
|
Landroidx/appcompat/widget/P$a;
|
||||||
|
HSPLandroidx/appcompat/widget/P$a;-><init>(Landroidx/appcompat/widget/P;)V
|
||||||
|
Landroidx/appcompat/widget/P;
|
||||||
|
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;Z)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;ZII)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->e()Landroid/content/Context;
|
||||||
|
HSPLandroidx/appcompat/widget/P;->g(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->h(I)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->l(Landroid/graphics/drawable/Drawable;)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->o(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->c(Landroid/view/Window$Callback;)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->b(Ljava/lang/CharSequence;)V
|
||||||
|
HSPLandroidx/appcompat/widget/P;->q()V
|
||||||
|
Landroidx/appcompat/widget/Q;
|
||||||
|
HSPLandroidx/appcompat/widget/Q;->a(Landroid/view/View;Ljava/lang/CharSequence;)V
|
||||||
|
Landroidx/appcompat/widget/W;
|
||||||
|
HSPLandroidx/appcompat/widget/W;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/W;->b()Z
|
||||||
|
HSPLandroidx/appcompat/widget/W;->c()Z
|
||||||
|
Landroidx/appcompat/widget/X;
|
||||||
|
HSPLandroidx/appcompat/widget/X;-><clinit>()V
|
||||||
|
HSPLandroidx/appcompat/widget/X;->a(Landroid/view/View;)Z
|
||||||
|
LE/b;
|
||||||
|
LE/c;
|
||||||
|
HSPLE/c;-><clinit>()V
|
||||||
|
Landroidx/fragment/app/a;
|
||||||
|
Landroidx/fragment/app/y;
|
||||||
|
Landroidx/fragment/app/q$h;
|
||||||
|
HSPLandroidx/fragment/app/a;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
HSPLandroidx/fragment/app/a;->i(I)V
|
||||||
|
HSPLandroidx/fragment/app/a;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
|
||||||
|
HSPLandroidx/fragment/app/a;->k()V
|
||||||
|
HSPLandroidx/fragment/app/a;->m(Ljava/util/ArrayList;Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/a;->a(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
|
||||||
|
HSPLandroidx/fragment/app/a;->n()V
|
||||||
|
Landroidx/fragment/app/d;
|
||||||
|
Landroidx/fragment/app/E;
|
||||||
|
HSPLandroidx/fragment/app/d;-><init>(Landroid/view/ViewGroup;)V
|
||||||
|
Landroidx/fragment/app/Fragment$a;
|
||||||
|
HSPLandroidx/fragment/app/Fragment$a;-><init>(Landroidx/fragment/app/Fragment;)V
|
||||||
|
Landroidx/fragment/app/Fragment$c;
|
||||||
|
Landroidx/fragment/app/g;
|
||||||
|
HSPLandroidx/fragment/app/Fragment$c;-><init>(Landroidx/fragment/app/Fragment;)V
|
||||||
|
Landroidx/fragment/app/Fragment$d;
|
||||||
|
HSPLandroidx/fragment/app/Fragment$d;-><init>()V
|
||||||
|
Landroidx/fragment/app/Fragment$e;
|
||||||
|
PLandroidx/fragment/app/Fragment$e;->a(Landroid/view/View;)V
|
||||||
|
Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;-><init>()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->h()Landroidx/fragment/app/g;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->i()Landroidx/fragment/app/Fragment$d;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->equals(Ljava/lang/Object;)Z
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->j()Landroidx/fragment/app/e;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->n()Landroidx/fragment/app/q;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->o()Landroid/content/Context;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->v()Landroid/view/View;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->w(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->g()Landroidx/lifecycle/g;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->x()I
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->z()Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->A()Landroidx/fragment/app/q;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->E()F
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->b()Landroidx/savedstate/a;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->M()Landroid/view/View;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->c()Landroidx/lifecycle/D;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->N()V
|
||||||
|
PLandroidx/fragment/app/Fragment;->O()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->P()Z
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->S()Z
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->U(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->V(Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/fragment/app/Fragment;->b0()V
|
||||||
|
PLandroidx/fragment/app/Fragment;->c0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->d0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->f0(Landroid/content/Context;Landroid/util/AttributeSet;Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/fragment/app/Fragment;->h0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->j0(Z)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->k0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->m0()V
|
||||||
|
PLandroidx/fragment/app/Fragment;->n0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->o0(Landroid/view/View;Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->p0(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->q0(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->r0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->t0(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->u0(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->v0(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/fragment/app/Fragment;->w0()V
|
||||||
|
PLandroidx/fragment/app/Fragment;->x0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->y0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
||||||
|
PLandroidx/fragment/app/Fragment;->B0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->C0(Landroid/view/Menu;)Z
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->D0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->E0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->G0()V
|
||||||
|
PLandroidx/fragment/app/Fragment;->H0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->I0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->L0()Landroid/content/Context;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->M0()Landroid/view/View;
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->N0(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->O0()V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->P0(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->Q0(IIII)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->R0(Landroid/view/View;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->S0(I)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->T0(Z)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->U0(F)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->V0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
|
||||||
|
HSPLandroidx/fragment/app/Fragment;->toString()Ljava/lang/String;
|
||||||
|
Landroidx/fragment/app/e;
|
||||||
|
HSPLandroidx/fragment/app/g;-><init>()V
|
||||||
|
Landroidx/fragment/app/h;
|
||||||
|
PLandroidx/fragment/app/h;->a(Landroid/view/View;)V
|
||||||
|
HSPLandroidx/fragment/app/h;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
|
||||||
|
HSPLandroidx/fragment/app/h;->dispatchDraw(Landroid/graphics/Canvas;)V
|
||||||
|
HSPLandroidx/fragment/app/h;->drawChild(Landroid/graphics/Canvas;Landroid/view/View;J)Z
|
||||||
|
PLandroidx/fragment/app/h;->removeView(Landroid/view/View;)V
|
||||||
|
Landroidx/fragment/app/i;
|
||||||
|
HSPLandroidx/fragment/app/i;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/i;-><init>()V
|
||||||
|
HSPLandroidx/fragment/app/i;->b(Ljava/lang/ClassLoader;Ljava/lang/String;)Z
|
||||||
|
HSPLandroidx/fragment/app/i;->c(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;
|
||||||
|
Landroidx/fragment/app/j;
|
||||||
|
Landroidx/fragment/app/k;
|
||||||
|
HSPLandroidx/fragment/app/k;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
HSPLandroidx/fragment/app/k;->onCreateView(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;
|
||||||
|
Landroidx/fragment/app/l;
|
||||||
|
HSPLandroidx/fragment/app/l;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
HSPLandroidx/fragment/app/l;->a(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->b(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->c(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
||||||
|
PLandroidx/fragment/app/l;->d(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
PLandroidx/fragment/app/l;->e(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->f(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->g(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->h(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->j(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
PLandroidx/fragment/app/l;->k(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/l;->l(Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;Z)V
|
||||||
|
PLandroidx/fragment/app/l;->m(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
Landroidx/fragment/app/q$a;
|
||||||
|
HSPLandroidx/fragment/app/q$a;-><init>(Landroidx/fragment/app/q;Z)V
|
||||||
|
Landroidx/fragment/app/q$b;
|
||||||
|
Landroidx/core/view/l;
|
||||||
|
HSPLandroidx/fragment/app/q$b;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/q$c;
|
||||||
|
HSPLandroidx/fragment/app/q$c;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/q$d;
|
||||||
|
Landroidx/fragment/app/F;
|
||||||
|
HSPLandroidx/fragment/app/q$d;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/FragmentManager$6;
|
||||||
|
Landroidx/fragment/app/q$f;
|
||||||
|
Landroidx/fragment/app/u;
|
||||||
|
Landroidx/fragment/app/q;
|
||||||
|
HSPLandroidx/fragment/app/q;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/q;-><init>()V
|
||||||
|
HSPLandroidx/fragment/app/q;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
|
||||||
|
HSPLandroidx/fragment/app/q;->i(Landroidx/fragment/app/u;)V
|
||||||
|
HSPLandroidx/fragment/app/q;->j(Landroidx/fragment/app/j;Landroidx/fragment/app/g;Landroidx/fragment/app/Fragment;)V
|
||||||
|
HSPLandroidx/fragment/app/q;->l()Landroidx/fragment/app/y;
|
||||||
|
HSPLandroidx/fragment/app/q;->m()Z
|
||||||
|
HSPLandroidx/fragment/app/q;->n()V
|
||||||
|
PLandroidx/fragment/app/q;->o()V
|
||||||
|
HSPLandroidx/fragment/app/q;->p()Ljava/util/Set;
|
||||||
|
HSPLandroidx/fragment/app/q;->q(Ljava/util/ArrayList;II)Ljava/util/Set;
|
||||||
|
HSPLandroidx/fragment/app/q;->r(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
|
||||||
|
HSPLandroidx/fragment/app/q;->t()V
|
||||||
|
HSPLandroidx/fragment/app/q;->v()V
|
||||||
|
HSPLandroidx/fragment/app/q;->w(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
|
||||||
|
PLandroidx/fragment/app/q;->x()V
|
||||||
|
PLandroidx/fragment/app/q;->y()V
|
||||||
|
HSPLandroidx/fragment/app/q;->C(Landroidx/fragment/app/Fragment;)V
|
||||||
|
PLandroidx/fragment/app/q;->D()V
|
||||||
|
HSPLandroidx/fragment/app/q;->E(Landroid/view/Menu;)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->F()V
|
||||||
|
HSPLandroidx/fragment/app/q;->G()V
|
||||||
|
HSPLandroidx/fragment/app/q;->H()V
|
||||||
|
HSPLandroidx/fragment/app/q;->I(I)V
|
||||||
|
PLandroidx/fragment/app/q;->J()V
|
||||||
|
HSPLandroidx/fragment/app/q;->K()V
|
||||||
|
HSPLandroidx/fragment/app/q;->L()V
|
||||||
|
PLandroidx/fragment/app/q;->M()V
|
||||||
|
HSPLandroidx/fragment/app/q;->N(Z)V
|
||||||
|
HSPLandroidx/fragment/app/q;->O(Z)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->Q(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
|
||||||
|
HSPLandroidx/fragment/app/q;->R(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
|
||||||
|
HSPLandroidx/fragment/app/q;->S(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/q;->T(I)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/q;->Y(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->Z()I
|
||||||
|
HSPLandroidx/fragment/app/q;->a0(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
|
||||||
|
HSPLandroidx/fragment/app/q;->b0()Landroidx/fragment/app/g;
|
||||||
|
HSPLandroidx/fragment/app/q;->c0(Landroidx/fragment/app/Fragment;)Landroid/view/ViewGroup;
|
||||||
|
HSPLandroidx/fragment/app/q;->d0()Landroidx/fragment/app/i;
|
||||||
|
HSPLandroidx/fragment/app/q;->e0()Landroidx/fragment/app/j;
|
||||||
|
HSPLandroidx/fragment/app/q;->f0()Landroidx/fragment/app/l;
|
||||||
|
HSPLandroidx/fragment/app/q;->g0()Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/q;->h0()Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/q;->i0()Landroidx/fragment/app/F;
|
||||||
|
HSPLandroidx/fragment/app/q;->j0()LF/c$c;
|
||||||
|
HSPLandroidx/fragment/app/q;->k0(Landroid/view/View;)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/q;->l0(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
|
||||||
|
PLandroidx/fragment/app/q;->o0()Z
|
||||||
|
HSPLandroidx/fragment/app/q;->p0(I)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->q0(Landroidx/fragment/app/Fragment;)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->t0(Landroidx/fragment/app/Fragment;)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->u0(Landroidx/fragment/app/Fragment;)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->v0(I)Z
|
||||||
|
HSPLandroidx/fragment/app/q;->w0()Z
|
||||||
|
HSPLandroidx/fragment/app/q;->x0(IZ)V
|
||||||
|
HSPLandroidx/fragment/app/q;->y0()V
|
||||||
|
HSPLandroidx/fragment/app/q;->A0(Landroidx/fragment/app/w;)V
|
||||||
|
HSPLandroidx/fragment/app/q;->C0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
|
||||||
|
HSPLandroidx/fragment/app/q;->H0(Landroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/q;->J0(Landroidx/fragment/app/Fragment;)V
|
||||||
|
HSPLandroidx/fragment/app/q;->M0()V
|
||||||
|
HSPLandroidx/fragment/app/q;->N0()V
|
||||||
|
Landroidx/fragment/app/r;
|
||||||
|
HSPLandroidx/fragment/app/r;-><init>()V
|
||||||
|
Landroidx/fragment/app/t$a;
|
||||||
|
Landroidx/lifecycle/A$b;
|
||||||
|
HSPLandroidx/fragment/app/t$a;-><init>()V
|
||||||
|
HSPLandroidx/fragment/app/t$a;->b(Ljava/lang/Class;)Landroidx/lifecycle/z;
|
||||||
|
Landroidx/fragment/app/t;
|
||||||
|
Landroidx/lifecycle/z;
|
||||||
|
HSPLandroidx/fragment/app/t;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/t;-><init>(Z)V
|
||||||
|
PLandroidx/fragment/app/t;->f(Landroidx/fragment/app/Fragment;)V
|
||||||
|
PLandroidx/fragment/app/t;->g(Ljava/lang/String;)V
|
||||||
|
HSPLandroidx/fragment/app/t;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
|
||||||
|
HSPLandroidx/fragment/app/t;->k(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
|
||||||
|
PLandroidx/fragment/app/t;->d()V
|
||||||
|
HSPLandroidx/fragment/app/t;->m(Z)V
|
||||||
|
PLandroidx/fragment/app/t;->n(Landroidx/fragment/app/Fragment;)Z
|
||||||
|
Landroidx/fragment/app/w$a;
|
||||||
|
HSPLandroidx/fragment/app/w$a;-><init>(Landroidx/fragment/app/w;Landroid/view/View;)V
|
||||||
|
HSPLandroidx/fragment/app/w$a;->onViewAttachedToWindow(Landroid/view/View;)V
|
||||||
|
Landroidx/fragment/app/w$b;
|
||||||
|
HSPLandroidx/fragment/app/w$b;-><clinit>()V
|
||||||
|
Landroidx/fragment/app/w;
|
||||||
|
HSPLandroidx/fragment/app/w;-><init>(Landroidx/fragment/app/l;Landroidx/fragment/app/x;Landroidx/fragment/app/Fragment;)V
|
||||||
|
HSPLandroidx/fragment/app/w;->a()V
|
||||||
|
HSPLandroidx/fragment/app/w;->b()V
|
||||||
|
HSPLandroidx/fragment/app/w;->c()V
|
||||||
|
HSPLandroidx/fragment/app/w;->d()I
|
||||||
|
HSPLandroidx/fragment/app/w;->e()V
|
||||||
|
HSPLandroidx/fragment/app/w;->f()V
|
||||||
|
PLandroidx/fragment/app/w;->g()V
|
||||||
|
PLandroidx/fragment/app/w;->h()V
|
||||||
|
PLandroidx/fragment/app/w;->i()V
|
||||||
|
HSPLandroidx/fragment/app/w;->j()V
|
||||||
|
HSPLandroidx/fragment/app/w;->k()Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/w;->m()V
|
||||||
|
PLandroidx/fragment/app/w;->n()V
|
||||||
|
HSPLandroidx/fragment/app/w;->o()V
|
||||||
|
PLandroidx/fragment/app/w;->r()V
|
||||||
|
HSPLandroidx/fragment/app/w;->s(I)V
|
||||||
|
HSPLandroidx/fragment/app/w;->t()V
|
||||||
|
PLandroidx/fragment/app/w;->u()V
|
||||||
|
Landroidx/fragment/app/x;
|
||||||
|
HSPLandroidx/fragment/app/x;-><init>()V
|
||||||
|
HSPLandroidx/fragment/app/x;->a(Landroidx/fragment/app/Fragment;)V
|
||||||
|
HSPLandroidx/fragment/app/x;->b()V
|
||||||
|
HSPLandroidx/fragment/app/x;->c(Ljava/lang/String;)Z
|
||||||
|
HSPLandroidx/fragment/app/x;->d(I)V
|
||||||
|
HSPLandroidx/fragment/app/x;->e(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/x;->f(I)Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/x;->h(Landroidx/fragment/app/Fragment;)I
|
||||||
|
HSPLandroidx/fragment/app/x;->i()Ljava/util/List;
|
||||||
|
HSPLandroidx/fragment/app/x;->j()Ljava/util/List;
|
||||||
|
HSPLandroidx/fragment/app/x;->l(Ljava/lang/String;)Landroidx/fragment/app/w;
|
||||||
|
HSPLandroidx/fragment/app/x;->m()Ljava/util/List;
|
||||||
|
PLandroidx/fragment/app/x;->n()Landroidx/fragment/app/t;
|
||||||
|
HSPLandroidx/fragment/app/x;->p(Landroidx/fragment/app/w;)V
|
||||||
|
PLandroidx/fragment/app/x;->q(Landroidx/fragment/app/w;)V
|
||||||
|
HSPLandroidx/fragment/app/x;->r()V
|
||||||
|
HSPLandroidx/fragment/app/x;->y(Landroidx/fragment/app/t;)V
|
||||||
|
Landroidx/fragment/app/y$a;
|
||||||
|
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;)V
|
||||||
|
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;Z)V
|
||||||
|
HSPLandroidx/fragment/app/y;-><init>(Landroidx/fragment/app/i;Ljava/lang/ClassLoader;)V
|
||||||
|
HSPLandroidx/fragment/app/y;->d(Landroidx/fragment/app/y$a;)V
|
||||||
|
HSPLandroidx/fragment/app/y;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
|
||||||
|
HSPLandroidx/fragment/app/y;->h(Z)Landroidx/fragment/app/y;
|
||||||
|
Landroidx/fragment/app/C;
|
||||||
|
HSPLandroidx/fragment/app/C;-><init>(Landroidx/fragment/app/Fragment;Landroidx/lifecycle/D;)V
|
||||||
|
HSPLandroidx/fragment/app/C;->g()Landroidx/lifecycle/g;
|
||||||
|
HSPLandroidx/fragment/app/C;->b()Landroidx/savedstate/a;
|
||||||
|
HSPLandroidx/fragment/app/C;->d(Landroidx/lifecycle/g$a;)V
|
||||||
|
HSPLandroidx/fragment/app/C;->e()V
|
||||||
|
HSPLandroidx/fragment/app/C;->h(Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/fragment/app/C;->i(Landroid/os/Bundle;)V
|
||||||
|
Landroidx/fragment/app/E$a;
|
||||||
|
HSPLandroidx/fragment/app/E$a;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
|
||||||
|
HSPLandroidx/fragment/app/E$a;->run()V
|
||||||
|
Landroidx/fragment/app/E$b;
|
||||||
|
HSPLandroidx/fragment/app/E$b;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
|
||||||
|
HSPLandroidx/fragment/app/E$b;->run()V
|
||||||
|
Landroidx/fragment/app/E$c;
|
||||||
|
HSPLandroidx/fragment/app/E$c;-><clinit>()V
|
||||||
|
Landroidx/fragment/app/E$d;
|
||||||
|
Landroidx/fragment/app/E$e;
|
||||||
|
HSPLandroidx/fragment/app/E$d;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;Landroidx/core/os/b;)V
|
||||||
|
HSPLandroidx/fragment/app/E$d;->c()V
|
||||||
|
HSPLandroidx/fragment/app/E$d;->l()V
|
||||||
|
Landroidx/fragment/app/E$e$a;
|
||||||
|
Landroidx/core/os/b$a;
|
||||||
|
HSPLandroidx/fragment/app/E$e$a;-><init>(Landroidx/fragment/app/E$e;)V
|
||||||
|
Landroidx/fragment/app/E$e$b;
|
||||||
|
HSPLandroidx/fragment/app/E$e$b;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/E$e$b;-><init>(Ljava/lang/String;I)V
|
||||||
|
HSPLandroidx/fragment/app/E$e$b;->values()[Landroidx/fragment/app/E$e$b;
|
||||||
|
Landroidx/fragment/app/E$e$c;
|
||||||
|
HSPLandroidx/fragment/app/E$e$c;-><clinit>()V
|
||||||
|
HSPLandroidx/fragment/app/E$e$c;-><init>(Ljava/lang/String;I)V
|
||||||
|
HSPLandroidx/fragment/app/E$e$c;->a(Landroid/view/View;)V
|
||||||
|
HSPLandroidx/fragment/app/E$e$c;->b(I)Landroidx/fragment/app/E$e$c;
|
||||||
|
HSPLandroidx/fragment/app/E$e$c;->values()[Landroidx/fragment/app/E$e$c;
|
||||||
|
HSPLandroidx/fragment/app/E$e;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/Fragment;Landroidx/core/os/b;)V
|
||||||
|
HSPLandroidx/fragment/app/E$e;->a(Ljava/lang/Runnable;)V
|
||||||
|
HSPLandroidx/fragment/app/E$e;->b()V
|
||||||
|
HSPLandroidx/fragment/app/E$e;->c()V
|
||||||
|
HSPLandroidx/fragment/app/E$e;->e()Landroidx/fragment/app/E$e$c;
|
||||||
|
HSPLandroidx/fragment/app/E$e;->f()Landroidx/fragment/app/Fragment;
|
||||||
|
HSPLandroidx/fragment/app/E$e;->g()Landroidx/fragment/app/E$e$b;
|
||||||
|
HSPLandroidx/fragment/app/E$e;->h()Z
|
||||||
|
HSPLandroidx/fragment/app/E$e;->k(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;)V
|
||||||
|
HSPLandroidx/fragment/app/E;-><init>(Landroid/view/ViewGroup;)V
|
||||||
|
HSPLandroidx/fragment/app/E;->a(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;)V
|
||||||
|
HSPLandroidx/fragment/app/E;->b(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/w;)V
|
||||||
|
PLandroidx/fragment/app/E;->d(Landroidx/fragment/app/w;)V
|
||||||
|
HSPLandroidx/fragment/app/E;->g()V
|
||||||
|
HSPLandroidx/fragment/app/E;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
|
||||||
|
HSPLandroidx/fragment/app/E;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
|
||||||
|
HSPLandroidx/fragment/app/E;->j()V
|
||||||
|
HSPLandroidx/fragment/app/E;->l(Landroidx/fragment/app/w;)Landroidx/fragment/app/E$e$b;
|
||||||
|
HSPLandroidx/fragment/app/E;->n(Landroid/view/ViewGroup;Landroidx/fragment/app/q;)Landroidx/fragment/app/E;
|
||||||
|
HSPLandroidx/fragment/app/E;->o(Landroid/view/ViewGroup;Landroidx/fragment/app/F;)Landroidx/fragment/app/E;
|
||||||
|
HSPLandroidx/fragment/app/E;->p()V
|
||||||
|
HSPLandroidx/fragment/app/E;->q()V
|
||||||
|
HSPLandroidx/fragment/app/E;->r(Z)V
|
||||||
|
LF/c$a;
|
||||||
|
HSPLF/c$a;->a()[LF/c$a;
|
||||||
|
HSPLF/c$a;-><clinit>()V
|
||||||
|
HSPLF/c$a;-><init>(Ljava/lang/String;I)V
|
||||||
|
LF/c$c$a;
|
||||||
|
HSPLF/c$c$a;-><init>()V
|
||||||
|
HSPLF/c$c$a;-><init>(Lt0/d;)V
|
||||||
|
LF/c$c;
|
||||||
|
HSPLF/c$c;-><clinit>()V
|
||||||
|
HSPLF/c$c;-><init>(Ljava/util/Set;LF/c$b;Ljava/util/Map;)V
|
||||||
|
HSPLF/c$c;->a()Ljava/util/Set;
|
||||||
|
LF/c;
|
||||||
|
HSPLF/c;-><clinit>()V
|
||||||
|
HSPLF/c;-><init>()V
|
||||||
|
HSPLF/c;->b(Landroidx/fragment/app/Fragment;)LF/c$c;
|
||||||
|
HSPLF/c;->e(LF/d;)V
|
||||||
|
LF/d;
|
||||||
|
HSPLF/d;-><init>(Landroidx/fragment/app/Fragment;Ljava/lang/String;)V
|
||||||
|
Landroidx/lifecycle/d;
|
||||||
|
HSPLandroidx/lifecycle/d;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/d;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/lifecycle/d;->onActivityDestroyed(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/d;->onActivityPaused(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/d;->onActivityResumed(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/d;->onActivityStarted(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/d;->onActivityStopped(Landroid/app/Activity;)V
|
||||||
|
Landroidx/lifecycle/i$a;
|
||||||
|
HSPLandroidx/lifecycle/i$a;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/i$a;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
||||||
|
Landroidx/lifecycle/i;
|
||||||
|
HSPLandroidx/lifecycle/i;-><clinit>()V
|
||||||
|
HSPLandroidx/lifecycle/i;->a(Landroid/content/Context;)V
|
||||||
|
Landroidx/lifecycle/m$b;
|
||||||
|
HSPLandroidx/lifecycle/m$b;-><init>(Landroidx/lifecycle/k;Landroidx/lifecycle/g$b;)V
|
||||||
|
HSPLandroidx/lifecycle/m$b;->a(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
||||||
|
Landroidx/lifecycle/m;
|
||||||
|
Landroidx/lifecycle/g;
|
||||||
|
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;)V
|
||||||
|
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;Z)V
|
||||||
|
HSPLandroidx/lifecycle/m;->a(Landroidx/lifecycle/k;)V
|
||||||
|
HPLandroidx/lifecycle/m;->d(Landroidx/lifecycle/l;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->e(Landroidx/lifecycle/k;)Landroidx/lifecycle/g$b;
|
||||||
|
HSPLandroidx/lifecycle/m;->f(Ljava/lang/String;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->g(Landroidx/lifecycle/l;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->b()Landroidx/lifecycle/g$b;
|
||||||
|
HSPLandroidx/lifecycle/m;->h(Landroidx/lifecycle/g$a;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->i()Z
|
||||||
|
HSPLandroidx/lifecycle/m;->j(Landroidx/lifecycle/g$b;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->k()V
|
||||||
|
HSPLandroidx/lifecycle/m;->l(Landroidx/lifecycle/g$b;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->c(Landroidx/lifecycle/k;)V
|
||||||
|
HSPLandroidx/lifecycle/m;->m()V
|
||||||
|
Landroidx/lifecycle/LiveData$a;
|
||||||
|
HSPLandroidx/lifecycle/LiveData$a;-><init>(Landroidx/lifecycle/LiveData;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData$a;->run()V
|
||||||
|
Landroidx/lifecycle/LiveData$LifecycleBoundObserver;
|
||||||
|
Landroidx/lifecycle/LiveData$b;
|
||||||
|
PLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->i()V
|
||||||
|
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->j()Z
|
||||||
|
HSPLandroidx/lifecycle/LiveData$b;->d(Z)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData$b;->i()V
|
||||||
|
Landroidx/lifecycle/LiveData;
|
||||||
|
HSPLandroidx/lifecycle/LiveData;-><clinit>()V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->a(Ljava/lang/String;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->b(I)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->c(Landroidx/lifecycle/LiveData$b;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->d(Landroidx/lifecycle/LiveData$b;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->e()V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->f()V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->g(Landroidx/lifecycle/q;)V
|
||||||
|
HSPLandroidx/lifecycle/LiveData;->h(Ljava/lang/Object;)V
|
||||||
|
Landroidx/lifecycle/p;
|
||||||
|
HSPLandroidx/lifecycle/p;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/p;->h(Ljava/lang/Object;)V
|
||||||
|
Landroidx/lifecycle/ProcessLifecycleInitializer;
|
||||||
|
LJ/a;
|
||||||
|
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->c(Landroid/content/Context;)Landroidx/lifecycle/l;
|
||||||
|
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->b(Landroid/content/Context;)Ljava/lang/Object;
|
||||||
|
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->a()Ljava/util/List;
|
||||||
|
Landroidx/lifecycle/t;
|
||||||
|
HSPLandroidx/lifecycle/t;-><clinit>()V
|
||||||
|
HSPLandroidx/lifecycle/t;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/t;->g()Landroidx/lifecycle/g;
|
||||||
|
Landroidx/lifecycle/u$c;
|
||||||
|
HSPLandroidx/lifecycle/u$c;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityDestroyed(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityPaused(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityPostResumed(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityPostStarted(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityPreDestroyed(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityPrePaused(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityPreStopped(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityResumed(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->onActivityStarted(Landroid/app/Activity;)V
|
||||||
|
PLandroidx/lifecycle/u$c;->onActivityStopped(Landroid/app/Activity;)V
|
||||||
|
HSPLandroidx/lifecycle/u$c;->registerIn(Landroid/app/Activity;)V
|
||||||
|
Landroidx/lifecycle/u;
|
||||||
|
HSPLandroidx/lifecycle/u;-><init>()V
|
||||||
|
HSPLandroidx/lifecycle/u;->a(Landroidx/lifecycle/g$a;)V
|
||||||
|
HSPLandroidx/lifecycle/u;->b(Landroidx/lifecycle/u$a;)V
|
||||||
|
HSPLandroidx/lifecycle/u;->c(Landroidx/lifecycle/u$a;)V
|
||||||
|
HSPLandroidx/lifecycle/u;->d(Landroidx/lifecycle/u$a;)V
|
||||||
|
HSPLandroidx/lifecycle/u;->onActivityCreated(Landroid/os/Bundle;)V
|
||||||
|
PLandroidx/lifecycle/u;->onDestroy()V
|
||||||
|
PLandroidx/lifecycle/u;->onPause()V
|
||||||
|
HSPLandroidx/lifecycle/u;->onResume()V
|
||||||
|
HSPLandroidx/lifecycle/u;->onStart()V
|
||||||
|
PLandroidx/lifecycle/u;->onStop()V
|
||||||
|
HSPLandroidx/lifecycle/z;-><init>()V
|
||||||
|
PLandroidx/lifecycle/z;->a()V
|
||||||
|
PLandroidx/lifecycle/z;->d()V
|
||||||
|
Landroidx/lifecycle/A;
|
||||||
|
HSPLandroidx/lifecycle/A;-><init>(Landroidx/lifecycle/D;Landroidx/lifecycle/A$b;)V
|
||||||
|
HSPLandroidx/lifecycle/A;->a(Ljava/lang/Class;)Landroidx/lifecycle/z;
|
||||||
|
HSPLandroidx/lifecycle/A;->b(Ljava/lang/String;Ljava/lang/Class;)Landroidx/lifecycle/z;
|
||||||
|
Landroidx/lifecycle/D;
|
||||||
|
HSPLandroidx/lifecycle/D;-><init>()V
|
||||||
|
PLandroidx/lifecycle/D;->a()V
|
||||||
|
HSPLandroidx/lifecycle/D;->b(Ljava/lang/String;)Landroidx/lifecycle/z;
|
||||||
|
HSPLandroidx/lifecycle/D;->d(Ljava/lang/String;Landroidx/lifecycle/z;)V
|
||||||
|
Landroidx/lifecycle/F;
|
||||||
|
HSPLandroidx/lifecycle/F;->a(Landroid/view/View;Landroidx/lifecycle/l;)V
|
||||||
|
Landroidx/lifecycle/G;
|
||||||
|
HSPLandroidx/lifecycle/G;->a(Landroid/view/View;Landroidx/lifecycle/E;)V
|
||||||
|
Landroidx/startup/a;
|
||||||
|
HSPLandroidx/startup/a;-><clinit>()V
|
||||||
|
HSPLandroidx/startup/a;-><init>(Landroid/content/Context;)V
|
||||||
|
HSPLandroidx/startup/a;->a()V
|
||||||
|
HSPLandroidx/startup/a;->b(Landroid/os/Bundle;)V
|
||||||
|
HSPLandroidx/startup/a;->c(Ljava/lang/Class;)Ljava/lang/Object;
|
||||||
|
HSPLandroidx/startup/a;->d(Ljava/lang/Class;Ljava/util/Set;)Ljava/lang/Object;
|
||||||
|
HSPLandroidx/startup/a;->e(Landroid/content/Context;)Landroidx/startup/a;
|
||||||
|
HSPLandroidx/startup/a;->f(Ljava/lang/Class;)Ljava/lang/Object;
|
||||||
|
HSPLandroidx/startup/a;->g(Ljava/lang/Class;)Z
|
||||||
|
Landroidx/appcompat/widget/N;
|
||||||
|
HSPLandroidx/appcompat/widget/N;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
||||||
|
HSPLandroidx/appcompat/widget/N;->run()V
|
||||||
|
Landroidx/fragment/app/m;
|
||||||
|
Lw/a;
|
||||||
|
HSPLandroidx/fragment/app/m;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/n;
|
||||||
|
HSPLandroidx/fragment/app/n;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/o;
|
||||||
|
HSPLandroidx/fragment/app/o;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/fragment/app/p;
|
||||||
|
HSPLandroidx/fragment/app/p;-><init>(Landroidx/fragment/app/q;)V
|
||||||
|
Landroidx/lifecycle/s;
|
||||||
|
HSPLandroidx/lifecycle/s;-><init>(Landroidx/lifecycle/t;)V
|
||||||
|
Lh/c;
|
||||||
|
HSPLh/c;->a(Ljava/lang/Object;)V
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "me.humetrain.tr.twa",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": []
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,89 @@
|
||||||
|
# This is a configuration file for ProGuard.
|
||||||
|
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||||
|
#
|
||||||
|
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||||
|
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||||
|
# will be ignored by new version of the Android plugin for Gradle.
|
||||||
|
|
||||||
|
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
|
||||||
|
# instead of this one, which turns off the optimization flags.
|
||||||
|
-allowaccessmodification
|
||||||
|
|
||||||
|
# Preserve some attributes that may be required for reflection.
|
||||||
|
-keepattributes AnnotationDefault,
|
||||||
|
EnclosingMethod,
|
||||||
|
InnerClasses,
|
||||||
|
RuntimeVisibleAnnotations,
|
||||||
|
RuntimeVisibleParameterAnnotations,
|
||||||
|
RuntimeVisibleTypeAnnotations,
|
||||||
|
Signature
|
||||||
|
|
||||||
|
-keep public class com.google.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.android.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||||
|
|
||||||
|
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep setters in Views so that animations can still work.
|
||||||
|
-keepclassmembers public class * extends android.view.View {
|
||||||
|
void set*(***);
|
||||||
|
*** get*();
|
||||||
|
}
|
||||||
|
|
||||||
|
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||||
|
-keepclassmembers class * extends android.app.Activity {
|
||||||
|
public void *(android.view.View);
|
||||||
|
}
|
||||||
|
|
||||||
|
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve annotated Javascript interface methods.
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@android.webkit.JavascriptInterface <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# The support libraries contains references to newer platform versions.
|
||||||
|
# Don't warn about those in case this app is linking against an older
|
||||||
|
# platform version. We know about them, and they are safe.
|
||||||
|
-dontnote android.support.**
|
||||||
|
-dontnote androidx.**
|
||||||
|
-dontwarn android.support.**
|
||||||
|
-dontwarn androidx.**
|
||||||
|
|
||||||
|
# Understand the @Keep support annotation.
|
||||||
|
-keep class android.support.annotation.Keep
|
||||||
|
|
||||||
|
-keep @android.support.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <init>(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||||
|
-dontnote org.apache.http.**
|
||||||
|
-dontnote android.net.http.**
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||||
|
-dontnote java.lang.invoke.**
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
# This is a configuration file for ProGuard.
|
||||||
|
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||||
|
#
|
||||||
|
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||||
|
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||||
|
# will be ignored by new version of the Android plugin for Gradle.
|
||||||
|
|
||||||
|
# Optimization is turned off by default. Dex does not like code run
|
||||||
|
# through the ProGuard optimize steps (and performs some
|
||||||
|
# of these optimizations on its own).
|
||||||
|
# Note that if you want to enable optimization, you cannot just
|
||||||
|
# include optimization flags in your own project configuration file;
|
||||||
|
# instead you will need to point to the
|
||||||
|
# "proguard-android-optimize.txt" file instead of this one from your
|
||||||
|
# project.properties file.
|
||||||
|
-dontoptimize
|
||||||
|
|
||||||
|
# Preserve some attributes that may be required for reflection.
|
||||||
|
-keepattributes AnnotationDefault,
|
||||||
|
EnclosingMethod,
|
||||||
|
InnerClasses,
|
||||||
|
RuntimeVisibleAnnotations,
|
||||||
|
RuntimeVisibleParameterAnnotations,
|
||||||
|
RuntimeVisibleTypeAnnotations,
|
||||||
|
Signature
|
||||||
|
|
||||||
|
-keep public class com.google.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.android.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||||
|
|
||||||
|
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep setters in Views so that animations can still work.
|
||||||
|
-keepclassmembers public class * extends android.view.View {
|
||||||
|
void set*(***);
|
||||||
|
*** get*();
|
||||||
|
}
|
||||||
|
|
||||||
|
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||||
|
-keepclassmembers class * extends android.app.Activity {
|
||||||
|
public void *(android.view.View);
|
||||||
|
}
|
||||||
|
|
||||||
|
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve annotated Javascript interface methods.
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@android.webkit.JavascriptInterface <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# The support libraries contains references to newer platform versions.
|
||||||
|
# Don't warn about those in case this app is linking against an older
|
||||||
|
# platform version. We know about them, and they are safe.
|
||||||
|
-dontnote android.support.**
|
||||||
|
-dontnote androidx.**
|
||||||
|
-dontwarn android.support.**
|
||||||
|
-dontwarn androidx.**
|
||||||
|
|
||||||
|
# Understand the @Keep support annotation.
|
||||||
|
-keep class android.support.annotation.Keep
|
||||||
|
|
||||||
|
-keep @android.support.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <init>(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||||
|
-dontnote org.apache.http.**
|
||||||
|
-dontnote android.net.http.**
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||||
|
-dontnote java.lang.invoke.**
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
# This is a configuration file for ProGuard.
|
||||||
|
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||||
|
#
|
||||||
|
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
||||||
|
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
||||||
|
# will be ignored by new version of the Android plugin for Gradle.
|
||||||
|
|
||||||
|
# Optimizations can be turned on and off in the 'postProcessing' DSL block.
|
||||||
|
# The configuration below is applied if optimizations are enabled.
|
||||||
|
-allowaccessmodification
|
||||||
|
|
||||||
|
# Preserve some attributes that may be required for reflection.
|
||||||
|
-keepattributes AnnotationDefault,
|
||||||
|
EnclosingMethod,
|
||||||
|
InnerClasses,
|
||||||
|
RuntimeVisibleAnnotations,
|
||||||
|
RuntimeVisibleParameterAnnotations,
|
||||||
|
RuntimeVisibleTypeAnnotations,
|
||||||
|
Signature
|
||||||
|
|
||||||
|
-keep public class com.google.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.android.vending.licensing.ILicensingService
|
||||||
|
-keep public class com.google.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.android.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.vending.licensing.ILicensingService
|
||||||
|
-dontnote com.google.android.vending.licensing.ILicensingService
|
||||||
|
|
||||||
|
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep setters in Views so that animations can still work.
|
||||||
|
-keepclassmembers public class * extends android.view.View {
|
||||||
|
void set*(***);
|
||||||
|
*** get*();
|
||||||
|
}
|
||||||
|
|
||||||
|
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
||||||
|
-keepclassmembers class * extends android.app.Activity {
|
||||||
|
public void *(android.view.View);
|
||||||
|
}
|
||||||
|
|
||||||
|
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve annotated Javascript interface methods.
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@android.webkit.JavascriptInterface <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# The support libraries contains references to newer platform versions.
|
||||||
|
# Don't warn about those in case this app is linking against an older
|
||||||
|
# platform version. We know about them, and they are safe.
|
||||||
|
-dontnote android.support.**
|
||||||
|
-dontnote androidx.**
|
||||||
|
-dontwarn android.support.**
|
||||||
|
-dontwarn androidx.**
|
||||||
|
|
||||||
|
# Understand the @Keep support annotation.
|
||||||
|
-keep class android.support.annotation.Keep
|
||||||
|
|
||||||
|
-keep @android.support.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@android.support.annotation.Keep <init>(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||||
|
-dontnote org.apache.http.**
|
||||||
|
-dontnote android.net.http.**
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||||
|
-dontnote java.lang.invoke.**
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
||||||
|
31=0/.dm
|
||||||
|
2147483647=0/.dm
|
||||||
|
28=1/.dm
|
||||||
|
29=1/.dm
|
||||||
|
30=1/.dm
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/main/assets"/></dataSet><dataSet config="release" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/release/assets"/></dataSet><dataSet config="generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/build/intermediates/shader_assets/release/compileReleaseShaders/out"/></dataSet></merger>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/main/jniLibs"/></dataSet><dataSet config="release" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/release/jniLibs"/></dataSet></merger>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/main/shaders"/></dataSet><dataSet config="release" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/home/ckh08045/Tr_Code/app/src/release/shaders"/></dataSet></merger>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#Tue Oct 21 01:44:18 KST 2025
|
||||||
|
base.0=/home/ckh08045/Tr_Code/app/build/intermediates/dex/release/minifyReleaseWithR8/classes.dex
|
||||||
|
renamed.0=classes.dex
|
||||||
|
path.0=classes.dex
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue