Compare commits
No commits in common. "main" and "cursor/w4-web-appid-live-382d" have entirely different histories.
main
...
cursor/w4-
|
|
@ -73,9 +73,8 @@
|
||||||
|
|
||||||
- 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`**
|
||||||
- **웹 고도화 (W0~W7):** 구현·배포 완료 — [`docs/web-upgrade.md`](./docs/web-upgrade.md).
|
- **진행 중 (설계 완료·구현 Wi 승인 대기):** 웹 고도화 [`docs/web-upgrade.md`](./docs/web-upgrade.md) (W0 경계 done · W1~W7 todo)
|
||||||
**남은 것:** W4 **accept** (하드 리프레시·알림 허용·로그인 후 `/admin/push-test` → `sent >= 1`)
|
- **후순위 (Master):** Android N4 — `google-services.json` · 실기기 FCM · UI 탭 · APK · `SHARE_URL`
|
||||||
- **후순위 (Master):** Android N4 — `google-services.json` PC 배치 · 실기기 FCM · UI 탭 · APK · `SHARE_URL`
|
|
||||||
- **결정 대기:** Q9 — L2 의미 (카피 정렬 ± 수신 자동응대) — 웹 트랙에도 **비범위**
|
- **결정 대기:** Q9 — L2 의미 (카피 정렬 ± 수신 자동응대) — 웹 트랙에도 **비범위**
|
||||||
- **맨 마지막:** N5 / D — 사람 PoC. §3 기본값 추측 금지
|
- **맨 마지막:** N5 / D — 사람 PoC. §3 기본값 추측 금지
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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~W7 구현·배포 **done** · **W4 accept만** (하드 리프레시·알림·push-test)
|
- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W3·W5~W7 **done** · **W4 web APP_ID live** (accept: 브라우저 하드 리프레시·알림 허용·push-test)
|
||||||
- 후순위: **N4 Android FCM/UI** — `google-services.json` PC 배치·실기기 (서버 SA는 배치됨)
|
- 후순위: **N4 Android FCM/UI** — Master 시크릿·실기기
|
||||||
- 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) | **web-live** (2026-08-04) | 시크릿·`:web:`·SW 배포 done · **accept만:** 하드 리프레시·알림·실 토큰·push-test |
|
| **N4-W4** | Web Push (FCM + VAPID) | **web-live** (2026-08-04) | `:web:` APP_ID·SW populated · **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) | **done** (2026-08) | 프로덕션 `secrets/firebase-service-account.json` 마운트됨. 웹 W4와 공유 |
|
| **N4-3** | 서버 FCM 자격증명 (HTTP v1) | doing | Master: `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** — `msn.iykyka.com` 하드 리프레시·알림 허용·로그인 후 `/admin/push-test` · 또는 Android N4-1/4.
|
**바로 다음:** 웹 **W4 accept** — `msn.iykyka.com` 하드 리프레시·알림 허용·로그인 후 `/admin/push-test` · 또는 Android N4-1/3.
|
||||||
웹 W0~W7 코드·`:web:` 시크릿 배포·서버 SA **done** (브라우저 실토큰·push-test만 남음).
|
웹 W0~W7 코드·`:web:` 시크릿 배포 **done** (브라우저 실토큰 확인만 남음).
|
||||||
|
|
||||||
|
|
||||||
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.
|
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@
|
||||||
서버·클라이언트의 푸시 **코드 경로는 준비됨**.
|
서버·클라이언트의 푸시 **코드 경로는 준비됨**.
|
||||||
새 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` + 프로덕션 `.env` `FIREBASE_*`/`VAPID` 주입 · SW populated (`:web:` APP_ID) |
|
| Web (N4-W4) | `FirebaseWebConfig` + `FIREBASE_*`/`VAPID` dart-define · SW template · 미설정 시 `install:` |
|
||||||
| core-backend | `notifyUser` + `POST /admin/push-test` — **FCM HTTP v1**(서비스 계정) 우선 |
|
| core-backend | `notifyUser` + `POST /admin/push-test` — **FCM HTTP v1**(서비스 계정) 우선, 레거시 `FCM_SERVER_KEY`는 폴백 |
|
||||||
| 서버 SA (N4-3) | 프로덕션 `secrets/firebase-service-account.json` 마운트 **done** |
|
|
||||||
|
|
||||||
## Master가 할 일
|
## Master가 할 일
|
||||||
|
|
||||||
|
|
@ -19,35 +18,44 @@
|
||||||
|
|
||||||
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`을 **Android 빌드 PC**에만 배치 (git 금지):
|
2. `google-services.json`을 로컬에만 배치 (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) — **프로덕션 done**
|
### N4-3 — 서비스 계정 JSON (HTTP v1)
|
||||||
|
|
||||||
레거시 **서버 키**가 Cloud Messaging 탭에서 `사용 중지됨`이면 정상이다.
|
레거시 **서버 키**가 Cloud Messaging 탭에서 `사용 중지됨`이면 정상이다. 아래를 쓴다.
|
||||||
|
|
||||||
프로덕션 호스트에 이미 배치됨:
|
1. Google Cloud → 사용자 인증 정보 → 서비스 계정 만들기
|
||||||
|
(API: Firebase Cloud Messaging API, 데이터: **애플리케이션 데이터**)
|
||||||
```bash
|
2. 역할: **Firebase Cloud Messaging Admin** (없으면 Firebase 관리자 / 임시 소유자)
|
||||||
# ~/project/ykavu/secrets/firebase-service-account.json
|
3. 키 유형 **JSON** 다운로드
|
||||||
# compose: ./secrets → /secrets, FCM_SERVICE_ACCOUNT_FILE=/secrets/firebase-service-account.json
|
4. 서버(또는 이 워크스페이스)에 배치:
|
||||||
```
|
|
||||||
|
|
||||||
새 환경에 다시 놓을 때:
|
|
||||||
|
|
||||||
```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 — Android 실기기 스모크
|
### N4-2 / N4-4 — 실기기 스모크
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd mobile
|
cd mobile
|
||||||
|
|
@ -64,27 +72,24 @@ 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.
|
||||||
|
|
||||||
**프로덕션:** Firebase Web config + VAPID + `:web:` APP_ID가 서버 `.env`에 있고 `web` 이미지에 빌드 주입됨.
|
Android N4(실기기)와 **병행 가능**. 서버 서비스 계정(HTTP v1)은 공유하고,
|
||||||
SW(`firebase-messaging-sw.js`) populated. 서버 SA는 Android와 공유.
|
클라이언트는 `google-services.json`(Android) vs Firebase Web config + VAPID(Web)로 분리.
|
||||||
|
|
||||||
### 남은 accept (브라우저)
|
**코드 shipped.** define이 비어 있으면 웹은 계속 `install:`만 등록 → `/admin/push-test`의 `only_placeholder_tokens`는 정상.
|
||||||
|
|
||||||
1. `https://msn.iykyka.com` **하드 리프레시** (또는 사이트 데이터 삭제)
|
### Master 준비물 (live accept)
|
||||||
2. 알림 **허용** → 로그인
|
|
||||||
3. device_tokens에 실 토큰(`platform=web`, `install:` 아님)
|
|
||||||
4. `/admin/push-test` → `sent >= 1` (탭 백그라운드에서도 알림)
|
|
||||||
|
|
||||||
로컬/스테이징에서 define이 비어 있으면 웹은 `install:`만 등록 → `only_placeholder_tokens`가 정상.
|
1. Firebase Console → **Web** 앱 추가 (authorized domain: `msn.iykyka.com`) — SA 프로젝트와 동일
|
||||||
|
- `FIREBASE_APP_ID`는 **`1:…:web:…` 형태**여야 함. Android(`:android:`) / `google-services.json`의 mobilesdk_app_id를 넣으면 웹 토큰 발급이 실패할 수 있음.
|
||||||
### 새 환경에 Web 시크릿을 넣을 때
|
2. Cloud Messaging → Web Push certificates / **VAPID public key**
|
||||||
|
3. 배포 호스트 **`~/project/ykavu/.env`**(클라우드 에이전트 `/workspace/.env`가 아님)에 기입
|
||||||
배포 호스트 **`~/project/ykavu/.env`**(클라우드 에이전트 `/workspace/.env`와 별개):
|
(`.env.example` 키 이름 — **값 git 금지**):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
FIREBASE_API_KEY=
|
FIREBASE_API_KEY=
|
||||||
|
|
@ -92,14 +97,18 @@ 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 1:…:web:… (not :android:)
|
FIREBASE_APP_ID= # must be :web: app id
|
||||||
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
|
||||||
|
|
@ -117,5 +126,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`, `scripts/docker_build_web.sh`)
|
- `mobile/web/firebase-messaging-sw.js` (+ `.template`, Dockerfile 치환)
|
||||||
- `mobile/Dockerfile` / `docker-compose.yml` build-args
|
- `mobile/Dockerfile` / `docker-compose.yml` build-args
|
||||||
|
|
|
||||||
|
|
@ -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 · `:web:` APP_ID 프로덕션 주입) — **accept 대기:** 브라우저 하드 리프레시·알림·push-test
|
- [x] **W4** Web Push 코드 경로 (FCM + VAPID dart-define) — **live accept:** Master `.env` 시크릿 후
|
||||||
- [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는 후속
|
||||||
|
|
||||||
W0~W7 코드·배포 완료. W4만 브라우저 실토큰·`/admin/push-test` 확인 남음. Android N4와 서버 SA 공유.
|
구현은 Wi 단위. W4만 Master 시크릿 블로커. Android N4와 서버 SA 공유.
|
||||||
|
|
||||||
|
|
||||||
## Phase 2 — L3 확장 + 베타 확대
|
## Phase 2 — L3 확장 + 베타 확대
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포
|
||||||
|
|
||||||
1. **넓은 화면(≥900px)** — 왼쪽 대화 목록 · 오른쪽 채팅이 나란히 보입니다.
|
1. **넓은 화면(≥900px)** — 왼쪽 대화 목록 · 오른쪽 채팅이 나란히 보입니다.
|
||||||
2. **내 ID 복사** — 목록 상단 칩 또는 설정 → 계정 행을 탭합니다.
|
2. **내 ID 복사** — 목록 상단 칩 또는 설정 → 계정 행을 탭합니다.
|
||||||
3. **브라우저 알림** — 설정 → 「브라우저 알림」에서 허용하면 답장 마감 리마인드·(허용 시) Web Push를 받을 수 있습니다(거부해도 인앱 배지는 동작).
|
3. **브라우저 알림** — 설정 → 「브라우저 알림」에서 허용하면 답장 마감 리마인드를 받을 수 있습니다(거부해도 인앱 배지는 동작).
|
||||||
4. **로그인 세션 만료 시각** — 설정 → 로그인 세션의 「만료」는 **기기 로컬 시간**(`yyyy-MM-dd HH:mm`)으로 표시됩니다.
|
4. **홈 화면 추가(PWA)** — Chrome 주소창 옆 설치 아이콘 또는 메뉴 → 「앱 설치」.
|
||||||
5. **홈 화면 추가(PWA)** — Chrome 주소창 옆 설치 아이콘 또는 메뉴 → 「앱 설치」.
|
5. **로그아웃** — 설정 → 로그아웃 → 입장 화면 「이미 가입」으로 복귀.
|
||||||
6. **로그아웃** — 설정 → 로그아웃 → 입장 화면 「이미 가입」으로 복귀.
|
6. **캐시가 옛 버전일 때** — 강력 새로고침, 또는 DevTools → Application → Service Workers → Unregister 후 새로고침.
|
||||||
7. **캐시가 옛 버전일 때** — 강력 새로고침, 또는 DevTools → Application → Service Workers → Unregister 후 새로고침.
|
|
||||||
|
|
||||||
상세 설계: [`web-upgrade.md`](./web-upgrade.md).
|
상세 설계: [`web-upgrade.md`](./web-upgrade.md).
|
||||||
|
|
||||||
|
|
@ -43,9 +42,9 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포
|
||||||
|
|
||||||
- **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다.
|
- **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다.
|
||||||
- **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송.
|
- **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송.
|
||||||
- 푸시: 웹은 프로덕션에 Firebase Web(`:web:`)·VAPID가 이미 주입되어 있습니다.
|
- 푸시: Android·Web Push는 Master Firebase 설정 후 (`docs/fcm-setup.md`, `web-upgrade` W4).
|
||||||
**하드 리프레시**(또는 사이트 데이터 삭제) → 알림 **허용** → 로그인하면 실 FCM 토큰이 등록됩니다.
|
웹은 서버 `.env`에 `FIREBASE_*`/`VAPID`가 있고 `web` 이미지를 그 값으로 빌드한 뒤에야 실 토큰이 등록됩니다. 그전엔 `install:` 플레이스홀더.
|
||||||
Android 실기기 푸시는 `google-services.json` 배치 후 (`docs/fcm-setup.md`).
|
Web Push 배포 직후엔 **하드 리프레시**(또는 사이트 데이터 삭제)로 `firebase-messaging-sw.js`를 갱신하세요.
|
||||||
- 문제/스크린샷은 Master에게 전달해 주세요.
|
- 문제/스크린샷은 Master에게 전달해 주세요.
|
||||||
- 민감 정보·실명 대화는 베타 특성상 최소화해 주세요.
|
- 민감 정보·실명 대화는 베타 특성상 최소화해 주세요.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,38 +255,38 @@ flowchart LR
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|--|--|
|
|--|--|
|
||||||
| **Status** | **web-live** (2026-08-04) — `:web:` APP_ID·VAPID·SW 프로덕션 주입 완료 · **accept 대기:** 하드 리프레시→알림→로그인→실 토큰·`/admin/push-test` |
|
| **Status** | **web APP_ID live** (2026-08-04) — SW `:web:` · **accept 대기:** 하드 리프레시→알림 허용→로그인 후 실 토큰·`/admin/push-test` |
|
||||||
| **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 |
|
| **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 |
|
||||||
| **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 |
|
| **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 |
|
||||||
|
|
||||||
### Shipped (프로덕션)
|
### As-is → shipped
|
||||||
|
|
||||||
- `FirebaseWebConfig` + compose/`Dockerfile` build-args · SW template 치환
|
- `FirebaseWebConfig` + `--dart-define=FIREBASE_*` / `FIREBASE_VAPID_KEY`
|
||||||
- `PushTokenService` 웹 분기 · `msn.iykyka.com`에 `:web:` APP_ID·VAPID 주입·재배포됨
|
- `PushTokenService` 웹 분기: config 없으면 `install:` soft-fallback, 있으면 `initializeApp` + `getToken(vapidKey:)`
|
||||||
- 서버 SA(`secrets/firebase-service-account.json`) 공유 · 시크릿 git 미커밋
|
- `mobile/web/firebase-messaging-sw.js` (+ Dockerfile이 template로 치환) · `index.html` SW 등록
|
||||||
|
- compose `web` build-args · `.env.example` 키 이름만 · nginx no-cache for SW
|
||||||
|
|
||||||
### Master 액션 (accept만)
|
### Master 액션 (live accept 블로커)
|
||||||
|
|
||||||
1. Chrome에서 `https://msn.iykyka.com` **하드 리프레시** (또는 사이트 데이터 삭제)
|
1. Firebase Console에 Web 앱 추가 (`msn.iykyka.com`) — 프로젝트 `iykyka` SA와 동일 프로젝트
|
||||||
2. 알림 **허용** → DEMO/계정 로그인
|
2. Cloud Messaging Web Push 인증서/VAPID 키 발급
|
||||||
3. device_tokens에 `platform=web` 실 토큰 확인 후 `POST /admin/push-test` → `sent >= 1`
|
3. 서버 `~/project/ykavu/.env`에 `FIREBASE_API_KEY` … `FIREBASE_VAPID_KEY` 기입 (git 금지)
|
||||||
|
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)
|
||||||
- [x] 프로덕션 `.env`에 `FIREBASE_*` / VAPID · `:web:` APP_ID · `web` 재빌드
|
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신 (**Master 시크릿 후**)
|
||||||
- [ ] `/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 시크릿·accept 체크리스트 (W4)
|
## 부록 B — Master 시크릿 체크리스트 (W4)
|
||||||
|
|
||||||
- [x] Firebase Web app 등록 (도메인 `msn.iykyka.com`, APP_ID `:web:`)
|
- [ ] Firebase Web app 등록 (도메인 `msn.iykyka.com`)
|
||||||
- [x] VAPID key
|
- [ ] VAPID key
|
||||||
- [x] 서버 `~/project/ykavu/.env`에 `FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `web` 재빌드
|
- [ ] 서버 `~/project/ykavu/.env`에 `FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `docker compose up -d --build web`
|
||||||
- [x] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
|
- [ ] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
|
||||||
- [ ] 하드 리프레시·알림 허용·로그인 후 push-test로 `sent >= 1` 확인
|
- [ ] push-test로 `sent >= 1` 확인
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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_*` + VAPID 주입됨 · 로컬은 dart-define 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절)
|
- 푸시: 웹은 `FIREBASE_*` + `FIREBASE_VAPID_KEY` dart-define 시 실 FCM 토큰 · 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절)
|
||||||
- PWA: **W5 done** — manifest 테마·설치·셸 캐시
|
- PWA: `web/manifest.json` 골격 있음 → **W5** 테마·설치·셸 캐시
|
||||||
- **Q9**(수신 자동응대)는 웹 트랙 비범위
|
- **Q9**(수신 자동응대)는 웹 트랙 비범위
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -86,8 +86,8 @@ cd mobile && flutter run -d chrome --dart-define=CORE_API_BASE=http://localhost:
|
||||||
|
|
||||||
## 아직 없는 것
|
## 아직 없는 것
|
||||||
|
|
||||||
- 웹 W4 **accept** — 브라우저 실 FCM 토큰·`/admin/push-test` (코드·`:web:` 배포는 완료, [`web-upgrade.md`](../docs/web-upgrade.md))
|
- 웹 W1~W7 구현 ([`docs/web-upgrade.md`](../docs/web-upgrade.md) — 설계만 완료, Wi 승인 후)
|
||||||
- Android: `google-services.json` PC 배치 후 실기기 푸시 스모크 (N4-1/4; 서버 SA는 N4-3 done)
|
- Master Firebase Android 시크릿 후 실기기 푸시 스모크 (N4-1/3/4)
|
||||||
- 멀티디바이스 실시간 설정 동기화 고도화
|
- 멀티디바이스 실시간 설정 동기화 고도화
|
||||||
- 온보딩 말투 UX 디테일 (PoC #1 결과는 맨 마지막에 반영)
|
- 온보딩 말투 UX 디테일 (PoC #1 결과는 맨 마지막에 반영)
|
||||||
- iOS 빌드 (v1 범위 밖)
|
- iOS 빌드 (v1 범위 밖)
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
@ -115,10 +104,7 @@ 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(
|
subtitle: Text('만료: ${s['expires_at'] ?? ''}', style: theme.textTheme.bodySmall),
|
||||||
'만료: ${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),
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue