# MUFROLIC 정산 포털 운영 매뉴얼

> 최종 업데이트: 2026-04-26
> Production URL: https://settlement-portal.onrender.com

---

## 1. 시스템 개요

### 1.1 목적
권리자(아티스트·작곡가·배급사 등)에게 정산 내역을 안전하게 제공하고, 관리자가 정산 데이터·계정을 관리하는 웹 포털.

### 1.2 구성
| 계층 | 기술 | 위치 |
|---|---|---|
| Frontend | Vanilla JS + HTML/CSS | `static/index.html`, `static/app.js` |
| Backend | Flask + Gunicorn | `app.py` |
| 사용자/로그 저장 | Google Sheets | (USERS_SHEET_ID, SHEETS_ID) |
| 정산 CSV 저장 | Google Drive | (DRIVE_FOLDER_ID 폴더) |
| 호스팅 | Render (Starter 플랜) | settlement-portal.onrender.com |
| 소스 | GitHub (private) | jaymnp/settlement-portal |

### 1.3 외부 의존
- Google Cloud Service Account (Sheets·Drive API)
- Cloudflare (DNS·CDN)
- Render Dashboard (배포·환경변수)

---

## 2. 계정 체계

| 유형 | 저장 위치 | 권한 | TOTP |
|---|---|---|---|
| **마스터** | Render env var | 모든 admin 작업 | env var로 강제 가능 |
| **Admin** | Users 시트 (role=admin) | 모든 admin 작업 | 첫 로그인 시 자동 셋업 |
| **권리자** | Users 시트 (role=user) | 본인 정산 조회만 | 미적용 |

### 권한 차이
- **마스터/Admin**: CSV 업로드·상태변경·삭제, 권리자 추가, 비밀번호 재발급, TOTP 초기화, 로그 조회
- **권리자**: 본인 `rights_id` 로 시작하는 정산코드의 데이터만 조회·CSV 다운로드

---

## 3. 마스터 계정 운영

### 3.1 로그인
1. https://settlement-portal.onrender.com 접속
2. 아이디 / 비밀번호 입력 → **로그인**
3. (TOTP 활성 시) 6자리 코드 입력 → **확인**
4. 관리자 페이지 진입

### 3.2 TOTP 최초 활성화
> 시트 admin과 달리 마스터는 웹에서 TOTP 셋업 불가. 로컬 CLI 사용.

**1) 로컬에서 시크릿 발급:**
```bash
cd /Users/jaym/Desktop/settlement-portal
python3 tools/setup_master_totp.py
```
출력에 ASCII QR과 `MASTER_TOTP_SECRET=...` 표시.

**2) Authenticator 앱으로 QR 스캔** (Google·Microsoft·1Password 등)

**3) Render 환경변수 등록:**
- Render Dashboard → 서비스 → **Environment**
- **Add Environment Variable**
  - Key: `MASTER_TOTP_SECRET`
  - Value: 출력된 secret 값
- **Save Changes** → 자동 재배포

**4) 배포 완료 후 로그인 → 비밀번호 + 6자리 코드**

### 3.3 마스터 비밀번호 변경
1. Render → Environment → `MASTER_PW` 항목 우측 ✏️ 클릭
2. 새 값 입력 → 저장
3. **수동 재배포 필요** (§8.1 참조)

### 3.4 마스터 TOTP 시크릿 갱신
1. `python3 tools/setup_master_totp.py` 다시 실행 → 새 secret 발급
2. 새 QR을 Authenticator로 스캔 (기존 항목과 별도로 추가)
3. Render → Environment → `MASTER_TOTP_SECRET` 값 새로 입력 → 저장
4. 수동 재배포

### 3.5 마스터 분실 / 복구

| 상황 | 복구 |
|---|---|
| 비번만 잊음 | Render에서 `MASTER_PW` 새로 설정 |
| Authenticator 분실 | Render에서 `MASTER_TOTP_SECRET` **삭제** → 비번만으로 로그인 → 재셋업 |
| 비번+TOTP 둘 다 분실 | 둘 다 처리 (위 두 경우 합치기) |
| Render 계정 자체 분실 | Render 2FA recovery code 사용. 그것도 없으면 Render 지원팀 (`support@render.com`)에 신원 확인 요청 |

> **중요:** Render 2FA를 켤 때 발급받은 recovery codes를 **반드시 안전한 곳에 보관**하세요. Render 계정이 진짜 single point of failure입니다.

---

## 4. Admin 계정 운영

### 4.1 Admin 추가
1. 마스터로 로그인 → 관리자 페이지
2. **권리자 계정 추가** 카드
3. 유형 드롭다운에서 **ADMIN** 선택
4. 아이디·이름 입력 → **계정 추가**
5. 모달에 표시되는 **초기 비밀번호**를 안전한 곳에 복사
   ※ 모달 닫으면 다시 볼 수 없음. 시트엔 해시만 저장됨.

### 4.2 Admin 첫 로그인 (TOTP 등록)
1. 받은 아이디·비밀번호로 로그인
2. 자동으로 QR 코드 화면이 뜸
3. Authenticator 앱으로 스캔 → 6자리 코드 입력 → **확인**
4. 시트의 `totp_secret` 컬럼에 자동 저장
5. 다음 로그인부턴 비번 + 6자리

### 4.3 비밀번호 재발급
> Admin이 비번을 잊었을 때 다른 admin이 처리.

1. 관리자 페이지 → **계정 목록**
2. 해당 행의 **비번재발급** 클릭
3. 확인 알림 → 모달에 새 비밀번호 표시
4. 사용자에게 안전하게 전달 → 모달 닫음

### 4.4 TOTP 초기화
> Admin이 Authenticator 분실 시.

1. 관리자 페이지 → 계정 목록
2. 해당 admin 행의 **TOTP초기화** 클릭
3. 확인 → 시트의 `totp_secret` 비워짐
4. 다음 로그인 시 새 QR로 재등록

### 4.5 Admin 삭제
> 웹 UI에서 삭제 미지원. 시트에서 직접 행 제거.

1. 관리자 페이지 → **📊 시트 열기** (Users 시트)
2. 해당 admin 행 우클릭 → **행 삭제**

---

## 5. 권리자 계정 운영

### 5.1 권리자 추가
1. 마스터/Admin으로 로그인
2. **권리자 계정 추가** 카드
3. 유형: **권리자** (기본값)
4. 아이디·이름·**정산 ID** 입력
   - 정산 ID는 정산코드 prefix와 일치해야 함 (예: `M001`)
5. **계정 추가** → 모달의 초기 비밀번호 사용자에게 전달

### 5.2 비밀번호 재발급
관리자 페이지 → 계정 목록 → 해당 행 **비번재발급** → 모달 표시.

### 5.3 정산 ID 매칭 규칙
권리자는 본인 `rights_id`로 시작하는 `정산코드` 데이터만 볼 수 있음.

| rights_id | 보이는 정산코드 |
|---|---|
| `M001` | `M001P25A`, `M001P26A` … |
| `M002` | `M002P25A`, `M002P26S` … |

---

## 6. 정산 데이터 관리

### 6.1 CSV 헤더 규격
**필수 10개 컬럼 (이 외 헤더가 있으면 업로드 거부):**
```
정산코드, 정산월, 앨범명(국내), 곡명, 아티스트, 국가, 플랫폼, 발생금액, 유통수수료, 정산금액
```

| 컬럼 | 형식 | 예시 |
|---|---|---|
| 정산코드 | 8자 고정: `{rights_id}P{nn}{category}` (M+3자리+P+2자리+카테고리) | `M001P25A` |
| 정산월 | `YYMM` 또는 `YYYY-MM` | `2501` 또는 `2025-01` |
| 발생금액·정산금액 | 숫자 (콤마 허용) | `624,453` |
| 유통수수료 | 숫자 또는 `XX%` | `30%` |

**카테고리 (정산코드 마지막 글자):**
| 코드 | 카테고리 |
|---|---|
| A | 음원 |
| S | 악보 |
| C | 저작권 |
| M | 굿즈 |

샘플 CSV는 관리자 페이지의 **📄 샘플 CSV** 버튼으로 다운로드 가능.

### 6.2 업로드 절차
1. 관리자 페이지 → **정산 데이터 업로드** 카드
2. CSV 파일을 영역에 드래그앤드롭 (또는 클릭하여 선택)
3. **정산 상태**: `정산 완료` 또는 `정산 예정` 선택
4. **업로드** 클릭 → Drive 폴더에 자동 저장

### 6.3 정산 상태 변경
1. 관리자 페이지 → **업로드된 정산 파일** 목록
2. 해당 파일 우측 드롭다운에서 `정산 완료` ⇄ `정산 예정` 변경
3. 즉시 반영 (60초 캐시 무효화 자동 처리)

### 6.4 파일 삭제
> 웹 UI에서 직접 삭제 미지원 (실수 방지). Drive에서 직접 처리.

1. 관리자 페이지 → **📂 Drive 폴더** 클릭
2. Drive 화면에서 해당 파일 우클릭 → **삭제**
3. 시스템에 즉시 반영되려면 다음 60초 후 자동 새로고침

### 6.5 데이터 캐싱
- 통합 정산 데이터는 메모리에 60초 동안 캐시됨
- 업로드/상태변경 시 자동 무효화
- 단순 페이지 이동·필터 적용은 캐시 활용 → 빠름

---

## 7. 모니터링

### 7.1 접속 로그
- 관리자 페이지 → **최근 접속 로그** (최근 20건)
- 전체 로그: **📊 시트 열기** 버튼 → 월별 시트 탭

기록되는 이벤트:
- 로그인·로그아웃
- 로그인 실패·IP 잠금
- TOTP 실패·재사용 시도
- CSV 업로드/상태변경/다운로드
- 비밀번호 재발급·TOTP 초기화
- 세션 만료

### 7.2 Drive 폴더 직접 접근
관리자 페이지 → **📂 Drive 폴더** 버튼 → 정산 CSV 원본 폴더로 이동.

### 7.3 Users 시트 직접 접근
관리자 페이지 → **📊 시트 열기** 버튼 → 사용자 목록 시트.
- `password_hash`만 채워져 있어야 정상 (평문 `password` 컬럼은 비어있어야 함).

---

## 8. 운영 절차

### 8.1 코드 변경 후 배포 (수동 배포)
> Auto-deploy가 **꺼져 있음**. main에 push해도 자동 배포되지 않음.

**절차:**
1. 로컬에서 코드 수정 → 커밋 → push
   ```bash
   git add -A
   git commit -m "변경 내용"
   git push
   ```
2. Render Dashboard → 서비스 클릭
3. 우측 상단 **Manual Deploy** → **Deploy latest commit**
4. 1~2분 대기. **Logs** 탭에 `==> Your service is live 🎉` 뜨면 완료
5. 브라우저에서 `Cmd+Shift+R` 로 캐시 무시 새로고침

**배포 안 된 변경 확인:**
- Render Dashboard → **Deploys** 탭
- 최상단 deploy의 commit hash가 `git log` 의 최신 커밋과 일치하는지 확인

### 8.2 환경변수 변경
1. Render → 서비스 → **Environment**
2. 항목 추가/수정/삭제
3. **Save Changes** → **자동으로 재배포 됨** (Auto-deploy 옵션과 별개)

**주요 환경변수:**
| 키 | 용도 | 비공개 |
|---|---|---|
| `SECRET_KEY` | Flask 세션 서명 | ✅ |
| `MASTER_USER` | 마스터 계정 ID | (반공개) |
| `MASTER_PW` | 마스터 비밀번호 | ✅ |
| `MASTER_TOTP_SECRET` | 마스터 TOTP 시크릿 | ✅ |
| `GOOGLE_CREDENTIALS` | service account JSON | ✅ (가장 민감) |
| `GOOGLE_SHEETS_ID` | 로그 시트 ID | (반공개) |
| `USERS_SHEET_ID` | 사용자 시트 ID | (반공개) |
| `DRIVE_FOLDER_ID` | 정산 CSV 폴더 ID | (반공개) |

### 8.3 의존성 업그레이드
1. `requirements.txt` 의 정확 버전(`==`) 수정
2. 로컬에서 테스트
3. push → 수동 배포

### 8.4 백업
**시스템:**
- Users 시트, 로그 시트, Drive 정산 폴더 → Google Workspace 자체 보관

**권장 추가 백업:**
- https://takeout.google.com → Drive 선택 → **2개월마다** 자동 export
- 받은 ZIP을 별도 저장소에 보관

**복구:**
- 시트가 손상되면 Google Sheets의 **버전 기록** (파일 → 버전 기록) 으로 복원
- Drive 파일은 휴지통에서 30일 내 복원 가능

---

## 9. 트러블슈팅

### 9.1 마스터 로그인 안 됨
| 증상 | 원인 / 조치 |
|---|---|
| "비밀번호 불일치" | `MASTER_PW` 값 확인. Render env var 비밀번호와 입력 비밀번호 일치 여부 |
| TOTP 6자리 입력 안 받음 | `MASTER_TOTP_SECRET` 값과 Authenticator의 시크릿 동기화 안 됨. §3.4로 갱신 |
| `[Master] disabled` 로그 | env var 미설정. Render에서 `MASTER_USER`/`MASTER_PW` 등록 |
| 응답이 `"서버 오류가 발생했습니다."` | Render Logs에서 stack trace 확인 |

### 9.2 Prod 500 에러 (모든 API)
1. Render → Logs → 최근 5분 이내 `Traceback` 검색
2. 가장 흔한 원인:
   - Google API 인증 실패 → `GOOGLE_CREDENTIALS` env var 확인
   - Sheet 또는 Drive 권한 박탈 → service account 공유 권한 재확인
   - pandas 버전 변경에 따른 호환성 → `requirements.txt` 핀 확인

### 9.3 화면이 옛날 데이터 / undefined 표시
- `Cmd+Shift+R` (Mac) 또는 `Ctrl+Shift+R` (Win) 강제 새로고침
- 그래도 같으면 → 개발자 도구 → Network 탭에서 `/api/summary` 응답 확인 → Render Logs

### 9.4 Admin TOTP 분실 (다른 admin이 있는 경우)
다른 admin이 §4.4 절차로 초기화.

### 9.5 Admin TOTP 분실 (단일 admin 인 경우)
마스터로 로그인 → §4.4 절차로 해당 admin TOTP 초기화.

### 9.6 마스터 + 모든 admin TOTP 분실
1. Render → Environment → `MASTER_TOTP_SECRET` 삭제 → 저장
2. 마스터 비번만으로 로그인
3. §4.4로 admin들 TOTP 초기화 OR §3.2로 마스터 TOTP 재셋업

### 9.7 SSH push 실패
**증상:** `git push` 시 `Permission denied (publickey)`

**조치:**
```bash
ssh -T git@github.com
# "Hi jaymnp!" 메시지 떠야 정상
```
안 뜨면 SSH 키 등록 상태 확인 → https://github.com/settings/ssh

### 9.8 데이터가 즉시 반영 안 됨
- 60초 캐시 때문 → 1분 정도 기다리거나 admin이 한 번 더 업로드/상태변경 트리거

---

## 10. 보안 운영

### 10.1 정기 점검 (분기별 권장)
- [ ] Users 시트의 `password` 컬럼이 모두 비어있는지 (해시 마이그레이션 완료 확인)
- [ ] Render env var 의 `MASTER_PW` 마지막 변경일
- [ ] Render env var 의 `MASTER_TOTP_SECRET` 마지막 변경일
- [ ] GitHub Settings → Tokens 의 만료/회수 상태
- [ ] Google Cloud → service account 키 개수 (1개 권장)
- [ ] Render `Deploys` 탭에서 최근 배포 모두 정상 완료 확인
- [ ] Dependabot alerts 처리

### 10.2 비밀번호·시크릿 갱신 주기 (권장)
| 항목 | 주기 |
|---|---|
| `MASTER_PW` | 6개월 |
| `MASTER_TOTP_SECRET` | 분실/공유 의심 시 즉시 |
| GitHub PAT | 90일 만료 (자동) |
| Google Service Account 키 | 1년 |
| 권리자 비밀번호 | 분실 시 |

### 10.3 의심 활동 감지
**신호:**
- 접속 로그에 짧은 시간 내 다수 IP에서 동일 user_id 시도
- TOTP 실패 폭주
- 모르는 IP에서 admin 작업
- Sheet/Drive 외부 공유 알림

**대응:**
1. 해당 user_id 의 비밀번호 즉시 재발급 (§4.3·5.2)
2. TOTP 초기화 (§4.4)
3. 마스터 비밀번호 갱신 (§3.3)
4. Render·GitHub 로그 검토

---

## 부록 A — 환경변수 전체 목록

### 필수
- `SECRET_KEY` — Flask 세션 서명 (32+ 문자 랜덤)

### 인증
- `MASTER_USER` — 마스터 계정 아이디
- `MASTER_PW` — 마스터 비밀번호 (평문, env var)
- `MASTER_TOTP_SECRET` — 마스터 TOTP Base32 시크릿 (선택, 강력 권장)
- `TOTP_ISSUER` — Authenticator 앱 표시 라벨 (기본: "MUFROLIC 정산")

### Google 연동
- `GOOGLE_CREDENTIALS` — service account JSON 전체 (또는 `/etc/secrets/credentials.json`)
- `GOOGLE_SHEETS_ID` — 로그 기록용 시트
- `USERS_SHEET_ID` — 사용자 계정 시트
- `USERS_SHEET_TAB` — 사용자 시트의 탭 이름 (기본: "Users")
- `DRIVE_FOLDER_ID` — 정산 CSV 저장 폴더

### 접근 제어 (선택)
- `ALLOWED_IPS` — 허용 IP 화이트리스트 (콤마 구분, 빈 값이면 무제한)

---

## 부록 B — API 엔드포인트

### 인증
| 메서드 | 경로 | 권한 |
|---|---|---|
| POST | /api/login | 누구나 |
| POST | /api/login/totp | pending 세션 |
| POST | /api/logout | 로그인 |
| GET | /api/me | 로그인 |

### 권리자/사용자
| 메서드 | 경로 | 용도 |
|---|---|---|
| GET | /api/summary | KPI 요약 |
| GET | /api/charts | 차트 데이터 (플랫폼·국가·곡·월별) |
| GET | /api/settlements | 정산 내역 (페이지네이션) |
| GET | /api/filter-options | 필터 드롭다운 옵션 |
| GET | /api/settlements/export | CSV 다운로드 |

### Admin
| 메서드 | 경로 | 용도 |
|---|---|---|
| POST | /api/admin/upload | CSV 업로드 |
| GET | /api/admin/files | 업로드 파일 목록 |
| POST | /api/admin/files/`<id>`/status | 정산 상태 변경 |
| DELETE | /api/admin/files/`<id>` | 파일 삭제 (휴지통) |
| GET | /api/admin/users | 사용자 목록 |
| POST | /api/admin/users | 사용자 추가 |
| POST | /api/admin/users/`<uid>`/reset-password | 비밀번호 재발급 |
| POST | /api/admin/users/`<uid>`/reset-totp | TOTP 초기화 |
| GET | /api/admin/logs | 접속 로그 |
| GET | /api/admin/sheet-urls | 시트·Drive URL 묶음 |
| GET | /api/admin/sample-csv | 샘플 CSV |

---

## 부록 C — 파일/폴더 구조

```
settlement-portal/
├── app.py                 ← Flask 백엔드 (메인)
├── requirements.txt       ← 패키지 (정확 버전 핀)
├── README.md              ← 설치 가이드
├── SECURITY_REPORT.md     ← 보안 점검 보고서
├── data/
│   ├── settlements.csv    ← (구) 데이터, 현재는 Drive 사용
│   └── credentials.json   ← (.gitignore — 실제 service account JSON, 로컬 개발용)
├── static/
│   ├── index.html         ← 프론트 HTML/CSS
│   ├── app.js             ← 프론트 JS (외부 분리)
│   └── MANUAL.md          ← 본 매뉴얼
└── tools/
    ├── convert_csv.py     ← 원본 → 시스템 포맷 변환
    └── setup_master_totp.py  ← 마스터 TOTP 발급 CLI
```

---

## 부록 D — 의존성

```
flask==3.1.0
pandas==2.2.3
gunicorn==23.0.0
gspread==6.1.4
google-auth==2.36.0
google-api-python-client==2.151.0
pyotp==2.9.0
qrcode==8.2
```

업그레이드 시 §8.3 절차.

---

## 부록 E — 외부 서비스 의존

| 서비스 | 역할 | 장애 시 영향 |
|---|---|---|
| GitHub | 소스코드 | 신규 배포 불가 (기존 prod은 정상) |
| Render | 호스팅 | 전체 서비스 중단 |
| Google Sheets | 사용자/로그 | 로그인 불가 + 신규 로그 미기록 |
| Google Drive | 정산 CSV | 모든 정산 조회 실패 |
| Cloudflare | DNS·CDN | 도메인 접근 불가 (직접 onrender.com 도메인 사용 가능) |

---

## 변경 이력

| 일자 | 내용 |
|---|---|
| 2026-04-26 | 보안 점검 (TOTP·해싱·CSP·SSH·CF IP·deps 핀·자동 배포 게이트) |
| 2026-04-26 | 매뉴얼 작성 |
