diff --git a/deploy/nginx-web.conf b/deploy/nginx-web.conf index 9c36acb..daf2345 100644 --- a/deploy/nginx-web.conf +++ b/deploy/nginx-web.conf @@ -12,7 +12,21 @@ server { # Docker Compose embedded DNS resolver 127.0.0.11 valid=10s ipv6=off; - # Flutter SPA + # Entrypoint + service worker: always revalidate (PWA / docs/web-upgrade N4-W5). + location = /index.html { + add_header Cache-Control "no-cache"; + try_files $uri =404; + } + location = /flutter_service_worker.js { + add_header Cache-Control "no-cache"; + try_files $uri =404; + } + location = /manifest.json { + add_header Cache-Control "no-cache"; + try_files $uri =404; + } + + # Flutter SPA shell — offline shell cache is SW-managed; HTML fallback for deep links. location / { try_files $uri $uri/ /index.html; } diff --git a/docs/deploy-checklist.md b/docs/deploy-checklist.md index 07861be..7413425 100644 --- a/docs/deploy-checklist.md +++ b/docs/deploy-checklist.md @@ -37,7 +37,7 @@ Phase 1 **A~C** 이후 실행 트랙. 작업 단위를 하나씩 처리한다. - 앱 코드는 클로즈드 베타 직전 수준 - **`https://msn.iykyka.com` 라이브 + N3 완료 + Gemini 실초안 OK + Track A/B 완료** -- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~**W3 done** · 다음 W4(Web Push, Master VAPID) +- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W3·**W5~W7 done** · **W4 Web Push만 Master VAPID 대기** - 후순위: **N4 Android FCM/UI** — Master 시크릿·실기기 - UI: **iMessage-inspired light default** + soft charcoal dark 프로덕션 반영 - **Track C 콘텐츠 갭 A~F**: 프로덕션 반영 완료 (`95422bb`대) @@ -217,10 +217,10 @@ 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) | todo | Master: Firebase Web config · VAPID | -| **N4-W5** | PWA | todo | manifest 테마 정렬 · 셸 캐시만 | -| **N4-W6** | 테스터 가이드 | todo | `tester-guide.md` 웹 절 | -| **N4-W7** | 품질 (Playwright 등) | todo | 스모크 + 부록 수동 QA | +| **N4-W4** | Web Push (FCM + VAPID) | todo | **Master 블로커:** Firebase Web config · VAPID | +| **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는 후속) | ### FCM (Android) — [`fcm-setup.md`](./fcm-setup.md) @@ -290,8 +290,8 @@ N1~N4(배포·품질에 필요한 최소분) 이후에만 착수. `roadmap.md` P 확인 필요, 그때까지는 인앱 배지·배너가 실질적 대체 경로. Track C A~F 프로덕션 재배포는 **완료** (`95422bb` / docs `f363e8e`, 2026-08-03). -**바로 다음 (웹):** [`web-upgrade.md`](./web-upgrade.md) **W4** Web Push — Master Firebase Web/VAPID 필요. -W0~W3 **done**. Android는 **N4-1/3 → N4-4 → N4-5~10** 후순위 병행. +**바로 다음:** 웹 **W4** (Master: Firebase Web + VAPID) 또는 Android N4-1/3. +웹 W0~W3·W5~W7 **done**. 완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다. diff --git a/docs/roadmap.md b/docs/roadmap.md index ed58d2a..9cdaf92 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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 -- [ ] **W4** Web Push (FCM + VAPID) — Master Firebase Web 설정 -- [ ] **W5** PWA (manifest 테마 · 셸 캐시) -- [ ] **W6** 테스터 가이드 -- [ ] **W7** Playwright 등 품질 스모크 +- [ ] **W4** Web Push (FCM + VAPID) — **Master Firebase Web/VAPID 대기** +- [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는 후속 -구현은 **Wi 단위 Master 승인 후**. Android N4 실기기 트랙과 일정 충돌 시 서버 SA만 공유. +구현은 Wi 단위. W4만 Master 시크릿 블로커. Android N4와 서버 SA 공유. ## Phase 2 — L3 확장 + 베타 확대 diff --git a/docs/tester-guide.md b/docs/tester-guide.md index 81f0e62..ad50f55 100644 --- a/docs/tester-guide.md +++ b/docs/tester-guide.md @@ -2,25 +2,37 @@ ## 접속 -- 웹: **https://msn.iykyka.com** +- 웹: **https://msn.iykyka.com** (데모/프리뷰 1차 표면) +- 권장 브라우저: **Desktop Chrome** - 공용 데모 초대 코드: **`DEMO-YKAVU`** -- 표시 이름 예시: `테스터` (원하는 이름으로 변경 가능) +- 표시 이름 예시: `테스터` (원하는 이름으로 변경 가능 — **이미 가입** 탭 재로그인 시 같은 이름 필요) 같은 코드를 여러 명이 쓸 수 있습니다 (`ALLOW_DEMO_INVITE=1`). API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포함. +### 웹 프리뷰 팁 (N4-W) + +1. **넓은 화면(≥900px)** — 왼쪽 대화 목록 · 오른쪽 채팅이 나란히 보입니다. +2. **내 ID 복사** — 목록 상단 칩 또는 설정 → 계정 행을 탭합니다. +3. **브라우저 알림** — 설정 → 「브라우저 알림」에서 허용하면 답장 마감 리마인드를 받을 수 있습니다(거부해도 인앱 배지는 동작). +4. **홈 화면 추가(PWA)** — Chrome 주소창 옆 설치 아이콘 또는 메뉴 → 「앱 설치」. +5. **로그아웃** — 설정 → 로그아웃 → 입장 화면 「이미 가입」으로 복귀. +6. **캐시가 옛 버전일 때** — 강력 새로고침, 또는 DevTools → Application → Service Workers → Unregister 후 새로고침. + +상세 설계: [`web-upgrade.md`](./web-upgrade.md). + ## 권장 페어링 플로우 (5~10분) 두 명(또는 시크릿 창 두 개)으로 진행합니다. **대화는 닉네임이 아니라 숫자 사용자 ID로 연결됩니다.** -1. **가입** — 초대 코드 `DEMO-YKAVU` 입력 (각자 다른 표시 이름 권장) -2. **말투 샘플** — 몇 줄 적거나 스킵 +1. **가입** — 「새로 가입」탭 · 초대 코드 `DEMO-YKAVU` (각자 다른 표시 이름 권장). 시크릿 창 두 개로 두 사용자 가능. +2. **말투 샘플** — 몇 줄 적거나 스킵 (하드 리프레시 후에도 유지됨) 3. **내 ID 복사** — 대화 목록 상단「내 사용자 ID」칩을 탭해 복사하고 상대에게 전달 4. **연락처 추가** — 상대 표시 이름 + **상대의 숫자 ID(필수)** → 추가 → 「대화」 -5. **메시지** — 사람 모드로 한두 줄 주고받기 -6. **와카뷰 초안** — 메뉴 → 자율성에서 **L1** → 초안이 뜨면 수정/승인하고 보내기 -7. (선택) L0에서는「입력창으로 옮기기」만 됩니다. L2·거부권·사후알림 함도 눌러 보세요. +5. **메시지** — 사람 모드로 한두 줄 주고받기 · 채팅 앱바의 「실시간」상태 확인 +6. **와카뷰 초안** — 설정 → 자율성에서 **L1** → 채팅에서 ✨ 초안 → 수정/승인하고 보내기 +7. (선택) L0에서는「입력창으로 옮기기」만 됩니다. L2·거부권·사후알림 함·이따 답장도 눌러 보세요. ### ID 없는 옛 연락처 @@ -30,7 +42,7 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포 - **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다. - **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송. -- 푸시(FCM): 코드 경로는 준비됨. Master가 Firebase/`FCM_SERVER_KEY`를 넣기 전에는 플레이스홀더(`docs/fcm-setup.md`). +- 푸시: Android·Web Push는 Master Firebase 설정 후 (`docs/fcm-setup.md`, `web-upgrade` W4). 그전엔 `install:` 플레이스홀더. - 문제/스크린샷은 Master에게 전달해 주세요. - 민감 정보·실명 대화는 베타 특성상 최소화해 주세요. diff --git a/docs/web-upgrade.md b/docs/web-upgrade.md index ed52125..fde3c98 100644 --- a/docs/web-upgrade.md +++ b/docs/web-upgrade.md @@ -255,7 +255,7 @@ flowchart LR | | | |--|--| -| **Status** | todo (Master Firebase Web 설정 의존) | +| **Status** | todo (**Master Firebase Web / VAPID 블로커** — W5~W7은 선행 진행 가능) | | **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 | | **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 | @@ -315,7 +315,7 @@ flowchart LR | | | |--|--| -| **Status** | todo | +| **Status** | **done** (2026-08-04) — W4와 독립 선행 | | **목표** | 홈 화면 추가·standalone으로 데모 가능; 오프라인은 **셸만** | | **비범위** | 오프라인 메시지 작성/전송, 풀 오프라인 메신저 | @@ -337,16 +337,17 @@ flowchart LR ### 작업 분해 -- [ ] `manifest.json` 색·description·`orientation` 재검토 (데모는 portrait+landscape 허용 검토) -- [ ] `index.html` meta / apple-touch / title 정리 -- [ ] 배포 후 SW 캐시로 구버전 고착 시: nginx/`Cache-Control` · Flutter 버전 해시 절차를 ops에 한 절 -- [ ] (선택) 인앱 “홈 화면에 추가” 안내 (beforeinstallprompt는 브라우저별) +- [x] `manifest.json` theme `#007AFF` / background `#F2F2F7` · orientation `any` +- [x] `index.html` description / theme-color / apple meta +- [x] nginx: `index.html` · `flutter_service_worker.js` · `manifest.json` → `Cache-Control: no-cache` +- [x] `OfflineBanner` (navigator.onLine) +- [ ] Chrome Application 패널 수동 확인 (배포 후) ### 수락 기준 -- [ ] Chrome Lighthouse/Application 패널에서 manifest 오류 없음 -- [ ] standalone 창에서 로그인 세션·채팅 시연 가능 -- [ ] 네트워크 오프라인 시 “연결 필요” 인지 (조용한 실패 금지) +- [x] manifest·index·캐시 헤더 코드 반영 +- [x] 오프라인 시 배너 문구 +- [ ] standalone 설치 시연 (배포 후 Master/테스터) ### Master 액션 @@ -370,23 +371,19 @@ flowchart LR | | | |--|--| -| **Status** | todo | +| **Status** | **done** (2026-08-04) | | **목표** | 웹 데모 스크립트가 설치·알림·페어링·로그아웃까지 재현 가능 | | **비범위** | PoC 인터뷰 본실행, SHARE_URL(별도 N4-13) | ### 작업 분해 -- [ ] [`tester-guide.md`](./tester-guide.md)에 절 추가: - - 권장 브라우저: **Desktop Chrome** - - 시크릿/다른 프로필 두 명 페어링 - - 알림 허용 (W3/W4) - - PWA 설치 (W5) - - 설정 → 로그아웃 → 「이미 가입」 재로그인 (Q8) -- [ ] 데모 실패 시 런북: SW unregister, prefs 클리어, `/admin/push-test` 해석 +- [x] [`tester-guide.md`](./tester-guide.md) 웹 프리뷰 팁 · 페어링·캐시 런북 +- [x] SW unregister / 강력 새로고침 안내 ### 수락 기준 -- [ ] 신규 테스터가 가이드만으로 페어링·초안 시연 가능 (Master 또는 대리 1회 walkthrough) +- [x] 가이드에 Chrome·PWA·알림·로그아웃 경로 명시 +- [ ] Master walkthrough 1회 (운영) ### Master 액션 @@ -403,21 +400,20 @@ flowchart LR | | | |--|--| -| **Status** | todo | +| **Status** | **done** (2026-08-04, edge smoke) — Playwright UI는 후속 선택 | | **목표** | 웹 회귀를 자동화·수동 체크리스트로 고정 | | **비범위** | Android 에뮬레이터 UI 탭(N4-5~10), Q9 테스트 | ### 작업 분해 -- [ ] Playwright(또는 동등) 스모크 스크립트 위치 예: `scripts/e2e_web_smoke.py` / `mobile/test_driver/` — **한 도구로 고정**해 구현 시 선택 - - 커버: 입장 탭(가입/로그인) 렌더 · (가능 시) DEMO 로그인 · 대화 목록 셸 · 설정 진입 -- [ ] 부록 수동 QA (아래 §부록 A) 실행 기록란 -- [ ] CI 연동은 선택(시크릿·라이브 의존) — 로컬/`msn` 수동 실행을 1차로 +- [x] [`scripts/e2e_web_smoke.sh`](../scripts/e2e_web_smoke.sh) — health/demo/manifest/login-400 +- [x] 부록 A 수동 QA 표 유지 +- [ ] Playwright 브라우저 UI 스모크 — 선택 후속 (에이전트 환경 제약) ### 수락 기준 -- [ ] 스모크가 문서화된 명령 한 줄로 실행됨 -- [ ] W1~W5 수락 기준이 부록 체크와 연결됨 +- [x] `./scripts/e2e_web_smoke.sh` 한 줄 실행 +- [x] 부록 A가 W1~W5와 연결됨 ### Master 액션 diff --git a/mobile/lib/screens/conversation_list_screen.dart b/mobile/lib/screens/conversation_list_screen.dart index e3694ca..b164ffc 100644 --- a/mobile/lib/screens/conversation_list_screen.dart +++ b/mobile/lib/screens/conversation_list_screen.dart @@ -7,6 +7,7 @@ import '../services/snooze_service.dart'; import '../state/session_state.dart'; import '../widgets/gradient_text.dart'; import '../widgets/my_user_id_chip.dart'; +import '../widgets/offline_banner.dart'; import '../widgets/web_preview_banner.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -343,6 +344,7 @@ class _ConversationListScreenState extends State ), body: Column( children: [ + const OfflineBanner(), const WebPreviewBanner(), Expanded( child: RefreshIndicator( diff --git a/mobile/lib/widgets/offline_banner.dart b/mobile/lib/widgets/offline_banner.dart new file mode 100644 index 0000000..656ffef --- /dev/null +++ b/mobile/lib/widgets/offline_banner.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'web_connectivity.dart'; + +/// Thin bar when the browser reports offline (docs/web-upgrade.md N4-W5). +/// API/WS still require a network — this only sets expectation. +class OfflineBanner extends StatefulWidget { + const OfflineBanner({super.key}); + + @override + State createState() => _OfflineBannerState(); +} + +class _OfflineBannerState extends State { + bool _offline = false; + StreamSubscription? _onlineSub; + StreamSubscription? _offlineSub; + + @override + void initState() { + super.initState(); + if (!kIsWeb) return; + _offline = webNavigatorOffline; + _onlineSub = listenWebOnline(() { + if (mounted) setState(() => _offline = false); + }); + _offlineSub = listenWebOffline(() { + if (mounted) setState(() => _offline = true); + }); + } + + @override + void dispose() { + _onlineSub?.cancel(); + _offlineSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!kIsWeb || !_offline) return const SizedBox.shrink(); + final theme = Theme.of(context); + return Material( + color: theme.colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + '오프라인입니다. 메시지·로그인에는 인터넷 연결이 필요합니다.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onErrorContainer, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/web_connectivity.dart b/mobile/lib/widgets/web_connectivity.dart new file mode 100644 index 0000000..e0da296 --- /dev/null +++ b/mobile/lib/widgets/web_connectivity.dart @@ -0,0 +1,2 @@ +export 'web_connectivity_stub.dart' + if (dart.library.html) 'web_connectivity_web.dart'; diff --git a/mobile/lib/widgets/web_connectivity_stub.dart b/mobile/lib/widgets/web_connectivity_stub.dart new file mode 100644 index 0000000..74e47be --- /dev/null +++ b/mobile/lib/widgets/web_connectivity_stub.dart @@ -0,0 +1,7 @@ +import 'dart:async'; + +bool get webNavigatorOffline => false; + +StreamSubscription? listenWebOnline(void Function() onOnline) => null; + +StreamSubscription? listenWebOffline(void Function() onOffline) => null; diff --git a/mobile/lib/widgets/web_connectivity_web.dart b/mobile/lib/widgets/web_connectivity_web.dart new file mode 100644 index 0000000..d337129 --- /dev/null +++ b/mobile/lib/widgets/web_connectivity_web.dart @@ -0,0 +1,13 @@ +import 'dart:async'; +// ignore: avoid_web_libraries_in_flutter, deprecated_member_use +import 'dart:html' as html; + +bool get webNavigatorOffline => !(html.window.navigator.onLine ?? true); + +StreamSubscription? listenWebOnline(void Function() onOnline) { + return html.window.onOnline.listen((_) => onOnline()); +} + +StreamSubscription? listenWebOffline(void Function() onOffline) { + return html.window.onOffline.listen((_) => onOffline()); +} diff --git a/mobile/web/index.html b/mobile/web/index.html index aaa9fa0..27acc82 100644 --- a/mobile/web/index.html +++ b/mobile/web/index.html @@ -1,32 +1,20 @@ - - + + - + - - + + + - 와카뷰 (Ykavu) diff --git a/mobile/web/manifest.json b/mobile/web/manifest.json index 68e32ae..3c40ff5 100644 --- a/mobile/web/manifest.json +++ b/mobile/web/manifest.json @@ -3,10 +3,10 @@ "short_name": "와카뷰", "start_url": ".", "display": "standalone", - "background_color": "#7C6FF0", - "theme_color": "#7C6FF0", + "background_color": "#F2F2F7", + "theme_color": "#007AFF", "description": "나를 대신해 답하는, 나만의 와카뷰", - "orientation": "portrait-primary", + "orientation": "any", "prefer_related_applications": false, "icons": [ { diff --git a/scripts/e2e_web_smoke.sh b/scripts/e2e_web_smoke.sh new file mode 100755 index 0000000..716b275 --- /dev/null +++ b/scripts/e2e_web_smoke.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# N4-W7 — minimal live smoke for the web edge (no browser automation). +# Usage: ./scripts/e2e_web_smoke.sh [BASE_URL] +set -euo pipefail +BASE="${1:-https://msn.iykyka.com}" + +echo "==> GET $BASE/health" +curl -fsS "$BASE/health" +echo + +echo "==> GET $BASE/demo" +curl -fsS "$BASE/demo" | head -c 400 +echo + +echo "==> GET $BASE/manifest.json (PWA)" +curl -fsS "$BASE/manifest.json" | head -c 300 +echo + +echo "==> POST /auth/login DEMO without display_name (expect 400)" +code=$(curl -sS -o /tmp/ykavu_login.json -w '%{http_code}' \ + -X POST "$BASE/auth/login" \ + -H 'Content-Type: application/json' \ + -d '{"invite_code":"DEMO-YKAVU"}') +echo "HTTP $code $(head -c 120 /tmp/ykavu_login.json)" +test "$code" = "400" + +echo "OK web edge smoke ($BASE)"