Compare commits

..

No commits in common. "main" and "cursor/w4-web-appid-live-382d" have entirely different histories.

9 changed files with 82 additions and 113 deletions

View File

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

View File

@ -37,8 +37,8 @@ Phase 1 **A~C** 이후 실행 트랙. 작업 단위를 하나씩 처리한다.
- 앱 코드는 클로즈드 베타 직전 수준
- **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료**
- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W7 구현·배포 **done** · **W4 accept만** (하드 리프레시·알림·push-test)
- 후순위: **N4 Android FCM/UI**`google-services.json` PC 배치·실기기 (서버 SA는 배치됨)
- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W3·W5~W7 **done** · **W4 web APP_ID live** (accept: 브라우저 하드 리프레시·알림 허용·push-test)
- 후순위: **N4 Android FCM/UI**Master 시크릿·실기기
- UI: **iMessage-inspired light default** + soft charcoal dark 프로덕션 반영
- **Track C 콘텐츠 갭 A~F**: 프로덕션 반영 완료 (`95422bb`대)
- **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-W2** | 데모 메신저 UX | **done** (2026-08-04) | ≥900px 분할·WS 상태·ID 복사·웹 안내 배너 |
| **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-W6** | 테스터 가이드 | **done** (2026-08-04) | `tester-guide.md` 웹 절 |
| **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-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` + 기기 수신 |
### Android UI 탭 (`mobile/README.md`)
@ -290,8 +290,8 @@ N1~N4(배포·품질에 필요한 최소분) 이후에만 착수. `roadmap.md` P
확인 필요, 그때까지는 인앱 배지·배너가 실질적 대체 경로. Track C A~F 프로덕션
재배포는 **완료** (`95422bb` / docs `f363e8e`, 2026-08-03).
**바로 다음:** 웹 **W4 accept**`msn.iykyka.com` 하드 리프레시·알림 허용·로그인 후 `/admin/push-test` · 또는 Android N4-1/4.
웹 W0~W7 코드·`:web:` 시크릿 배포·서버 SA **done** (브라우저 실토큰·push-test만 남음).
**바로 다음:** 웹 **W4 accept**`msn.iykyka.com` 하드 리프레시·알림 허용·로그인 후 `/admin/push-test` · 또는 Android N4-1/3.
웹 W0~W7 코드·`:web:` 시크릿 배포 **done** (브라우저 실토큰 확인만 남음).
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.

View File

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

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] **W2** 데모 UX (분할·WS 상태·ID 복사) — 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] **W6** 테스터 가이드 — 2026-08-04
- [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 확장 + 베타 확대

View File

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

View File

@ -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 웹 토큰**으로 전달 |
| **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 |
### Shipped (프로덕션)
### As-is → shipped
- `FirebaseWebConfig` + compose/`Dockerfile` build-args · SW template 치환
- `PushTokenService` 웹 분기 · `msn.iykyka.com``:web:` APP_ID·VAPID 주입·재배포됨
- 서버 SA(`secrets/firebase-service-account.json`) 공유 · 시크릿 git 미커밋
- `FirebaseWebConfig` + `--dart-define=FIREBASE_*` / `FIREBASE_VAPID_KEY`
- `PushTokenService` 웹 분기: config 없으면 `install:` soft-fallback, 있으면 `initializeApp` + `getToken(vapidKey:)`
- `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` **하드 리프레시** (또는 사이트 데이터 삭제)
2. 알림 **허용** → DEMO/계정 로그인
3. device_tokens에 `platform=web` 실 토큰 확인 후 `POST /admin/push-test``sent >= 1`
1. Firebase Console에 Web 앱 추가 (`msn.iykyka.com`) — 프로젝트 `iykyka` SA와 동일 프로젝트
2. Cloud Messaging Web Push 인증서/VAPID 키 발급
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] `PushTokenService` 웹 분기 + permission
- [x] Docker web 빌드에 define 전달 (`mobile/Dockerfile` / compose)
- [x] 프로덕션 `.env``FIREBASE_*` / VAPID · `:web:` APP_ID · `web` 재빌드
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신 (**브라우저 accept**)
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신 (**Master 시크릿 후**)
- [x] `docs/fcm-setup.md` Web 절 동기화
- [x] `DataFlowScreen` 푸시 문구 갱신
### 수락 기준
- [ ] device_tokens에 `install:`이 아닌 토큰 + `platform=web` (**브라우저 로그인 후**)
- [ ] `POST /admin/push-test``sent >= 1`, 브라우저 알림 표시
- [ ] device_tokens에 `install:`이 아닌 토큰 + `platform=web` (**시크릿 후**)
- [ ] `POST /admin/push-test``sent >= 1`, 브라우저 알림 표시 (**시크릿 후**)
- [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:`)
- [x] VAPID key
- [x] 서버 `~/project/ykavu/.env``FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `web` 재빌드
- [x] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
- [ ] 하드 리프레시·알림 허용·로그인 후 push-test로 `sent >= 1` 확인
- [ ] Firebase Web app 등록 (도메인 `msn.iykyka.com`)
- [ ] VAPID key
- [ ] 서버 `~/project/ykavu/.env``FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `docker compose up -d --build web`
- [ ] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
- [ ] 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 지속성(비암호화 프리뷰)
- 스누즈 OS 알림: 웹 no-op → **W3** Notification API / 인앱
- 푸시: 프로덕션은 `FIREBASE_*` + VAPID 주입됨 · 로컬은 dart-define 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절)
- PWA: **W5 done** — manifest 테마·설치·셸 캐시
- 푸시: 웹은 `FIREBASE_*` + `FIREBASE_VAPID_KEY` dart-define 시 실 FCM 토큰 · 없으면 `install:` ([`docs/fcm-setup.md`](../docs/fcm-setup.md) Web 절)
- PWA: `web/manifest.json` 골격 있음 → **W5** 테마·설치·셸 캐시
- **Q9**(수신 자동응대)는 웹 트랙 비범위
```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))
- Android: `google-services.json` PC 배치 후 실기기 푸시 스모크 (N4-1/4; 서버 SA는 N4-3 done)
- 웹 W1~W7 구현 ([`docs/web-upgrade.md`](../docs/web-upgrade.md) — 설계만 완료, Wi 승인 후)
- Master Firebase Android 시크릿 후 실기기 푸시 스모크 (N4-1/3/4)
- 멀티디바이스 실시간 설정 동기화 고도화
- 온보딩 말투 UX 디테일 (PoC #1 결과는 맨 마지막에 반영)
- iOS 빌드 (v1 범위 밖)

View File

@ -8,17 +8,6 @@ import '../state/session_state.dart';
class SessionsScreen extends StatefulWidget {
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
State<SessionsScreen> createState() => _SessionsScreenState();
}
@ -115,10 +104,7 @@ class _SessionsScreenState extends State<SessionsScreen> {
s['is_current'] == true ? '이 기기 (현재)' : '세션 #${s['id']}',
style: theme.textTheme.titleSmall,
),
subtitle: Text(
'만료: ${SessionsScreen.formatExpiresAtLocal(s['expires_at'])}',
style: theme.textTheme.bodySmall,
),
subtitle: Text('만료: ${s['expires_at'] ?? ''}', style: theme.textTheme.bodySmall),
trailing: IconButton(
tooltip: '세션 종료',
icon: const Icon(Icons.logout),

View File

@ -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');
});
}