Compare commits

...

7 Commits

Author SHA1 Message Date
Cursor Agent 6a8833751c
Merge branch 'cursor/docs-w4-status-sync-382d' — docs W4 status sync
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 06:40:18 +00:00
Cursor Agent cd367a5715
docs: sync W4/web status — secrets live, accept remaining
Align README, roadmap, web-upgrade, fcm-setup, tester-guide, mobile
README, and deploy-checklist with production :web: injection. Mark N4-3
server SA done; keep W4 accept (browser push-test) open.

Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 06:40:14 +00:00
Cursor Agent 57fa4dca3c
Merge branch 'cursor/session-local-time-382d' — local session expiry display
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 05:18:17 +00:00
Cursor Agent c28c6ca2e8
fix(ui): show session expiry in device local time
Parse API UTC expires_at and render yyyy-MM-dd HH:mm via DateTime.toLocal()
instead of raw RFC3339 Z strings on the sessions screen.

Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 05:18:13 +00:00
Cursor Agent ed8cbef8fe
Merge branch 'cursor/w4-web-appid-live-382d' — W4 web APP_ID docs
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 05:01:55 +00:00
Cursor Agent e595703c6c
docs: W4 web APP_ID live; browser token accept remaining
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 05:01:51 +00:00
Cursor Agent 2b46db63ca
Merge branch 'cursor/w4-firebase-live-382d' — W4 live inject hardening
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
2026-08-04 04:53:48 +00:00
9 changed files with 113 additions and 82 deletions

View File

@ -73,8 +73,9 @@
- Phase 1 **A~C** + Track A/B/C + **Q8**(로그인·설정·로그아웃) 코드 완료 - Phase 1 **A~C** + Track A/B/C + **Q8**(로그인·설정·로그아웃) 코드 완료
- 라이브: [`https://msn.iykyka.com`](https://msn.iykyka.com) · 데모 코드 **`DEMO-YKAVU`** - 라이브: [`https://msn.iykyka.com`](https://msn.iykyka.com) · 데모 코드 **`DEMO-YKAVU`**
- **진행 중 (설계 완료·구현 Wi 승인 대기):** 웹 고도화 [`docs/web-upgrade.md`](./docs/web-upgrade.md) (W0 경계 done · W1~W7 todo) - **웹 고도화 (W0~W7):** 구현·배포 완료 — [`docs/web-upgrade.md`](./docs/web-upgrade.md).
- **후순위 (Master):** Android N4 — `google-services.json` · 실기기 FCM · UI 탭 · APK · `SHARE_URL` **남은 것:** W4 **accept** (하드 리프레시·알림 허용·로그인 후 `/admin/push-test``sent >= 1`)
- **후순위 (Master):** Android N4 — `google-services.json` PC 배치 · 실기기 FCM · UI 탭 · APK · `SHARE_URL`
- **결정 대기:** Q9 — L2 의미 (카피 정렬 ± 수신 자동응대) — 웹 트랙에도 **비범위** - **결정 대기:** Q9 — L2 의미 (카피 정렬 ± 수신 자동응대) — 웹 트랙에도 **비범위**
- **맨 마지막:** N5 / D — 사람 PoC. §3 기본값 추측 금지 - **맨 마지막:** N5 / D — 사람 PoC. §3 기본값 추측 금지

View File

@ -37,8 +37,8 @@ Phase 1 **A~C** 이후 실행 트랙. 작업 단위를 하나씩 처리한다.
- 앱 코드는 클로즈드 베타 직전 수준 - 앱 코드는 클로즈드 베타 직전 수준
- **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료** - **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료**
- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W3·W5~W7 **done** · **W4 live injected** (SW populated · accept: Web `:web:` APP_ID + push-test) - **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W7 구현·배포 **done** · **W4 accept만** (하드 리프레시·알림·push-test)
- 후순위: **N4 Android FCM/UI**Master 시크릿·실기기 - 후순위: **N4 Android FCM/UI**`google-services.json` PC 배치·실기기 (서버 SA는 배치됨)
- UI: **iMessage-inspired light default** + soft charcoal dark 프로덕션 반영 - UI: **iMessage-inspired light default** + soft charcoal dark 프로덕션 반영
- **Track C 콘텐츠 갭 A~F**: 프로덕션 반영 완료 (`95422bb`대) - **Track C 콘텐츠 갭 A~F**: 프로덕션 반영 완료 (`95422bb`대)
- **Q8 계정·설정 IA (2026-08-03 확정·구현):** 로그인 탭 · 설정 허브 · 로그아웃 - **Q8 계정·설정 IA (2026-08-03 확정·구현):** 로그인 탭 · 설정 허브 · 로그아웃
@ -217,7 +217,7 @@ N2-A 전체 확정. 다음 구현 트랙은 **N1 스모크 → N2-B (Dockerfile/
| **N4-W1** | 웹 로컬 지속성 (prefs/idb) | **done** (2026-08-04) | `app_database_web.dart` SharedPreferences · Drift WASM 비채택 · 라이브 하드 리프레시 스모크는 배포 후 | | **N4-W1** | 웹 로컬 지속성 (prefs/idb) | **done** (2026-08-04) | `app_database_web.dart` SharedPreferences · Drift WASM 비채택 · 라이브 하드 리프레시 스모크는 배포 후 |
| **N4-W2** | 데모 메신저 UX | **done** (2026-08-04) | ≥900px 분할·WS 상태·ID 복사·웹 안내 배너 | | **N4-W2** | 데모 메신저 UX | **done** (2026-08-04) | ≥900px 분할·WS 상태·ID 복사·웹 안내 배너 |
| **N4-W3** | 인앱·Notification API | **done** (2026-08-04) | 웹 Timer+Notification · 포커스 past-due · 설정 권한 | | **N4-W3** | 인앱·Notification API | **done** (2026-08-04) | 웹 Timer+Notification · 포커스 past-due · 설정 권한 |
| **N4-W4** | Web Push (FCM + VAPID) | **live-injected** (2026-08-04) | 서버 `.env` 동기·SW populated · **accept:** APP_ID를 `:web:`로 교체 후 push-test | | **N4-W4** | Web Push (FCM + VAPID) | **web-live** (2026-08-04) | 시크릿·`:web:`·SW 배포 done · **accept만:** 하드 리프레시·알림·실 토큰·push-test |
| **N4-W5** | PWA | **done** (2026-08-04) | manifest 테마 · nginx no-cache · OfflineBanner | | **N4-W5** | PWA | **done** (2026-08-04) | manifest 테마 · nginx no-cache · OfflineBanner |
| **N4-W6** | 테스터 가이드 | **done** (2026-08-04) | `tester-guide.md` 웹 절 | | **N4-W6** | 테스터 가이드 | **done** (2026-08-04) | `tester-guide.md` 웹 절 |
| **N4-W7** | 품질 | **done** (2026-08-04) | `scripts/e2e_web_smoke.sh` + 부록 QA (Playwright는 후속) | | **N4-W7** | 품질 | **done** (2026-08-04) | `scripts/e2e_web_smoke.sh` + 부록 QA (Playwright는 후속) |
@ -228,7 +228,7 @@ N2-A 전체 확정. 다음 구현 트랙은 **N1 스모크 → N2-B (Dockerfile/
|----|------|--------|-----------| |----|------|--------|-----------|
| **N4-1** | Firebase + `google-services.json` | doing | Master: 앱 등록됨 · JSON을 Android 빌드 PC에 배치 | | **N4-1** | Firebase + `google-services.json` | doing | Master: 앱 등록됨 · JSON을 Android 빌드 PC에 배치 |
| **N4-2** | 실 FCM registration token | done* | `PushTokenService` — Firebase 있으면 실 토큰, 없으면 `install:` (*전송은 N4-1 후) | | **N4-2** | 실 FCM registration token | done* | `PushTokenService` — Firebase 있으면 실 토큰, 없으면 `install:` (*전송은 N4-1 후) |
| **N4-3** | 서버 FCM 자격증명 (HTTP v1) | doing | Master: `secrets/firebase-service-account.json` (레거시 서버 키 대신). 웹 W4와 공유 | | **N4-3** | 서버 FCM 자격증명 (HTTP v1) | **done** (2026-08) | 프로덕션 `secrets/firebase-service-account.json` 마운트됨. 웹 W4와 공유 |
| **N4-4** | 푸시 수신 (Android) | blocked | N4-1+N4-3 후 `/admin/push-test` + 기기 수신 | | **N4-4** | 푸시 수신 (Android) | blocked | N4-1+N4-3 후 `/admin/push-test` + 기기 수신 |
### Android UI 탭 (`mobile/README.md`) ### Android UI 탭 (`mobile/README.md`)
@ -290,8 +290,8 @@ N1~N4(배포·품질에 필요한 최소분) 이후에만 착수. `roadmap.md` P
확인 필요, 그때까지는 인앱 배지·배너가 실질적 대체 경로. Track C A~F 프로덕션 확인 필요, 그때까지는 인앱 배지·배너가 실질적 대체 경로. Track C A~F 프로덕션
재배포는 **완료** (`95422bb` / docs `f363e8e`, 2026-08-03). 재배포는 **완료** (`95422bb` / docs `f363e8e`, 2026-08-03).
**바로 다음:** 웹 **W4 accept**Firebase Console에서 **Web** 앱(`:web:` APP_ID) 확인·`.env` 교체·`web` 재빌드·하드 리프레시·`/admin/push-test` · 또는 Android N4-1/3. **바로 다음:** 웹 **W4 accept**`msn.iykyka.com` 하드 리프레시·알림 허용·로그인 후 `/admin/push-test` · 또는 Android N4-1/4.
웹 W0~W7 코드·시크릿 주입 **done** (W4 browser token accept만 남음). 웹 W0~W7 코드·`:web:` 시크릿 배포·서버 SA **done** (브라우저 실토큰·push-test만 남음).
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다. 완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.

View File

@ -3,14 +3,15 @@
서버·클라이언트의 푸시 **코드 경로는 준비됨**. 서버·클라이언트의 푸시 **코드 경로는 준비됨**.
새 Firebase 프로젝트는 **레거시 서버 키가 비활성**인 경우가 많아, **HTTP v1 + 서비스 계정 JSON**을 쓴다. 새 Firebase 프로젝트는 **레거시 서버 키가 비활성**인 경우가 많아, **HTTP v1 + 서비스 계정 JSON**을 쓴다.
## 이미 된 것 (코드) ## 이미 된 것 (코드 · 프로덕션)
| 계층 | 동작 | | 계층 | 동작 |
|------|------| |------|------|
| Flutter | `PushTokenService` — Firebase 가능하면 실 FCM 토큰, 아니면 `install:` 플레이스홀더 | | Flutter | `PushTokenService` — Firebase 가능하면 실 FCM 토큰, 아니면 `install:` 플레이스홀더 |
| Android | `google-services.json`이 있을 때만 Google Services 플러그인 적용 | | Android | `google-services.json`이 있을 때만 Google Services 플러그인 적용 |
| Web (N4-W4) | `FirebaseWebConfig` + `FIREBASE_*`/`VAPID` dart-define · SW template · 미설정 시 `install:` | | Web (N4-W4) | `FirebaseWebConfig` + 프로덕션 `.env` `FIREBASE_*`/`VAPID` 주입 · SW populated (`:web:` APP_ID) |
| core-backend | `notifyUser` + `POST /admin/push-test`**FCM HTTP v1**(서비스 계정) 우선, 레거시 `FCM_SERVER_KEY`는 폴백 | | core-backend | `notifyUser` + `POST /admin/push-test`**FCM HTTP v1**(서비스 계정) 우선 |
| 서버 SA (N4-3) | 프로덕션 `secrets/firebase-service-account.json` 마운트 **done** |
## Master가 할 일 ## Master가 할 일
@ -18,44 +19,35 @@
1. [Firebase Console](https://console.firebase.google.com/) → Android 앱 추가 1. [Firebase Console](https://console.firebase.google.com/) → Android 앱 추가
package: **`com.ykavu.ykavu_mobile`** package: **`com.ykavu.ykavu_mobile`**
2. `google-services.json`로컬에만 배치 (git 금지): 2. `google-services.json`**Android 빌드 PC**에만 배치 (git 금지):
```bash ```bash
cp ~/Downloads/google-services.json mobile/android/app/google-services.json cp ~/Downloads/google-services.json mobile/android/app/google-services.json
``` ```
### N4-3 — 서비스 계정 JSON (HTTP v1) ### N4-3 — 서비스 계정 JSON (HTTP v1) — **프로덕션 done**
레거시 **서버 키**가 Cloud Messaging 탭에서 `사용 중지됨`이면 정상이다. 아래를 쓴다. 레거시 **서버 키**가 Cloud Messaging 탭에서 `사용 중지됨`이면 정상이다.
1. Google Cloud → 사용자 인증 정보 → 서비스 계정 만들기 프로덕션 호스트에 이미 배치됨:
(API: Firebase Cloud Messaging API, 데이터: **애플리케이션 데이터**)
2. 역할: **Firebase Cloud Messaging Admin** (없으면 Firebase 관리자 / 임시 소유자) ```bash
3. 키 유형 **JSON** 다운로드 # ~/project/ykavu/secrets/firebase-service-account.json
4. 서버(또는 이 워크스페이스)에 배치: # compose: ./secrets → /secrets, FCM_SERVICE_ACCOUNT_FILE=/secrets/firebase-service-account.json
```
새 환경에 다시 놓을 때:
```bash ```bash
# 파일명 고정
mkdir -p secrets mkdir -p secrets
mv ~/Downloads/iykyka-*.json secrets/firebase-service-account.json mv ~/Downloads/iykyka-*.json secrets/firebase-service-account.json
chmod 600 secrets/firebase-service-account.json chmod 600 secrets/firebase-service-account.json
```
5. 프로덕션 호스트에도 동일 파일:
```bash
# 예: scp 후
cd ~/project/ykavu
# secrets/firebase-service-account.json 존재 확인
docker compose up -d --build core-backend docker compose up -d --build core-backend
``` ```
`docker-compose.yml``./secrets` → 컨테이너 `/secrets`로 마운트하고
`FCM_SERVICE_ACCOUNT_FILE=/secrets/firebase-service-account.json`을 읽는다.
**git / 채팅에 JSON 내용을 붙여넣지 않는다.** **git / 채팅에 JSON 내용을 붙여넣지 않는다.**
### N4-2 / N4-4 — 실기기 스모크 ### N4-2 / N4-4 — Android 실기기 스모크
```bash ```bash
cd mobile cd mobile
@ -72,24 +64,27 @@ curl -sS -X POST https://msn.iykyka.com/admin/push-test \
기대: `sent >= 1`. 기대: `sent >= 1`.
`only_placeholder_tokens` → Android에 `google-services.json` 넣고 재설치. `only_placeholder_tokens` → Android에 `google-services.json` 넣고 재설치.
`fcm_not_configured` → 서버에 서비스 계정 파일 경로 확인. `fcm_not_configured` → 서비스 계정 경로 확인.
## Web (N4-W4) ## Web (N4-W4)
설계·수락: [`web-upgrade.md`](./web-upgrade.md) §N4-W4 · 실행 표: [`deploy-checklist.md`](./deploy-checklist.md) §N4-W. 설계·수락: [`web-upgrade.md`](./web-upgrade.md) §N4-W4 · 실행 표: [`deploy-checklist.md`](./deploy-checklist.md) §N4-W.
Android N4(실기기)와 **병행 가능**. 서버 서비스 계정(HTTP v1)은 공유하고, **프로덕션:** Firebase Web config + VAPID + `:web:` APP_ID가 서버 `.env`에 있고 `web` 이미지에 빌드 주입됨.
클라이언트는 `google-services.json`(Android) vs Firebase Web config + VAPID(Web)로 분리. SW(`firebase-messaging-sw.js`) populated. 서버 SA는 Android와 공유.
**코드 shipped.** define이 비어 있으면 웹은 계속 `install:`만 등록 → `/admin/push-test``only_placeholder_tokens`는 정상. ### 남은 accept (브라우저)
### Master 준비물 (live accept) 1. `https://msn.iykyka.com` **하드 리프레시** (또는 사이트 데이터 삭제)
2. 알림 **허용** → 로그인
3. device_tokens에 실 토큰(`platform=web`, `install:` 아님)
4. `/admin/push-test``sent >= 1` (탭 백그라운드에서도 알림)
1. Firebase Console → **Web** 앱 추가 (authorized domain: `msn.iykyka.com`) — SA 프로젝트와 동일 로컬/스테이징에서 define이 비어 있으면 웹은 `install:`만 등록 → `only_placeholder_tokens`가 정상.
- `FIREBASE_APP_ID`**`1:…:web:…` 형태**여야 함. Android(`:android:`) / `google-services.json`의 mobilesdk_app_id를 넣으면 웹 토큰 발급이 실패할 수 있음.
2. Cloud Messaging → Web Push certificates / **VAPID public key** ### 새 환경에 Web 시크릿을 넣을 때
3. 배포 호스트 **`~/project/ykavu/.env`**(클라우드 에이전트 `/workspace/.env`가 아님)에 기입
(`.env.example` 키 이름 — **값 git 금지**): 배포 호스트 **`~/project/ykavu/.env`**(클라우드 에이전트 `/workspace/.env`와 별개):
```bash ```bash
FIREBASE_API_KEY= FIREBASE_API_KEY=
@ -97,18 +92,14 @@ FIREBASE_AUTH_DOMAIN= # 비우면 {projectId}.firebaseapp.com
FIREBASE_PROJECT_ID= FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET= # 비우면 {projectId}.appspot.com FIREBASE_STORAGE_BUCKET= # 비우면 {projectId}.appspot.com
FIREBASE_MESSAGING_SENDER_ID= FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID= # must be :web: app id FIREBASE_APP_ID= # must be 1:…:web: (not :android:)
FIREBASE_VAPID_KEY= FIREBASE_VAPID_KEY=
``` ```
4. 웹 재빌드 후 하드 리프레시:
```bash ```bash
docker compose up -d --build web docker compose up -d --build web
``` ```
5. 로그인 → 알림 허용 → device_tokens에 실 토큰(`platform=web`) → `/admin/push-test``sent >= 1`
### 로컬 Flutter web ### 로컬 Flutter web
```bash ```bash
@ -126,5 +117,5 @@ flutter run -d chrome \
- `mobile/lib/config/firebase_web_config.dart` - `mobile/lib/config/firebase_web_config.dart`
- `mobile/lib/services/push_token_service.dart` (`_tryFirebaseWebToken`) - `mobile/lib/services/push_token_service.dart` (`_tryFirebaseWebToken`)
- `mobile/web/firebase-messaging-sw.js` (+ `.template`, Dockerfile 치환) - `mobile/web/firebase-messaging-sw.js` (+ `.template`, `scripts/docker_build_web.sh`)
- `mobile/Dockerfile` / `docker-compose.yml` build-args - `mobile/Dockerfile` / `docker-compose.yml` build-args

View File

@ -417,12 +417,12 @@ Claude/Cursor 통합 DONE 목록과 항목 ID(N1-1 … N5-5, **N4-W0…W7**)는
- [x] **W1** 웹 로컬 지속성 (prefs/idb · Drift WASM 비채택) — 2026-08-04 - [x] **W1** 웹 로컬 지속성 (prefs/idb · Drift WASM 비채택) — 2026-08-04
- [x] **W2** 데모 UX (분할·WS 상태·ID 복사) — 2026-08-04 - [x] **W2** 데모 UX (분할·WS 상태·ID 복사) — 2026-08-04
- [x] **W3** 인앱 + Notification API — 2026-08-04 - [x] **W3** 인앱 + Notification API — 2026-08-04
- [x] **W4** Web Push 코드 경로 (FCM + VAPID dart-define) — **live accept:** Master `.env` 시크릿 후 - [x] **W4** Web Push (FCM + VAPID · `:web:` APP_ID 프로덕션 주입) — **accept 대기:** 브라우저 하드 리프레시·알림·push-test
- [x] **W5** PWA (manifest 테마 · 셸 캐시) — 2026-08-04 (W4와 독립 선행) - [x] **W5** PWA (manifest 테마 · 셸 캐시) — 2026-08-04 (W4와 독립 선행)
- [x] **W6** 테스터 가이드 — 2026-08-04 - [x] **W6** 테스터 가이드 — 2026-08-04
- [x] **W7** edge 스모크 (`e2e_web_smoke.sh`) — 2026-08-04; Playwright UI는 후속 - [x] **W7** edge 스모크 (`e2e_web_smoke.sh`) — 2026-08-04; Playwright UI는 후속
구현은 Wi 단위. W4만 Master 시크릿 블로커. Android N4와 서버 SA 공유. W0~W7 코드·배포 완료. W4만 브라우저 실토큰·`/admin/push-test` 확인 남음. Android N4와 서버 SA 공유.
## Phase 2 — L3 확장 + 베타 확대 ## Phase 2 — L3 확장 + 베타 확대

View File

@ -15,10 +15,11 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포
1. **넓은 화면(≥900px)** — 왼쪽 대화 목록 · 오른쪽 채팅이 나란히 보입니다. 1. **넓은 화면(≥900px)** — 왼쪽 대화 목록 · 오른쪽 채팅이 나란히 보입니다.
2. **내 ID 복사** — 목록 상단 칩 또는 설정 → 계정 행을 탭합니다. 2. **내 ID 복사** — 목록 상단 칩 또는 설정 → 계정 행을 탭합니다.
3. **브라우저 알림** — 설정 → 「브라우저 알림」에서 허용하면 답장 마감 리마인드를 받을 수 있습니다(거부해도 인앱 배지는 동작). 3. **브라우저 알림** — 설정 → 「브라우저 알림」에서 허용하면 답장 마감 리마인드·(허용 시) Web Push를 받을 수 있습니다(거부해도 인앱 배지는 동작).
4. **홈 화면 추가(PWA)** — Chrome 주소창 옆 설치 아이콘 또는 메뉴 → 「앱 설치」. 4. **로그인 세션 만료 시각** — 설정 → 로그인 세션의 「만료」는 **기기 로컬 시간**(`yyyy-MM-dd HH:mm`)으로 표시됩니다.
5. **로그아웃** — 설정 → 로그아웃 → 입장 화면 「이미 가입」으로 복귀. 5. **홈 화면 추가(PWA)** — Chrome 주소창 옆 설치 아이콘 또는 메뉴 → 「앱 설치」.
6. **캐시가 옛 버전일 때** — 강력 새로고침, 또는 DevTools → Application → Service Workers → Unregister 후 새로고침. 6. **로그아웃** — 설정 → 로그아웃 → 입장 화면 「이미 가입」으로 복귀.
7. **캐시가 옛 버전일 때** — 강력 새로고침, 또는 DevTools → Application → Service Workers → Unregister 후 새로고침.
상세 설계: [`web-upgrade.md`](./web-upgrade.md). 상세 설계: [`web-upgrade.md`](./web-upgrade.md).
@ -42,9 +43,9 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포
- **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다. - **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다.
- **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송. - **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송.
- 푸시: Android·Web Push는 Master Firebase 설정 후 (`docs/fcm-setup.md`, `web-upgrade` W4). - 푸시: 웹은 프로덕션에 Firebase Web(`:web:`)·VAPID가 이미 주입되어 있습니다.
웹은 서버 `.env``FIREBASE_*`/`VAPID`가 있고 `web` 이미지를 그 값으로 빌드한 뒤에야 실 토큰이 등록됩니다. 그전엔 `install:` 플레이스홀더. **하드 리프레시**(또는 사이트 데이터 삭제) → 알림 **허용** → 로그인하면 실 FCM 토큰이 등록됩니다.
Web Push 배포 직후엔 **하드 리프레시**(또는 사이트 데이터 삭제)로 `firebase-messaging-sw.js`를 갱신하세요. Android 실기기 푸시는 `google-services.json` 배치 후 (`docs/fcm-setup.md`).
- 문제/스크린샷은 Master에게 전달해 주세요. - 문제/스크린샷은 Master에게 전달해 주세요.
- 민감 정보·실명 대화는 베타 특성상 최소화해 주세요. - 민감 정보·실명 대화는 베타 특성상 최소화해 주세요.

View File

@ -255,38 +255,38 @@ flowchart LR
| | | | | |
|--|--| |--|--|
| **Status** | **live injected** (2026-08-04) — 서버 `.env`+SW populated · **accept 대기:** `FIREBASE_APP_ID``:android:`로 보여 Web 앱(`:web:`) 교체 후 브라우저 토큰·push-test 확인 | | **Status** | **web-live** (2026-08-04) — `:web:` APP_ID·VAPID·SW 프로덕션 주입 완료 · **accept 대기:** 하드 리프레시→알림→로그인→실 토큰·`/admin/push-test` |
| **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 | | **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 |
| **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 | | **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 |
### As-is → shipped ### Shipped (프로덕션)
- `FirebaseWebConfig` + `--dart-define=FIREBASE_*` / `FIREBASE_VAPID_KEY` - `FirebaseWebConfig` + compose/`Dockerfile` build-args · SW template 치환
- `PushTokenService` 웹 분기: config 없으면 `install:` soft-fallback, 있으면 `initializeApp` + `getToken(vapidKey:)` - `PushTokenService` 웹 분기 · `msn.iykyka.com``:web:` APP_ID·VAPID 주입·재배포됨
- `mobile/web/firebase-messaging-sw.js` (+ Dockerfile이 template로 치환) · `index.html` SW 등록 - 서버 SA(`secrets/firebase-service-account.json`) 공유 · 시크릿 git 미커밋
- compose `web` build-args · `.env.example` 키 이름만 · nginx no-cache for SW
### Master 액션 (live accept 블로커) ### Master 액션 (accept만)
1. Firebase Console에 Web 앱 추가 (`msn.iykyka.com`) — 프로젝트 `iykyka` SA와 동일 프로젝트 1. Chrome에서 `https://msn.iykyka.com` **하드 리프레시** (또는 사이트 데이터 삭제)
2. Cloud Messaging Web Push 인증서/VAPID 키 발급 2. 알림 **허용** → DEMO/계정 로그인
3. 서버 `~/project/ykavu/.env``FIREBASE_API_KEY``FIREBASE_VAPID_KEY` 기입 (git 금지) 3. device_tokens에 `platform=web` 실 토큰 확인 후 `POST /admin/push-test``sent >= 1`
4. `docker compose up -d --build web` 후 하드 리프레시 → 로그인 → `/admin/push-test`
### 작업 분해 ### 작업 분해
- [x] 빌드 주입: `--dart-define=FIREBASE_…` + SW template 치환 (gitignore/시크릿 미커밋) - [x] 빌드 주입: `--dart-define=FIREBASE_…` + SW template 치환 (gitignore/시크릿 미커밋)
- [x] `PushTokenService` 웹 분기 + permission - [x] `PushTokenService` 웹 분기 + permission
- [x] Docker web 빌드에 define 전달 (`mobile/Dockerfile` / compose) - [x] Docker web 빌드에 define 전달 (`mobile/Dockerfile` / compose)
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신 (**Master 시크릿 후**) - [x] 프로덕션 `.env``FIREBASE_*` / VAPID · `:web:` APP_ID · `web` 재빌드
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신 (**브라우저 accept**)
- [x] `docs/fcm-setup.md` Web 절 동기화 - [x] `docs/fcm-setup.md` Web 절 동기화
- [x] `DataFlowScreen` 푸시 문구 갱신 - [x] `DataFlowScreen` 푸시 문구 갱신
### 수락 기준 ### 수락 기준
- [ ] device_tokens에 `install:`이 아닌 토큰 + `platform=web` (**시크릿 후**) - [ ] device_tokens에 `install:`이 아닌 토큰 + `platform=web` (**브라우저 로그인 후**)
- [ ] `POST /admin/push-test``sent >= 1`, 브라우저 알림 표시 (**시크릿 후**) - [ ] `POST /admin/push-test``sent >= 1`, 브라우저 알림 표시
- [x] 시크릿이 git history에 없음 (define/env만) - [x] 시크릿이 git history에 없음 (define/env만)
- [x] 프로덕션 SW populated (`firebase.initializeApp`, appId `:web:`)
### 위험 ### 위험
@ -466,13 +466,13 @@ Wi를 끝낼 때마다:
--- ---
## 부록 B — Master 시크릿 체크리스트 (W4) ## 부록 B — Master 시크릿·accept 체크리스트 (W4)
- [ ] Firebase Web app 등록 (도메인 `msn.iykyka.com`) - [x] Firebase Web app 등록 (도메인 `msn.iykyka.com`, APP_ID `:web:`)
- [ ] VAPID key - [x] VAPID key
- [ ] 서버 `~/project/ykavu/.env``FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `docker compose up -d --build web` - [x] 서버 `~/project/ykavu/.env``FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `web` 재빌드
- [ ] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유) - [x] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
- [ ] push-test로 `sent >= 1` 확인 - [ ] 하드 리프레시·알림 허용·로그인 후 push-test로 `sent >= 1` 확인
--- ---

View File

@ -69,8 +69,8 @@ sudo apt-get install -y libsqlite3-dev libsqlcipher1
- 로컬 DB: `lib/db/app_database_web.dart`**W1 done**: SharedPreferences 지속성(비암호화 프리뷰) - 로컬 DB: `lib/db/app_database_web.dart`**W1 done**: SharedPreferences 지속성(비암호화 프리뷰)
- 스누즈 OS 알림: 웹 no-op → **W3** Notification API / 인앱 - 스누즈 OS 알림: 웹 no-op → **W3** Notification API / 인앱
- 푸시: 웹은 `FIREBASE_*` + `FIREBASE_VAPID_KEY` dart-define 시 실 FCM 토큰 · 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절) - 푸시: 프로덕션은 `FIREBASE_*` + VAPID 주입됨 · 로컬은 dart-define 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절)
- PWA: `web/manifest.json` 골격 있음 → **W5** 테마·설치·셸 캐시 - PWA: **W5 done** — manifest 테마·설치·셸 캐시
- **Q9**(수신 자동응대)는 웹 트랙 비범위 - **Q9**(수신 자동응대)는 웹 트랙 비범위
```bash ```bash
@ -86,8 +86,8 @@ cd mobile && flutter run -d chrome --dart-define=CORE_API_BASE=http://localhost:
## 아직 없는 것 ## 아직 없는 것
- 웹 W1~W7 구현 ([`docs/web-upgrade.md`](../docs/web-upgrade.md) — 설계만 완료, Wi 승인 후) - 웹 W4 **accept** — 브라우저 실 FCM 토큰·`/admin/push-test` (코드·`:web:` 배포는 완료, [`web-upgrade.md`](../docs/web-upgrade.md))
- Master Firebase Android 시크릿 후 실기기 푸시 스모크 (N4-1/3/4) - Android: `google-services.json` PC 배치 후 실기기 푸시 스모크 (N4-1/4; 서버 SA는 N4-3 done)
- 멀티디바이스 실시간 설정 동기화 고도화 - 멀티디바이스 실시간 설정 동기화 고도화
- 온보딩 말투 UX 디테일 (PoC #1 결과는 맨 마지막에 반영) - 온보딩 말투 UX 디테일 (PoC #1 결과는 맨 마지막에 반영)
- iOS 빌드 (v1 범위 밖) - iOS 빌드 (v1 범위 밖)

View File

@ -8,6 +8,17 @@ import '../state/session_state.dart';
class SessionsScreen extends StatefulWidget { class SessionsScreen extends StatefulWidget {
const SessionsScreen({super.key}); const SessionsScreen({super.key});
/// Formats API `expires_at` (UTC RFC3339) for the device local timezone.
static String formatExpiresAtLocal(Object? raw) {
if (raw == null) return '';
final parsed = DateTime.tryParse(raw.toString());
if (parsed == null) return raw.toString();
final local = parsed.toLocal();
String two(int n) => n.toString().padLeft(2, '0');
return '${local.year}-${two(local.month)}-${two(local.day)} '
'${two(local.hour)}:${two(local.minute)}';
}
@override @override
State<SessionsScreen> createState() => _SessionsScreenState(); State<SessionsScreen> createState() => _SessionsScreenState();
} }
@ -104,7 +115,10 @@ class _SessionsScreenState extends State<SessionsScreen> {
s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}', s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}',
style: theme.textTheme.titleSmall, style: theme.textTheme.titleSmall,
), ),
subtitle: Text('만료: ${s['expires_at'] ?? ''}', style: theme.textTheme.bodySmall), subtitle: Text(
'만료: ${SessionsScreen.formatExpiresAtLocal(s['expires_at'])}',
style: theme.textTheme.bodySmall,
),
trailing: IconButton( trailing: IconButton(
tooltip: '세션 종료', tooltip: '세션 종료',
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),

View File

@ -0,0 +1,24 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ykavu_mobile/screens/sessions_screen.dart';
void main() {
test('formatExpiresAtLocal converts UTC Z to local wall clock', () {
// Fixed offset via DateTime.parse keeps instant; toLocal() uses test TZ.
final raw = '2026-09-02T09:06:45.137172Z';
final out = SessionsScreen.formatExpiresAtLocal(raw);
final expected = DateTime.parse(raw).toLocal();
final two = (int n) => n.toString().padLeft(2, '0');
expect(
out,
'${expected.year}-${two(expected.month)}-${two(expected.day)} '
'${two(expected.hour)}:${two(expected.minute)}',
);
expect(out.contains('T'), isFalse);
expect(out.endsWith('Z'), isFalse);
});
test('formatExpiresAtLocal handles null and garbage', () {
expect(SessionsScreen.formatExpiresAtLocal(null), '');
expect(SessionsScreen.formatExpiresAtLocal('not-a-date'), 'not-a-date');
});
}