Compare commits
2 Commits
4a38231656
...
4aa21a6099
| Author | SHA1 | Date |
|---|---|---|
|
|
4aa21a6099 | |
|
|
40c36a20c3 |
12
.env.example
12
.env.example
|
|
@ -27,3 +27,15 @@ PUBLIC_API_BASE=https://msn.iykyka.com
|
|||
|
||||
# Host port mapped to web:80
|
||||
WEB_HOST_PORT=8088
|
||||
|
||||
# --- N4-W4 Web Push (Flutter web build-args / dart-define; never commit real values) ---
|
||||
# Firebase Console → Project settings → Your apps → Web app (msn.iykyka.com)
|
||||
# + Cloud Messaging → Web Push certificates (VAPID key pair).
|
||||
# Leave empty to keep install: placeholder tokens until secrets are ready.
|
||||
FIREBASE_API_KEY=
|
||||
FIREBASE_AUTH_DOMAIN=
|
||||
FIREBASE_PROJECT_ID=
|
||||
FIREBASE_STORAGE_BUCKET=
|
||||
FIREBASE_MESSAGING_SENDER_ID=
|
||||
FIREBASE_APP_ID=
|
||||
FIREBASE_VAPID_KEY=
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
| [`docs/deploy-portainer.md`](./docs/deploy-portainer.md) | Portainer·컷오버 |
|
||||
| [`docs/ops-backup.md`](./docs/ops-backup.md) | 백업 |
|
||||
| [`docs/invite-ops.md`](./docs/invite-ops.md) | 초대 코드 운영 |
|
||||
| [`docs/fcm-setup.md`](./docs/fcm-setup.md) | FCM Android N4-1~4 · Web Push는 `web-upgrade` W4 |
|
||||
| [`docs/fcm-setup.md`](./docs/fcm-setup.md) | FCM Android N4-1~4 · Web Push(W4) dart-define 주입 |
|
||||
| [`docs/android-release.md`](./docs/android-release.md) | 내부 release APK 서명 |
|
||||
| [`docs/tester-guide.md`](./docs/tester-guide.md) | 테스터 안내 · `DEMO-YKAVU` |
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ server {
|
|||
add_header Cache-Control "no-cache";
|
||||
try_files $uri =404;
|
||||
}
|
||||
location = /firebase-messaging-sw.js {
|
||||
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 / {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ services:
|
|||
dockerfile: mobile/Dockerfile
|
||||
args:
|
||||
CORE_API_BASE: ${PUBLIC_API_BASE:-https://msn.iykyka.com}
|
||||
# N4-W4 Web Push — set in host .env (never commit). Empty → install: fallback.
|
||||
FIREBASE_API_KEY: ${FIREBASE_API_KEY:-}
|
||||
FIREBASE_AUTH_DOMAIN: ${FIREBASE_AUTH_DOMAIN:-}
|
||||
FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID:-}
|
||||
FIREBASE_STORAGE_BUCKET: ${FIREBASE_STORAGE_BUCKET:-}
|
||||
FIREBASE_MESSAGING_SENDER_ID: ${FIREBASE_MESSAGING_SENDER_ID:-}
|
||||
FIREBASE_APP_ID: ${FIREBASE_APP_ID:-}
|
||||
FIREBASE_VAPID_KEY: ${FIREBASE_VAPID_KEY:-}
|
||||
ports:
|
||||
- "${WEB_HOST_PORT:-8088}:80"
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -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·**W5~W7 done** · **W4 Web Push만 Master VAPID 대기**
|
||||
- **웹 고도화:** [`web-upgrade.md`](./web-upgrade.md) — W0~W3·W5~W7 **done** · **W4 code shipped** (live: Master `.env`에 FIREBASE_*/VAPID 후 `web` 재빌드)
|
||||
- 후순위: **N4 Android FCM/UI** — Master 시크릿·실기기
|
||||
- UI: **iMessage-inspired light default** + soft charcoal dark 프로덕션 반영
|
||||
- **Track C 콘텐츠 갭 A~F**: 프로덕션 반영 완료 (`95422bb`대)
|
||||
|
|
@ -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) | todo | **Master 블로커:** Firebase Web config · VAPID |
|
||||
| **N4-W4** | Web Push (FCM + VAPID) | **code** (2026-08-04) | dart-define+SW shipped · **live**는 Master `.env` FIREBASE_*/VAPID + `web` rebuild |
|
||||
| **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는 후속) |
|
||||
|
|
@ -290,8 +290,8 @@ N1~N4(배포·품질에 필요한 최소분) 이후에만 착수. `roadmap.md` P
|
|||
확인 필요, 그때까지는 인앱 배지·배너가 실질적 대체 경로. Track C A~F 프로덕션
|
||||
재배포는 **완료** (`95422bb` / docs `f363e8e`, 2026-08-03).
|
||||
|
||||
**바로 다음:** 웹 **W4** (Master: Firebase Web + VAPID) 또는 Android N4-1/3.
|
||||
웹 W0~W3·W5~W7 **done**.
|
||||
**바로 다음:** 웹 **W4 live** — Master가 Firebase Web config + VAPID를 서버 `.env`에 넣고 `docker compose up -d --build web` · 또는 Android N4-1/3.
|
||||
웹 W0~W7 코드 경로 **done** (W4 live accept만 시크릿 대기).
|
||||
|
||||
|
||||
완료 시 본 표의 Status를 `done`으로 바꾸고, [`roadmap.md`](./roadmap.md) §4/§5의 대응 `[~]`/`[ ]`도 같이 갱신한다.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# FCM 설정 (N4-1 ~ N4-4)
|
||||
# FCM 설정 (N4-1 ~ N4-4 · N4-W4)
|
||||
|
||||
서버·클라이언트의 푸시 **코드 경로는 준비됨**.
|
||||
새 Firebase 프로젝트는 **레거시 서버 키가 비활성**인 경우가 많아, **HTTP v1 + 서비스 계정 JSON**을 쓴다.
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
|------|------|
|
||||
| Flutter | `PushTokenService` — Firebase 가능하면 실 FCM 토큰, 아니면 `install:` 플레이스홀더 |
|
||||
| Android | `google-services.json`이 있을 때만 Google Services 플러그인 적용 |
|
||||
| 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가 할 일
|
||||
|
|
@ -73,20 +74,55 @@ curl -sS -X POST https://msn.iykyka.com/admin/push-test \
|
|||
`only_placeholder_tokens` → Android에 `google-services.json` 넣고 재설치.
|
||||
`fcm_not_configured` → 서버에 서비스 계정 파일 경로 확인.
|
||||
|
||||
## Web
|
||||
## Web (N4-W4)
|
||||
|
||||
Web Push(FCM Web + VAPID)는 **웹 고도화 트랙 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)은 공유하고,
|
||||
Android N4(실기기)와 **병행 가능**. 서버 서비스 계정(HTTP v1)은 공유하고,
|
||||
클라이언트는 `google-services.json`(Android) vs Firebase Web config + VAPID(Web)로 분리.
|
||||
- 코드 as-is: `PushTokenService`가 `kIsWeb`이면 토큰을 건너뛰고 `install:`만 등록 —
|
||||
W4 구현 전까지 웹 `/admin/push-test`는 `only_placeholder_tokens`가 정상.
|
||||
|
||||
### W4 Master 준비물 (요약)
|
||||
**코드 shipped.** define이 비어 있으면 웹은 계속 `install:`만 등록 → `/admin/push-test`의 `only_placeholder_tokens`는 정상.
|
||||
|
||||
1. Firebase Console → Web 앱 (`msn.iykyka.com`)
|
||||
2. Cloud Messaging → Web Push 인증서 / **VAPID key**
|
||||
3. 빌드 시크릿으로 config 주입 (git 금지) — 상세는 `web-upgrade.md` 부록 B
|
||||
4. 스모크: 실 웹 토큰 등록 후 `POST /admin/push-test` → `sent >= 1`
|
||||
### Master 준비물 (live accept)
|
||||
|
||||
1. Firebase Console → **Web** 앱 추가 (authorized domain: `msn.iykyka.com`) — SA 프로젝트와 동일
|
||||
2. Cloud Messaging → Web Push certificates / **VAPID public key**
|
||||
3. 배포 호스트 `~/project/ykavu/.env`에 기입 (`.env.example` 키 이름 — **값 git 금지**):
|
||||
|
||||
```bash
|
||||
FIREBASE_API_KEY=
|
||||
FIREBASE_AUTH_DOMAIN= # 비우면 {projectId}.firebaseapp.com
|
||||
FIREBASE_PROJECT_ID=
|
||||
FIREBASE_STORAGE_BUCKET= # 비우면 {projectId}.appspot.com
|
||||
FIREBASE_MESSAGING_SENDER_ID=
|
||||
FIREBASE_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
|
||||
cd mobile
|
||||
flutter run -d chrome \
|
||||
--dart-define=CORE_API_BASE=http://localhost:8080 \
|
||||
--dart-define=FIREBASE_API_KEY=... \
|
||||
--dart-define=FIREBASE_APP_ID=... \
|
||||
--dart-define=FIREBASE_MESSAGING_SENDER_ID=... \
|
||||
--dart-define=FIREBASE_PROJECT_ID=... \
|
||||
--dart-define=FIREBASE_VAPID_KEY=...
|
||||
```
|
||||
|
||||
### 구현 위치
|
||||
|
||||
- `mobile/lib/config/firebase_web_config.dart`
|
||||
- `mobile/lib/services/push_token_service.dart` (`_tryFirebaseWebToken`)
|
||||
- `mobile/web/firebase-messaging-sw.js` (+ `.template`, Dockerfile 치환)
|
||||
- `mobile/Dockerfile` / `docker-compose.yml` build-args
|
||||
|
|
|
|||
|
|
@ -417,7 +417,7 @@ 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/VAPID 대기**
|
||||
- [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는 후속
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ API 메타: `GET https://msn.iykyka.com/demo` — `pairing_steps` / `notes` 포
|
|||
|
||||
- **초안(AI)**: Gemini 키가 서버에 설정되어 있어 **실제 초안**이 생성됩니다.
|
||||
- **L0(비서)**: 와카뷰 발송이 서버에서 막혀 있습니다. 초안 → 입력창 → 직접 전송.
|
||||
- 푸시: Android·Web Push는 Master Firebase 설정 후 (`docs/fcm-setup.md`, `web-upgrade` W4). 그전엔 `install:` 플레이스홀더.
|
||||
- 푸시: Android·Web Push는 Master Firebase 설정 후 (`docs/fcm-setup.md`, `web-upgrade` W4).
|
||||
웹은 서버 `.env`에 `FIREBASE_*`/`VAPID`가 있고 `web` 이미지를 그 값으로 빌드한 뒤에야 실 토큰이 등록됩니다. 그전엔 `install:` 플레이스홀더.
|
||||
Web Push 배포 직후엔 **하드 리프레시**(또는 사이트 데이터 삭제)로 `firebase-messaging-sw.js`를 갱신하세요.
|
||||
- 문제/스크린샷은 Master에게 전달해 주세요.
|
||||
- 민감 정보·실명 대화는 베타 특성상 최소화해 주세요.
|
||||
|
||||
|
|
|
|||
|
|
@ -255,59 +255,56 @@ flowchart LR
|
|||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Status** | todo (**Master Firebase Web / VAPID 블로커** — W5~W7은 선행 진행 가능) |
|
||||
| **Status** | **code shipped** (2026-08-04) — **live accept**는 Master Firebase Web config + VAPID를 서버 `.env`에 넣고 `web` 재빌드 후 |
|
||||
| **목표** | 웹 탭이 백그라운드여도 에스컬레이션 등 서버 `notifyUser`가 **실 FCM 웹 토큰**으로 전달 |
|
||||
| **비범위** | 레거시 `FCM_SERVER_KEY` 신규 의존, 커스텀 푸시 프로토콜 |
|
||||
|
||||
### As-is
|
||||
### As-is → shipped
|
||||
|
||||
- `_tryFirebaseToken()`이 `kIsWeb`이면 즉시 `null`
|
||||
- DB device_tokens는 `install:…` / `platform=web`만 → `only_placeholder_tokens`
|
||||
- `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
|
||||
|
||||
### To-be
|
||||
### Master 액션 (live accept 블로커)
|
||||
|
||||
- Firebase **Web** 앱 + Messaging + **VAPID 키**
|
||||
- `Firebase.initializeApp(options: …)` + `FirebaseMessaging.instance.getToken(vapidKey: …)`
|
||||
- 성공 시 `platform=web` **실토큰** 등록 (서버 v1 전송 경로 재사용)
|
||||
- 실패 시 기존처럼 `install:` soft-fallback (앱 기동 막지 않음)
|
||||
|
||||
### Master 액션 (블로커)
|
||||
|
||||
1. Firebase Console에 Web 앱 추가 (`msn.iykyka.com`)
|
||||
1. Firebase Console에 Web 앱 추가 (`msn.iykyka.com`) — 프로젝트 `iykyka` SA와 동일 프로젝트
|
||||
2. Cloud Messaging Web Push 인증서/VAPID 키 발급
|
||||
3. Web config (`apiKey`, `authDomain`, `projectId`, `messagingSenderId`, `appId`, `vapidKey`)를 **빌드 시크릿**으로 전달 (git 금지)
|
||||
4. (권장) `firebase-messaging-sw.js` 요구사항 확인 — FlutterFire 문서에 맞게 `web/`에 스켈레톤
|
||||
3. 서버 `~/project/ykavu/.env`에 `FIREBASE_API_KEY` … `FIREBASE_VAPID_KEY` 기입 (git 금지)
|
||||
4. `docker compose up -d --build web` 후 하드 리프레시 → 로그인 → `/admin/push-test`
|
||||
|
||||
### 작업 분해
|
||||
|
||||
- [ ] 빌드 주입 방식 확정: `--dart-define=FIREBASE_…` 및/또는 생성 `firebase_options_web.dart` (gitignore)
|
||||
- [ ] `PushTokenService._tryFirebaseToken` 웹 분기 구현 + permission
|
||||
- [ ] Docker web 빌드에 define 전달 (`mobile/Dockerfile` / compose)
|
||||
- [ ] `/admin/push-test`로 user 지정 → Chrome 백그라운드 수신
|
||||
- [ ] `docs/fcm-setup.md` Web 절을 본 W4와 동기화
|
||||
- [ ] `DataFlowScreen` 푸시 문구 갱신
|
||||
- [x] 빌드 주입: `--dart-define=FIREBASE_…` + SW template 치환 (gitignore/시크릿 미커밋)
|
||||
- [x] `PushTokenService` 웹 분기 + permission
|
||||
- [x] Docker web 빌드에 define 전달 (`mobile/Dockerfile` / compose)
|
||||
- [ ] `/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`, 브라우저 알림 표시
|
||||
- [ ] 시크릿이 git history에 없음
|
||||
- [ ] device_tokens에 `install:`이 아닌 토큰 + `platform=web` (**시크릿 후**)
|
||||
- [ ] `POST /admin/push-test` → `sent >= 1`, 브라우저 알림 표시 (**시크릿 후**)
|
||||
- [x] 시크릿이 git history에 없음 (define/env만)
|
||||
|
||||
### 위험
|
||||
|
||||
- SW 등록 순서/캐시로 토큰 null → 배포 후 hard refresh 절차를 W6에 적음
|
||||
- SW 등록 순서/캐시로 토큰 null → 배포 후 hard refresh 절차를 W6/tester-guide에 적음
|
||||
- Android `google-services.json`과 Web config 혼동 → 문서에서 경로 분리
|
||||
|
||||
### 예상 터치 파일
|
||||
### 터치 파일
|
||||
|
||||
- `mobile/lib/config/firebase_web_config.dart`
|
||||
- `mobile/lib/services/push_token_service.dart`
|
||||
- `mobile/web/` (messaging SW, index 훅)
|
||||
- `mobile/Dockerfile` / `.env.example` (define 이름만)
|
||||
- `mobile/Dockerfile` / `docker-compose.yml` / `.env.example`
|
||||
- `docs/fcm-setup.md`, `docs/web-upgrade.md`
|
||||
|
||||
### 서버
|
||||
|
||||
- **코드 변경 최소.** 기존 `notifyUser` / HTTP v1. 웹 토큰도 동일 messages:send
|
||||
- **코어 코드 변경 없음.** 기존 `notifyUser` / HTTP v1. 웹 토큰도 동일 messages:send
|
||||
- 클라이언트 시크릿만 host `.env` → compose build-args
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -473,7 +470,7 @@ Wi를 끝낼 때마다:
|
|||
|
||||
- [ ] Firebase Web app 등록 (도메인 `msn.iykyka.com`)
|
||||
- [ ] VAPID key
|
||||
- [ ] 빌드 환경에 define/options 주입 (git 미포함)
|
||||
- [ ] 서버 `~/project/ykavu/.env`에 `FIREBASE_*` / `FIREBASE_VAPID_KEY` (git 미포함) → `docker compose up -d --build web`
|
||||
- [ ] 서버 `secrets/firebase-service-account.json` 유지 (Android와 공유)
|
||||
- [ ] push-test로 `sent >= 1` 확인
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,53 @@
|
|||
# Flutter Web → nginx. Build from repository root:
|
||||
# docker build -f mobile/Dockerfile --build-arg CORE_API_BASE=https://msn.iykyka.com .
|
||||
# Optional Web Push (N4-W4) — pass FIREBASE_* / FIREBASE_VAPID_KEY as build-args
|
||||
# (never commit real values; see .env.example and docs/fcm-setup.md).
|
||||
ARG FLUTTER_IMAGE=ghcr.io/cirruslabs/flutter:3.32.7
|
||||
FROM ${FLUTTER_IMAGE} AS build
|
||||
|
||||
WORKDIR /app
|
||||
ARG CORE_API_BASE=https://msn.iykyka.com
|
||||
ARG FIREBASE_API_KEY=
|
||||
ARG FIREBASE_AUTH_DOMAIN=
|
||||
ARG FIREBASE_PROJECT_ID=
|
||||
ARG FIREBASE_STORAGE_BUCKET=
|
||||
ARG FIREBASE_MESSAGING_SENDER_ID=
|
||||
ARG FIREBASE_APP_ID=
|
||||
ARG FIREBASE_VAPID_KEY=
|
||||
|
||||
COPY mobile/pubspec.yaml mobile/pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
|
||||
COPY mobile/ .
|
||||
|
||||
# Populate firebase-messaging-sw.js when Web config is present; keep stub otherwise.
|
||||
RUN set -eu; \
|
||||
if [ -n "${FIREBASE_API_KEY}" ] && [ -n "${FIREBASE_APP_ID}" ] && \
|
||||
[ -n "${FIREBASE_MESSAGING_SENDER_ID}" ] && [ -n "${FIREBASE_PROJECT_ID}" ]; then \
|
||||
AUTH_DOMAIN="${FIREBASE_AUTH_DOMAIN:-${FIREBASE_PROJECT_ID}.firebaseapp.com}"; \
|
||||
STORAGE_BUCKET="${FIREBASE_STORAGE_BUCKET:-${FIREBASE_PROJECT_ID}.appspot.com}"; \
|
||||
sed \
|
||||
-e "s|__FIREBASE_API_KEY__|${FIREBASE_API_KEY}|g" \
|
||||
-e "s|__FIREBASE_AUTH_DOMAIN__|${AUTH_DOMAIN}|g" \
|
||||
-e "s|__FIREBASE_PROJECT_ID__|${FIREBASE_PROJECT_ID}|g" \
|
||||
-e "s|__FIREBASE_STORAGE_BUCKET__|${STORAGE_BUCKET}|g" \
|
||||
-e "s|__FIREBASE_MESSAGING_SENDER_ID__|${FIREBASE_MESSAGING_SENDER_ID}|g" \
|
||||
-e "s|__FIREBASE_APP_ID__|${FIREBASE_APP_ID}|g" \
|
||||
web/firebase-messaging-sw.js.template > web/firebase-messaging-sw.js; \
|
||||
echo "firebase-messaging-sw.js: populated for project ${FIREBASE_PROJECT_ID}"; \
|
||||
else \
|
||||
echo "firebase-messaging-sw.js: stub (FIREBASE_* build-args empty)"; \
|
||||
fi
|
||||
|
||||
RUN flutter build web --release \
|
||||
--dart-define=CORE_API_BASE=${CORE_API_BASE}
|
||||
--dart-define=CORE_API_BASE=${CORE_API_BASE} \
|
||||
--dart-define=FIREBASE_API_KEY=${FIREBASE_API_KEY} \
|
||||
--dart-define=FIREBASE_AUTH_DOMAIN=${FIREBASE_AUTH_DOMAIN} \
|
||||
--dart-define=FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID} \
|
||||
--dart-define=FIREBASE_STORAGE_BUCKET=${FIREBASE_STORAGE_BUCKET} \
|
||||
--dart-define=FIREBASE_MESSAGING_SENDER_ID=${FIREBASE_MESSAGING_SENDER_ID} \
|
||||
--dart-define=FIREBASE_APP_ID=${FIREBASE_APP_ID} \
|
||||
--dart-define=FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY}
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
COPY --from=build /app/build/web /usr/share/nginx/html
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Optional: serve a host-built Flutter web tree (faster CI/agent smoke).
|
||||
# cd mobile && flutter build web --release --dart-define=CORE_API_BASE=https://msn.iykyka.com
|
||||
# cd mobile && flutter build web --release --dart-define=CORE_API_BASE=https://msn.iykyka.com \
|
||||
# # optional W4: --dart-define=FIREBASE_API_KEY=... --dart-define=FIREBASE_VAPID_KEY=...
|
||||
# docker build -f mobile/Dockerfile.prebuilt -t ykavu-web .
|
||||
# Production/Portainer should prefer mobile/Dockerfile (multi-stage flutter build).
|
||||
FROM nginx:1.27-alpine
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ sudo apt-get install -y libsqlite3-dev libsqlcipher1
|
|||
|
||||
- 로컬 DB: `lib/db/app_database_web.dart` — **W1 done**: SharedPreferences 지속성(비암호화 프리뷰)
|
||||
- 스누즈 OS 알림: 웹 no-op → **W3** Notification API / 인앱
|
||||
- 푸시: 웹에서 FCM skip → **W4** Web Push + VAPID ([`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: `web/manifest.json` 골격 있음 → **W5** 테마·설치·셸 캐시
|
||||
- **Q9**(수신 자동응대)는 웹 트랙 비범위
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:firebase_core/firebase_core.dart';
|
||||
|
||||
/// Firebase Web + VAPID settings injected at build time via `--dart-define`.
|
||||
///
|
||||
/// Never commit real values. Pass them through Docker build-args / `.env`
|
||||
/// (see `.env.example` and `docs/fcm-setup.md` Web section).
|
||||
class FirebaseWebConfig {
|
||||
const FirebaseWebConfig({
|
||||
required this.apiKey,
|
||||
required this.appId,
|
||||
required this.messagingSenderId,
|
||||
required this.projectId,
|
||||
required this.vapidKey,
|
||||
this.authDomain = '',
|
||||
this.storageBucket = '',
|
||||
});
|
||||
|
||||
/// Reads compile-time defines (`FIREBASE_*`).
|
||||
factory FirebaseWebConfig.fromEnvironment() {
|
||||
return const FirebaseWebConfig(
|
||||
apiKey: String.fromEnvironment('FIREBASE_API_KEY'),
|
||||
appId: String.fromEnvironment('FIREBASE_APP_ID'),
|
||||
messagingSenderId: String.fromEnvironment('FIREBASE_MESSAGING_SENDER_ID'),
|
||||
projectId: String.fromEnvironment('FIREBASE_PROJECT_ID'),
|
||||
vapidKey: String.fromEnvironment('FIREBASE_VAPID_KEY'),
|
||||
authDomain: String.fromEnvironment('FIREBASE_AUTH_DOMAIN'),
|
||||
storageBucket: String.fromEnvironment('FIREBASE_STORAGE_BUCKET'),
|
||||
);
|
||||
}
|
||||
|
||||
final String apiKey;
|
||||
final String appId;
|
||||
final String messagingSenderId;
|
||||
final String projectId;
|
||||
final String vapidKey;
|
||||
final String authDomain;
|
||||
final String storageBucket;
|
||||
|
||||
/// True when the minimum set for FCM Web token registration is present.
|
||||
bool get isReady =>
|
||||
apiKey.isNotEmpty &&
|
||||
appId.isNotEmpty &&
|
||||
messagingSenderId.isNotEmpty &&
|
||||
projectId.isNotEmpty &&
|
||||
vapidKey.isNotEmpty;
|
||||
|
||||
/// [FirebaseOptions] for [Firebase.initializeApp], or null when not ready.
|
||||
FirebaseOptions? get options {
|
||||
if (!isReady) return null;
|
||||
final domain =
|
||||
authDomain.isNotEmpty ? authDomain : '$projectId.firebaseapp.com';
|
||||
final bucket = storageBucket.isNotEmpty
|
||||
? storageBucket
|
||||
: '$projectId.appspot.com';
|
||||
return FirebaseOptions(
|
||||
apiKey: apiKey,
|
||||
appId: appId,
|
||||
messagingSenderId: messagingSenderId,
|
||||
projectId: projectId,
|
||||
authDomain: domain,
|
||||
storageBucket: bucket,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,8 @@ class DataFlowScreen extends StatelessWidget {
|
|||
body: '초안이 필요할 때: 최근 대화 몇 줄 + 말투 샘플 일부(최소 컨텍스트)\n'
|
||||
'채팅 릴레이: 보낸 메시지 본문\n'
|
||||
'계정: 표시 이름·초대 코드·세션 토큰\n'
|
||||
'푸시: Firebase 연결 시 실 FCM 토큰 등록 · 미연결 시 install 플레이스홀더 (docs/fcm-setup.md)',
|
||||
'푸시: Android는 google-services.json, 웹은 FIREBASE_*+VAPID 빌드 주입 시 실 FCM 토큰 등록 · '
|
||||
'미연결 시 install 플레이스홀더 (docs/fcm-setup.md · web-upgrade W4)',
|
||||
),
|
||||
_section(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ import 'package:firebase_core/firebase_core.dart';
|
|||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../config/firebase_web_config.dart';
|
||||
|
||||
/// Resolves a device push token for core-backend registration.
|
||||
///
|
||||
/// Prefer a real FCM registration token when Firebase is configured
|
||||
/// (`google-services.json` on Android). Otherwise fall back to a stable
|
||||
/// `install:` placeholder that the server intentionally skips for delivery.
|
||||
/// (`google-services.json` on Android, dart-define Web config + VAPID on web).
|
||||
/// Otherwise fall back to a stable `install:` placeholder that the server
|
||||
/// intentionally skips for delivery.
|
||||
class PushTokenService {
|
||||
PushTokenService({this.storeInstallId, this.readInstallId});
|
||||
PushTokenService({
|
||||
this.storeInstallId,
|
||||
this.readInstallId,
|
||||
FirebaseWebConfig? webConfig,
|
||||
}) : webConfig = webConfig ?? FirebaseWebConfig.fromEnvironment();
|
||||
|
||||
/// Persist a newly generated install id (KV / secure storage).
|
||||
final Future<void> Function(String installId)? storeInstallId;
|
||||
|
|
@ -18,6 +25,9 @@ class PushTokenService {
|
|||
/// Load a previously stored install id.
|
||||
final Future<String?> Function()? readInstallId;
|
||||
|
||||
/// Injected for tests; production uses [FirebaseWebConfig.fromEnvironment].
|
||||
final FirebaseWebConfig webConfig;
|
||||
|
||||
Future<({String token, String platform, bool isFcm})> resolve() async {
|
||||
final platform = _platformLabel();
|
||||
final fcm = await _tryFirebaseToken();
|
||||
|
|
@ -29,8 +39,9 @@ class PushTokenService {
|
|||
}
|
||||
|
||||
Future<String?> _tryFirebaseToken() async {
|
||||
// Web push needs a separate Firebase web config + VAPID; skip until provided.
|
||||
if (kIsWeb) return null;
|
||||
if (kIsWeb) {
|
||||
return _tryFirebaseWebToken();
|
||||
}
|
||||
if (defaultTargetPlatform != TargetPlatform.android &&
|
||||
defaultTargetPlatform != TargetPlatform.iOS) {
|
||||
return null;
|
||||
|
|
@ -50,6 +61,31 @@ class PushTokenService {
|
|||
}
|
||||
}
|
||||
|
||||
/// N4-W4 — Web Push via FCM + VAPID (build-time dart-define secrets).
|
||||
Future<String?> _tryFirebaseWebToken() async {
|
||||
if (!webConfig.isReady) {
|
||||
debugPrint(
|
||||
'FCM web skipped: FIREBASE_* / VAPID dart-defines not set '
|
||||
'(see docs/fcm-setup.md Web)',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final options = webConfig.options!;
|
||||
if (Firebase.apps.isEmpty) {
|
||||
await Firebase.initializeApp(options: options);
|
||||
}
|
||||
final messaging = FirebaseMessaging.instance;
|
||||
await messaging.requestPermission(alert: true, badge: true, sound: true);
|
||||
final token = await messaging.getToken(vapidKey: webConfig.vapidKey);
|
||||
if (token == null || token.isEmpty) return null;
|
||||
return token;
|
||||
} catch (e) {
|
||||
debugPrint('FCM web token unavailable (using install placeholder): $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _ensureInstallId() async {
|
||||
final existing = await readInstallId?.call();
|
||||
if (existing != null && existing.isNotEmpty) return existing;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ykavu_mobile/config/firebase_web_config.dart';
|
||||
|
||||
void main() {
|
||||
test('isReady false when any required field is empty', () {
|
||||
expect(
|
||||
const FirebaseWebConfig(
|
||||
apiKey: 'k',
|
||||
appId: 'a',
|
||||
messagingSenderId: '1',
|
||||
projectId: 'p',
|
||||
vapidKey: '',
|
||||
).isReady,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
const FirebaseWebConfig(
|
||||
apiKey: '',
|
||||
appId: 'a',
|
||||
messagingSenderId: '1',
|
||||
projectId: 'p',
|
||||
vapidKey: 'v',
|
||||
).isReady,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('isReady true and options fill default domains', () {
|
||||
const cfg = FirebaseWebConfig(
|
||||
apiKey: 'api',
|
||||
appId: '1:123:web:abc',
|
||||
messagingSenderId: '123',
|
||||
projectId: 'iykyka',
|
||||
vapidKey: 'vapid-public',
|
||||
);
|
||||
expect(cfg.isReady, isTrue);
|
||||
final opts = cfg.options!;
|
||||
expect(opts.apiKey, 'api');
|
||||
expect(opts.projectId, 'iykyka');
|
||||
expect(opts.authDomain, 'iykyka.firebaseapp.com');
|
||||
expect(opts.storageBucket, 'iykyka.appspot.com');
|
||||
});
|
||||
|
||||
test('explicit authDomain and storageBucket win over defaults', () {
|
||||
const cfg = FirebaseWebConfig(
|
||||
apiKey: 'api',
|
||||
appId: '1:123:web:abc',
|
||||
messagingSenderId: '123',
|
||||
projectId: 'iykyka',
|
||||
vapidKey: 'vapid-public',
|
||||
authDomain: 'custom.example.com',
|
||||
storageBucket: 'iykyka.firebasestorage.app',
|
||||
);
|
||||
final opts = cfg.options!;
|
||||
expect(opts.authDomain, 'custom.example.com');
|
||||
expect(opts.storageBucket, 'iykyka.firebasestorage.app');
|
||||
});
|
||||
|
||||
test('fromEnvironment without defines is not ready', () {
|
||||
expect(FirebaseWebConfig.fromEnvironment().isReady, isFalse);
|
||||
expect(FirebaseWebConfig.fromEnvironment().options, isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// FCM Web service worker stub.
|
||||
// When Firebase Web config is injected at Docker build time, this file is
|
||||
// overwritten with a populated worker (see mobile/Dockerfile).
|
||||
// An empty/stub file prevents 404s while Web Push secrets are absent.
|
||||
console.log('[ykavu] firebase-messaging-sw.js stub (Web Push not configured)');
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Generated at Docker build from firebase-messaging-sw.js.template — do not edit on server.
|
||||
// Compat SDK is required for importScripts-based FCM service workers.
|
||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js');
|
||||
importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js');
|
||||
|
||||
firebase.initializeApp({
|
||||
apiKey: '__FIREBASE_API_KEY__',
|
||||
authDomain: '__FIREBASE_AUTH_DOMAIN__',
|
||||
projectId: '__FIREBASE_PROJECT_ID__',
|
||||
storageBucket: '__FIREBASE_STORAGE_BUCKET__',
|
||||
messagingSenderId: '__FIREBASE_MESSAGING_SENDER_ID__',
|
||||
appId: '__FIREBASE_APP_ID__',
|
||||
});
|
||||
|
||||
firebase.messaging();
|
||||
|
|
@ -21,6 +21,15 @@
|
|||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
<!-- FCM Web SW (N4-W4). Scope matches FlutterFire messaging defaults. -->
|
||||
<script src="flutter_bootstrap.js" async>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('firebase-messaging-sw.js', {
|
||||
scope: '/firebase-cloud-messaging-push-scope',
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ echo "==> GET $BASE/manifest.json (PWA)"
|
|||
curl -fsS "$BASE/manifest.json" | head -c 300
|
||||
echo
|
||||
|
||||
echo "==> GET $BASE/firebase-messaging-sw.js (W4 SW)"
|
||||
code=$(curl -sS -o /tmp/ykavu_fcm_sw.js -w '%{http_code}' "$BASE/firebase-messaging-sw.js")
|
||||
echo "HTTP $code ($(wc -c </tmp/ykavu_fcm_sw.js) bytes)"
|
||||
test "$code" = "200"
|
||||
|
||||
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" \
|
||||
|
|
|
|||
Loading…
Reference in New Issue