마스터 플랜(docs/PLAN.md)에서 추출한 단계별 작업과 현재 상태.
완료한 것은 [x], 진행 중은 [~], 폐기는 [-]로 표시한다.
총 14–21주 (3.5–5개월) 목표. 사용자의 모토는 “서두르지 않고 고득하게”.
매니저 모드 4팀 협의 산출. 출시에 끼우지 않고 LA~NB는 출시 후 첫 마이너, PA~PC는 그다음. liker 목록 미표시(A안), 본인 콘텐츠는 카운트만.
20260609111229_likes.sql. book_reviews.id surrogate + quote_likes·review_likes + RLS(대상 RLS 자연 상속, self-like 차단) + quote_like_counts/review_like_counts(INVOKER) RPC. RLS 테스트 rls_likes.test.sql 로컬 48/48 통과. ✅ 원격 적용 완료(MCP apply_migration, 2026-06-09, security advisor 신규 경고 0). 클라이언트 미사용이라 사용자 영향 없음.20260609122434_notifications.sql: notifications 테이블 + 적재 트리거(quote_like·review_like·follow, SECURITY DEFINER) + unlike/unfollow 시 안읽은 알림 정리 트리거 + unread_notification_count/my_notifications(actor 프로필 RLS 자연 게이트=비공개·차단 익명)/mark_all_notifications_read RPC + RLS(수신자 본인 read/read처리/삭제, INSERT 트리거 전용). 보강 20260609140000_*: 트리거 함수 EXECUTE 회수(advisor 0028/0029 제거). 원격 적용 완료(MCP), pgTAP rls_notifications 9/9, advisor 신규 경고 0. 클라 미연결(PR-NB).lib/features/likes/data/like_repository.dart(LikeRepository·LikeTargetKind·LikeCount·멱등 setLiked·배치 counts) + state/like_providers.dart(likeCountProvider family·LikeActionController). 낙관 헬퍼 LikeCount.toggled + 단위 테스트 5/5. flutter analyze clean. UI 미연결.lib/features/likes/presentation/like_button.dart(낙관적 토글, 자체 Consumer). 후기 카드(book_review_card, 책 상세+전체화면)·활동탭 후기 카드·친구 인용구 카드(quote_list_card via 친구 프로필)에 연결. 본인 후기는 카운트만, 내 인용구 피드는 미주입(노이즈·위축 방지). 후기 RPC 2종에 id 추가 20260609113147_review_id_in_rpcs.sql(MCP 원격 적용 완료) + PublicBookReview/RecentBookReview에 id 반영. flutter analyze clean + 전체 테스트 302/302. ⏳ release AAB 폰 검증은 미실행(다음 손작업).lib/features/notifications/(domain AppNotification·repo·providers·NotificationsScreen·NotificationBell). 활동 AppBar에 벨+배지, /notifications 라우트, 진입 시 전체 읽음. Supabase Realtime 라이브 안읽음 배지(unreadNotificationCountProvider 스트림 — notifications 변경 구독). 마이그 20260609123016_notifications_realtime.sql(publication 추가 + replica identity full, 원격 적용 완료). flutter analyze clean + 전체 302/302. release 폰 검증 예정.20260609160000_device_tokens_and_push_prefs.sql: device_tokens(token PK, 본인 RLS select/delete) + register_device_token(DEFINER, 계정 전환 시 소유자 재지정, authenticated만) + profiles.push_*(마스터+타입별, 기본 ON). 원격 적용 완료(MCP).firebase_messaging + flutter_local_notifications. push_service.dart(권한 요청·register_device_token RPC·onTokenRefresh·탭→data.route 라우팅·로그아웃 토큰 삭제 + 고중요도 채널 bookquote_high 생성 + 포그라운드 onMessage 로컬 표시) + main.dart(백그라운드 핸들러·로그인/로그아웃 start/stop) + POST_NOTIFICATIONS + Manifest default_notification_channel_id + build.gradle core library desugaring. 삼성 One UI에서 채널 명시 없으면 종료 상태 알림 누락되던 것 해결. release 폰 검증 완료.supabase/functions/push-notification/index.ts(웹훅 record → prefs 확인 → device_tokens → 서비스계정 RS256 JWT OAuth2 → FCM HTTP v1 발송 → stale 토큰 정리, actor 비공개 익명). 배포 완료 + 폰 end-to-end 검증(sent:1, 종료 상태 푸시 수신 확인). 인프라(원격, 깃 미커밋): FCM 활성화 + FCM_SERVICE_ACCOUNT·WEBHOOK_SECRET Edge secret + functions deploy --no-verify-jwt + pg_net 웹훅 트리거 notifications_push_webhook(notifications INSERT → 함수, 헤더 x-webhook-secret). 셋업 절차 docs/ops/fcm-push-setup.md.notification_settings_screen.dart(/me/notifications) — 마스터 + 타입별(인용구·후기 좋아요·팔로우) 토글이 profiles.push_* 제어. 마스터 OFF면 타입별 비활성 + OS 권한 안내. Profile/repo에 push_* 추가. 내정보 ‘알림’ 비활성 타일 → 진입 ActionTile 교체(미사용 _ValueTile 제거). 마스터 OFF→푸시 차단 end-to-end 검증(skipped: push disabled). 전체 302/302.순서: LA → {NA, LB} → {NB, LC} → PA → PB → PC.
2026-05-28 산출 — V1.0.x 백로그 13 PR + 마이그레이션 1 + STAGES doc 1:
전 세션에서 백로그 1차 배치 7 PR, 2차 배치 5 PR(폰 dogfood 피드백 반영), 디스커버리 보강 1 PR. 모두 main origin push 완료. 각 PR마다 297/297 테스트 통과 + flutter analyze clean + release APK 폰(R3CXA0PANWX) 설치 검증.
PR0 (3c1f565) — 공유 카드 카탈로그에서 mono·typography 제외 + 글자 baseline +3. CardTemplate.all을 3종(minimal/warm/coverExtract)으로 축소. 위젯 클래스와 byId 폴백 경로는 dormant 유지(과거 draft templateId='mono'/'typography'는 MinimalTemplate으로 자연 폴백). CardEditorState.initial.fontStep 0→3, setTemplate F8 리셋·applyRecommended·card_editor_screen의 fontReset SnackBar 비교 모두 새 baseline 사용. orphan 골든 6장 삭제.
PR1 (c755a25) — 인용구 삭제 확인 다이얼로그 → 5초 [되돌리기] SnackBar. 홈·서재 양쪽 낙관적 제거 후 SnackBar.closed.reason으로 분기: action → 원래 index 재삽입(DB 호출 0), 그 외 → deleteQuote() 커밋. QuoteFeedNotifier.insertAt(index, entry) 신규.
PR2 (skip 확인) — Markdown XFile 첨부는 PR14-E B4에서 이미 완료(quote_export.dart가 임시 디렉토리 + XFile + SharePlus.share(files:)). 백로그 항목 stale.
PR3 (b15c3ec) — 카드 인라인 [수정] + [무드 변경] 시트. QuoteListCard.onEdit/onChangeMoods 콜백 추가. QuoteRepository.setMoods(id, moods) 신규 — updateQuote가 isPrivate를 caller가 매번 정확히 전달해야 평문/잠금 컬럼이 안 흔들리는 footgun이 있어, mood-only 갱신은 본 메서드로 위임(잠금 인용구가 silent로 평문화되지 않게).
PR4-A (9cf78b5) — 무드 chip 탭 → /library?tab=quotes&mood= navigation. 홈 카드 _MoodBadge + 책 상세 무드 chip 둘 다 탭 가능. QuoteListCard.onMoodTap(QuoteMood) 콜백 추가(readOnly 친구 카드는 null로 비-인터랙티브). 검색은 PR20-B에서 이미 완료.
PR5-A (d36b8ae) — 친구 프로필 segment 카운트(책 N / 인용구 N). 마이그레이션 20260528120000_friend_profile_aggregate.sql — SECURITY INVOKER RPC가 책·인용구 카운트 한 round-trip 반환(caller RLS 자동 적용 — 잠금 인용구 자연 제외, 비공개 친구 비팔로워 0). FollowRepository.getFriendProfileAggregate(uid) + friendProfileAggregateProvider family. ✅ 마이그레이션 원격 push 완료(npx supabase db push, 2026-05-28).
PR6 (ab86d55) — 서재 책 표지 long-press → 빠른 액션 시트(Letterboxd 패턴). 4뷰(grid/list/shelf/stack) 표지 길게 누르면 [인용구 추가 / 읽기 시작 / 다 읽음 / 공유] 1탭. book_quick_actions_sheet.dart 신규. grid·shelf는 StatelessWidget → ConsumerWidget 전환.
PR8 (d6ab1ca) — connectivity_plus 연결-회복 시 outbox flush. Connectivity().onConnectivityChanged 구독으로 오프라인→온라인 전환 시점 자동 flush(AppLifecycle.resumed로는 못 잡던 wifi 회복 케이스).
PR9 (b6ad6c8) — 인용구 폰트 base 전 구간 +6 (PR0의 baseline=3가 step max라 추가 여유 없음). getQuoteFontSize: 짧은 22→28, 중간 22→17→28→23, 긴 15→21. clamp(15-36) → clamp(21-44)로 확장. 골든 6장 재생성.
PR10 (a88f49b) — 책 상세에 교보문고 + 알라딘 chip(buildAladinSearchUrl 신규, buildBookPurchaseUrl과 동일 패턴). ISBN13 있을 때만 노출(직접 입력 책 미노출). url_launcher.externalApplication. 실패 시 SnackBar. V1.0엔 plain 검색 URL(제휴 ID/UTM은 출시 후 추가 — 백로그).
PR11 (2600842) — 책 long-press 디스커버리 — 첫 진입 SnackBar(8초, [알겠어요] action, SharedPreferences library_long_press_hint_v1 1회 한정) + 표지 우상단 ⋮ overlay(14×14 반투명 원형, LongPressHintOverlay 위젯 grid/list/stack 3뷰 적용 — shelf spine은 폭 부족으로 skip).
PR12 (0c04eea) — 공유 텍스트에 책 출처·페이지 정보. 카드 공유: buildShareMessage가 bookTitle/bookAuthor/quotePage 받아 "— 〈책 제목〉 김저자 (p.42)" 한 줄 prepend. QuoteCardData.quotePage 필드 추가, provider가 quote.page 전달. 책 long-press 공유는 book.pageCount 있을 때 "제목 · 저자 · 423쪽" 한 줄.
io.github.tgparkk.bookquote://u/<myUid>?from=invite + 안내 카피. 받는 친구가 탭 → /u/:userId 직진.profileRepository.updateMine.deep_link_handler._routeFor에 u/:userId segment 허용 추가. 본인 진입은 router _redirect가 /me로 안전 처리(기존).20260528120000_friend_profile_aggregate.sql 적용 → PR5-A 친구 segment 카운트 활성화.friend-profile.md §2/§6 명시.유지 — 출시 트랙 항목: 카카오 로그인 재오픈 (V1.0.x, 검수 의존) · iOS 출시 (안드로이드 트랙션 확인 후) · keystore 비밀번호 rotate · Supabase DPA + 키 회전 SOP · pgTAP RLS 단위 테스트 · 레이어 누수 5곳 정리 · PostHog 연동 · release 환경 진단 배너.
광고 도입 결정 (2026-05-28 4팀 협의):
google_mobile_ads + AndroidManifest meta-data ② NativeAd 위젯 ③ me_screen.dart 정보 섹션 하단 삽입(로그아웃·탈퇴 버튼과 SizedBox 격리) ④ 데이터 보안 폼 재제출 ⑤ 개인정보처리방침 HTML 갱신 ⑥ release APK 폰 검증.PopScope + ★별점/공유 카드(K-factor, 정책 위반 0). 상세 메모리 feedback_ads_policy.md.2026-05-23 산출 — V1.0 출시 직전 마지막 정렬일:
책글귀 재리브랜드 (588c678) — Play Store에서 컨셉이 거의 동일한 “복로그” 앱(1만↓ 다운로드) 발견. 음운·시각 유사로 사칭·혼동 리스크 + 검색 누수 → 비공개 테스트 단계인 지금이 표시명을 바꿀 마지막 골든타임. “북로그” → “책글귀“로 재변경. 어원: 책 + 句(귀) = 책의 한 구절(글귀의 책 버전, “book quote”). 어제 4a5d2c5 변경 패턴을 동일하게 재치환 — Android android:label·iOS CFBundleDisplayName·인앱 문구·카드 워터마크(tokens.dart)·약관/개인정보/계정삭제 HTML·웹 메타데이터·tool 스크립트 docstring + 워드마크·신고·차단 마이그레이션 코멘트. 불변 (어제와 동일): 패키지 ID io.github.tgparkk.bookquote·Dart 패키지명·딥링크 스킴 — 코드 식별자는 처음부터 bookquote라 한국어 표시명(책귀=冊句, 책의 글귀)과 영문 식별자(book quote)가 다시 의미 일치. 골든 카드 12종 재생성.
네이밍 회의 기록 (feedback_brand_translation.md 메모리) — 매니저 모드 3라운드 후 책귀(원래 이름)와 책귀의 직설형 책글귀 비교 → 책귀의 약점(“친구가 책귀가 뭐야?” 질문)을 책글귀가 정확히 보완(글귀=표준국어대사전 표제어, 즉시 이해). 회의 중 직역 매핑(DogEar) 실수 + V1.0 영문 ASO 평가 실수 2건 — 메모리에 어원 추정 금지·V1.0 단계 글로벌화 평가 금지 기록.
카드 책 정보 폰트 두 단계 상향 (15a81b8) — 폰 실기기에서 책 제목·저자가 “확대해야 보일 만큼” 작다는 시인성 이슈. 5종 카드(minimal·mono·warm·typography·cover_extract) 일괄 부스트: 책 제목 base/md(15~17) → 27px / 저자 sm(13) → AppFontSize.lg(22) / 출판사 xs(11) → 20px (minimal 한정). 27·20은 토큰 단계(lg 22 ~ xl 28) 사이 값이라 카드 한정 직접 픽셀 지정. mono 카드 letterSpacing 공식도 새 사이즈 기준 갱신. 다른 화면의 타이포그래피 토큰은 영향 없음. 골든 카드 12종 재생성·SM F956N 실기기 visual QA 통과.
versionCode 4 상향 (6b4a97d) — Play 비공개 테스트 트랙에 v3 AAB가 이미 한 번 업로드돼 동일 코드로 재업로드 불가. 책글귀 + 폰트 변경 반영 AAB 업로드용으로 4로 상향. 앱 표시 버전(1.0.0)은 유지. Play 트랙 versionCode 1·2·3 모두 소진.
Play Console 진행 (2026-05-23):
python tool/generate_store_assets.py 실행 — 워드마크 “책글귀”·태그라인 “책 속 한 줄을 모으는 곳” 그대로, 아이콘은 텍스트 무관해서 그대로 유지)build/app/outputs/bundle/release/app-release.aab (58.7MB) 비공개 테스트 트랙 Alpha에 업로드/keymanagement에서 Play App Signing SHA-1 A5:65:E8:FB:14:7F:D4:7C:28:51:5D:8C:12:17:A6:77:E6:0E:B3:3B 확인 → Google Cloud Console(프로젝트 bookquote) Android OAuth 클라이언트로 별도 추가 등록. 어제 등록한 upload 키 SHA-1(5E:39:BA:AC:...)과는 다른 클라이언트(Android OAuth는 1 클라이언트 = 1 SHA-1 제약). 스토어 설치 테스터의 구글 로그인 동작에 필수./keymanagement 직접 입력으로만 도달. “고급 설정” 탭은 앱 이용 가능 여부·폼 팩터·Managed Google Play 등 다른 항목만.🔑 OAuth SHA-1 2종 정리 (둘 다 Google Cloud Console 등록 완료):
5E:39:BA:AC:46:32:EE:13:17:46:67:4C:38:24:21:E1:45:7F:D2:FC — android/app/upload-keystore.jks(alias upload) 추출. flutter install·adb install 직접 설치 빌드의 구글 로그인용.A5:65:E8:FB:14:7F:D4:7C:28:51:5D:8C:12:17:A6:77:E6:0E:B3:3B — Google이 Play App Signing으로 생성. Play Store(비공개/내부/프로덕션 트랙)로 설치된 빌드의 구글 로그인용.▶ 다음 세션 할 일 (검토 통과 후):
sttgpark@gmail.com) 설정 + 스토어 등록정보 확정._kakaoLoginEnabled=true. Crashlytics 모니터링(bookquote-aa178).2026-05-22 산출 — V1.0 출시 작업 집중일:
북로그 리브랜드 (4a5d2c5) — 앱 이름 “책귀” → “북로그”(book log). 플랫폼 표시명(Android android:label·iOS CFBundleDisplayName)·인앱 문구·카드 워터마크(tokens.dart)·약관/개인정보 HTML·웹 메타데이터 전부 변경. 불변: 패키지 ID io.github.tgparkk.bookquote·Dart 패키지명 bookquote·딥링크 스킴(앱 정체성). 코드 주석 유지. 골든 카드 12종 워터마크 텍스트 반영 재생성.
PR25 신고·차단 (b2839d4) — 친구 기능 유지 결정 → Google Play UGC 정책상 신고·차단 필수. 마이그레이션 20260522120000_reports_and_blocks.sql: reports·blocks 테이블 + is_blocked_with(uuid) SECURITY DEFINER(양방향 차단 판정) → 기존 친구 RLS 5종(profiles SELECT·quotes/user_books friends·follows ×2)에 not is_blocked_with 추가 + blocks insert 트리거로 양방향 follow 삭제 + list_blocked_profiles() RPC(차단 목록 화면용 — 차단 상대 프로필은 RLS가 가리므로 DEFINER 우회). lib/features/moderation/ 신규(repository·providers·report_dialog·blocked_users_screen). 친구 프로필 ⋮ 메뉴 신고/차단, 친구 인용구는 펼친 상태에서만 신고, 내정보 “차단 목록”(/me/blocked). 마이그레이션 원격 push 완료 — npx supabase db push로 20260522120000 + 보류돼 있던 20260519150000_mood_hub_snapshots 함께 적용. 테스트 신규 10건.
PR26 Firebase Crashlytics (06ed5fc) — 출시·비공개 테스트 크래시 가시성(1인 개발, QA 없음). firebase_core·firebase_crashlytics + com.google.gms.google-services·com.google.firebase.crashlytics Gradle 플러그인. main.dart 모바일 한정 초기화 — FlutterError.onError·PlatformDispatcher.onError → Crashlytics, setCrashlyticsCollectionEnabled(!kDebugMode)(debug 수집 off). Firebase 프로젝트 bookquote-aa178(애널리틱스 미사용 — 데이터 수집·신고 항목 최소화). android/app/google-services.json 커밋. 크래시는 다음 앱 실행 시 업로드.
PR27 친구 찾기 둘러보기 (585e6c0) — 검색어를 입력해야만 사용자가 보이던 dead-end 해소. follow_repository.listPublicProfiles(profiles SELECT RLS 게이트, 최근 가입 순) + discoverProfilesProvider → 친구 찾기 화면이 검색어 비었을 때 “둘러보기” 목록 노출. 마이그레이션 불필요. 테스트 2건.
계정 삭제 안내 페이지 (6d70aef) — docs/account-deletion/index.html → GitHub Pages https://tgparkk.github.io/bookquote/account-deletion/. Play 데이터 보안 폼의 “계정 삭제 URL” 요건 충족.
스토어 자산 (34b60c5, 2206d58) — tool/generate_store_assets.py(아이콘 512×512 + 피처 그래픽 1024×500, 브랜드 Ink-Paper-Copper) → tool/store/. tool/process_screenshots.py(휴대폰 스크린샷을 크림 패딩으로 9:16 정규화 — 원본 968×2376=2.46:1은 Play 규칙 초과) → tool/screenshot/store/play_01~04.png.
검증 — flutter analyze clean, 전체 277 테스트 통과. release APK 72.4MB(fat)·AAB 58.7MB. SM G998N 실기기 설치 + 구글 로그인·둘러보기 검증 완료.
Play Console 진행 (2026-05-22):
io.github.tgparkk.bookquote, 기본 언어 한국어.🔑 빌드·OAuth 함정 (기록):
flutter build는 --dart-define-from-file=.env.json 필수(APK·AAB 모두). 누락 시 SUPABASE_URL 등 빈 값으로 빌드돼 앱 먹통 — env.dart는 String.fromEnvironment 기본값 없음.android/app/upload-keystore.jks(alias upload) SHA-1 5E:39:BA:AC:46:32:EE:13:17:46:67:4C:38:24:21:E1:45:7F:D2:FC. Google Cloud Console(프로젝트 bookquote)에 별도 Android OAuth 클라이언트로 신규 생성해 등록(Android 클라이언트는 SHA-1당 1개 — 기존 칸 “추가” 불가). debug SHA-1 DB:3B:E2:77:...는 기존 bookquote-android 유지. 등록 후 release 구글 로그인 동작 확인.▶ 다음 세션 할 일:
sttgpark@gmail.com) + 스토어 등록정보(설명·아이콘·그래픽·스크린샷 업로드) → 11/11.flutter build appbundle --release --dart-define-from-file=.env.json. PR27(둘러보기)이 직전 AAB 빌드 이후라 미반영 → 업로드 직전 1회 재빌드.PR21 OAuth 랜딩 + 카카오 계정 모델 결정 (2026-05-21):
.env.json·android/local.properties(둘 다 gitignored)에 주입.git stash pop으로 PR21 코드 본선 반영 — 13파일, 충돌 0. flutter pub get → analyze clean → 전체 테스트 통과.sttgpark@gmail.com 계정과 이메일 일치 → 자동 통합). 카카오 ✅ — signInWithIdToken의 “Unacceptable audience” 오류를 Supabase Kakao Provider의 REST API Key 칸에 네이티브 앱 키를 콤마로 추가해 해결.login_screen.dart의 _kakaoLoginEnabled 플래그(false)로 숨김. 카카오 OAuth 코드·콘솔 설정·키는 그대로 유지 — V1.0.x에서 플래그만 되돌리면 재노출.profiles.display_name)을 대표 줄로, 이메일은 보조 줄(있을 때만). me_screen_test.dart 케이스 1건 신규.OAuth 후속 To-Do:
_kakaoLoginEnabled를 true로 되돌려 카카오 버튼 재노출. 검수에 개인정보처리방침 URL + 앱스토어 URL 필요. 이메일 일치 시 기존 계정 자동 통합.linkIdentity(“로그인 수단 연결”) — 내정보에서 다른 provider를 현재 계정에 수동 연결. 이메일·검수에 의존하지 않는 근본 해법.auth.users 4명:
f1d055aa… sttgpark@gmail.com (email + google) — 메인 계정, 유지1b210dfe… sttgpark2@naver.com (email) — 테스트, 삭제472a8312… 이메일 없음 (kakao) — 카카오 검증 중 생긴 더미, 삭제1c26614a… sttgparkk@gmail.com (k 2개 — sttgpark과 다른 별도 구글 계정) — 의도 확인 후 삭제select u.id, u.email, i.provider from auth.users u left join auth.identities i on i.user_id = u.id — Dashboard Users 목록 뷰는 캐시돼 신뢰 불가(빈 이메일 행 누락됨). + 이메일 없는 유저의 회원 탈퇴·데이터 내보내기 동작도 점검.sttgpark@gmail.com로 출시 가능.PR22 + PR23 + PR24 + 잠금 해제 버그 fix 산출 (2026-05-19):
매니저 모드 UX 3팀 6명 재토론 + 사용자 시나리오 4단계(D1·D7·D30·D90) 정합 결과 — 매니저 모드 5건에 누락된 IA 충돌(홈 vs 서재 [인용구])을 박태건 님 직접 발견. 코드 4트랙 동시 진행 (PR21 stash 상태에서 매직링크 코드 위에 작업, OAuth 작업과 코드 영역 안 겹침).
20260519150000_mood_hub_snapshots.sql — RPC my_quote_mood_hub_snapshots() SECURITY INVOKER. CTE 2단(counts GROUP BY + samples DISTINCT ON text IS NOT NULL)로 무드별 카운트 + 평문 대표 1줄 한 round-trip 반환. 잠금 인용구는 카운트 포함 / 발췌는 NULL. 원격 push 본인 손(supabase db push 또는 대시보드 SQL Editor).QuoteRepository.listMoodHubSnapshots() + typedef MoodHubSnapshot(mood, count, sampleText?). 카운트 내림차순 정렬.MoodHubGrid 신규 위젯 — 2열 그리드, childAspectRatio 0.95. 카드 = 아이콘 + 라벨 + 카운트 + 평문 발췌(없으면 “잠긴 인용구만 있어요”). mood_chips.moodColorOf 재사용(시각 언어 일관). RefreshIndicator 호환 위해 AlwaysScrollableScrollPhysics. Material 아이콘 매핑: 위로 favorite_outline / 먹먹 cloud / 새벽3시 nightlight / 통찰 lightbulb / 설렘 auto_awesome.QuoteListView 진입 분기 — initialMood == null && 무드 종류 ≥3이면 hub 모드(MoodHubGrid), 아니면 시간순. _resolveEntryMode 메서드가 hub fetch 후 결정 + 실패 시 시간순 안전 후퇴. _selectMood가 hub 복귀(_mood=null + snapshots≥3)에서 items reload 안 함 — 칩 “전체” 한 번으로 hub 자연 복귀.ref.listen(quoteFeedProvider) 외부 invalidation 채널 — initialMood null이면 _resolveEntryMode, 아니면 단면 reload.mood_hub_grid_test.dart 3건(카드 렌더·카운트·발췌 / 카드 탭 콜백 mood 전달 / 빈 snapshots 크래시 없음). QuoteListView 분기는 Repository 의존 깊어 폰 시각 검증으로.BookRepository.listCurrentlyReading(limit=7) — started_at IS NOT NULL AND finished_at IS NULL + books join. started_at desc. 메모리 partial index user_books_started_idx(20260517 마이그레이션) 재활용. typedef CurrentlyReading(book, startedAt).currentlyReadingProvider FutureProvider(non-autoDispose — 홈 BottomNav 첫 슬롯 캐시 유지).NowReadingRow 신규 — 헤더 “📖 지금 읽고 있어요” + 가로 스크롤 책 표지 행(width 64, height 96, row 142). 끝에 [+] 카드. 빈 상태 = “지금 읽는 책이 없어요” + “[+ 시작한 책 알려주기]” 카드 1장. 책 표지 탭 = /quote/new?bookId=... 직진(적기로 가는 다리). [+] 또는 빈 상태 탭 = showBookSearchSheet → setReadingDate(started_at=today) + invalidate(currentlyReadingProvider) + SnackBar [한 줄 적기] action.reading_dates_row.dart set 후 invalidate(currentlyReadingProvider) 추가 — 책 상세에서 시작/완독 변경 시 홈 NowReadingRow 자동 갱신.library_screen.dart FAB 분기: _tab == 1(인용구)면 null, 그 외(책·캘린더)는 [+ 책 추가]. 인용구 탭은 BottomNav 가운데 [+] 인용구 추가와 의미 충돌 회피 (사용자 직접 결정 — 매니저 모드 옵션 비교 후).QuoteListView._items는 로컬 list state라 외부 invalidate(quoteFeedProvider) 신호를 받지 못함. 편집 모드(/quote/new?quoteId=...)에서 잠금 해제 후 저장 → invalidate 호출돼도 서재 [인용구]의 카드는 stale로 남아 🔒 마크 유지. 카드 탭 시 quoteByIdProvider는 fresh fetch라 잠금 해제된 본문이 보임 — 두 경로의 동기화 갭이 사용자 증상.QuoteListView.build()에 ref.listen(quoteFeedProvider) 추가 → invalidate 신호 수신 시 _resolveEntryMode(initialMood null) 또는 _loadCounts + _reload(단면) 호출 ② quote_input_screen.dart 편집 모드 invalidate 블록에 bookQuotesProvider 추가(책 상세 미니리스트 stale도 함께 해소).검증 — 합산 254/254 통과 (PR22-PR24 신규 3건 = mood_hub_grid 3건). flutter analyze clean. release APK 67.7MB(PR21 시점 67.6MB +0.1MB)로 SM F956N 설치 검증 완료.
supabase db push 또는 대시보드 SQL Editor) — 20260519150000_mood_hub_snapshots.sql. push 안 해도 hub UI는 시간순으로 자연 fallback(서비스 정상). push하면 hub 활성화.android/app/build.gradle.kts의 release buildType이 현재 signingConfig = signingConfigs.getByName("debug")(TODO 상태, debug 키 서명). Google Play는 debug 키 APK를 거부하므로 출시 전 release 키스토어 생성 + build.gradle.kts signingConfig 교체 필수. OAuth 콘솔 작업의 선행 조건 — 카카오/구글 키 해시는 최종 서명 키 기준이라, release 키스토어를 먼저 만들고 그 키 해시를 콘솔에 등록해야 한다(debug 키 해시로 등록 후 release 키로 바꾸면 OAuth 로그인 깨짐). Play App Signing 사용 시에도 업로드 키는 필요.docs/ops/oauth-setup.md) — Play Console 차단 답변 무관하게 Google·Kakao 진행 가능. 단 2번(release 키스토어) 완료 후 그 키 해시로 등록할 것. 끝나면 git stash pop → 빌드.▶ 다음 세션 시작점 (위 마이그레이션 push + OAuth 콘솔 + Play Console 답변 후): git stash pop → flutter pub get → 빌드 → 폰 검증 → V1.0 출시 본 작업.
PR21 산출 (2026-05-19, 매직링크 제거 + 구글·카카오 OAuth SDK 직접 통합):
도메인 미보유로 Resend SMTP 경로가 막혀 매직링크를 V1에서 제거. 구글은 google_sign_in, 카카오는 kakao_flutter_sdk_user로 ID Token을 직접 받아 supabase.auth.signInWithIdToken에 전달하는 SDK 우회 방식. 카카오 비즈 앱 인증 없이도 KOE205 회피.
google_sign_in: ^6.2.1, kakao_flutter_sdk_user: ^1.10.0 추가.auth_controller.dart — sendMagicLink 제거. signInWithGoogle(웹 client ID로 serverClientId 지정 → Supabase audience 검증) + signInWithKakao(카카오톡 설치 시 loginWithKakaoTalk 우선, 실패 시 loginWithKakaoAccount 자연 fallback). signOut은 SDK 토큰도 함께 정리(narrow on PlatformException swallow).login_screen.dart — 매직링크 폼·_SentNotice·F1 회귀 제거. 두 OAuth 버튼 1급(구글 OutlinedButton + 카카오 FilledButton #FEE500). env 키 미주입 시 disabled + 안내 텍스트.env.dart — KAKAO_NATIVE_APP_KEY·GOOGLE_WEB_CLIENT_ID 컴파일 타임 상수 + isKakaoConfigured·isGoogleConfigured 게터.main.dart — KakaoSdk.init(env 키 있을 때만).AndroidManifest.xml — com.kakao.sdk.flutter.AuthCodeCustomTabsActivity 등록 + kakao${KAKAO_NATIVE_APP_KEY}://oauth scheme. app/build.gradle.kts에서 local.properties의 kakao.nativeAppKey 읽어 manifestPlaceholders["KAKAO_NATIVE_APP_KEY"]로 주입.Info.plist — 카카오 URL scheme(kakaoKAKAO_NATIVE_APP_KEY placeholder — 본인이 실제 키로 치환) + LSApplicationQueriesSchemes(kakaokompassauth, kakaolink).deep_link_handler.dart — auth callback 분기·getSessionFromUrl 호출 제거. 인앱 라우트(/book/:id)만 처리.auth_callback_screen.dart 삭제 — /auth/callback·/callback 라우트 제거, _redirect의 isAuthPath 분기 단순화.login_screen_test.dart 매직링크 회귀 가드 3건 제거. OAuth 버튼 노출·매직링크 UI 부재 회귀·env 미주입 disabled 안내 3건 신규.docs/ops/oauth-setup.md — Google Cloud Console + Kakao Developers + Supabase Dashboard 단계별. 키해시 추출 PowerShell 명령, 자주 발생 문제 7건 표 포함.To-Do 6건 갱신 (PR21로 1·2번 흡수) — 매니저 모드 백엔드 토론 결과 그대로 유지:
docs/ops/oauth-setup.md 가이드). SMTP·도메인 트랙은 V1.0.1 hotfix로 강등(매직링크 부활 옵션).docs/privacy/index.html 6장(처리위탁)과 별도로 “개인정보 국외이전 고지” 섹션 신설. 항목: 수탁사 Supabase Inc.(미국) · 실제 처리지 AWS ap-northeast-2(서울) · 이전 항목(이메일/닉네임/아바타URL/팔로우/책 메타데이터) · 이전 시점(가입 즉시 + 이용 중 상시) · 보유기간(탈퇴 또는 위탁계약 종료 시까지) · 이전 근거(정보주체 동의). 가입 동의 화면에 국외이전 동의 체크박스 분리.docs/ 또는 비공개 위치에 service_role 키·Edge Function 시크릿 회전 SOP(주기·트리거·절차) 1페이지. G/H 합의 결과 — 출시 전 *고지+운영의 두 다리 모두 박는다.*supabase.auth.currentUser 읽는 곳을 controller/provider 뒤로 격리: book_detail_screen.dart:197, card_editor_screen.dart:294, quick_share_screen.dart:157, splash_screen.dart:54, deep_link_handler.dart:85. 향후 어떤 백엔드로 가든 단일 교체 지점 확보.supabase/tests/database/에 RLS 정책 25개 검증 자산 추가. 기존 rls_friends.test.sql 패턴 확장. 회귀 가드 + 백엔드 이전 시 의미론적 동등성 증명 자산.PR20-D 산출 (2026-05-19, 홈 친구 최근 활동 1줄 배너 — UX#4 K-factor 다리):
20260519140000_friend_recent_activity.sql — RPC friend_recent_activity(since timestamptz) SECURITY INVOKER. quotes/profiles RLS 자연 게이트 활용(친구 + 공개 + 잠금 아님). user별 group-by + max(created_at) desc 정렬, limit 20. 원격 push 완료.friend_activity_provider.dart — FriendActivity typedef (userId·displayName·avatarUrl·count·latest) + friendActivityProvider autoDispose FutureProvider. last_seen SharedPreferences (friend_activity_last_seen_v1) — 미설정 시 기본 7일 lookback. markFriendActivitySeen() 헬퍼.FriendActivityBanner ConsumerWidget — 0건이면 자체 숨김. 1건 = “지윤님이 새 인용구 3개를 보탰어요” / ≥2건 = “지윤 외 N명이 새 인용구를 보탰어요”. 탭 시 markFriendActivitySeen + invalidate + /u/<first> push. accent50 배경 + accent200 border.invalidate(friendActivityProvider) 추가.PR20-C 산출 (2026-05-19, sender 컨텍스트 deep link 영속 + K-factor 다리 — UX#3):
PR20-C 산출 (2026-05-19, sender 컨텍스트 deep link 영속 + K-factor 다리 — UX#3):
share_sheet.buildDeepLinkForShare(bookId, senderUid) → io.github.tgparkk.bookquote://book/<id>?from=share&sender=<uid>. share_plus ShareParams.text로 전달. Kakao 단톡은 이미지만 받지만 Telegram·SMS·메일·트위터 등은 텍스트도 받음 → V1 K-factor 다리 살아남(받는 사람이 링크 탭 → 책 상세 진입).shareCardImage(text:) — share_service에 text 파라미터 추가. 카드 에디터·quick_share 두 진입점 모두 data.bookId + supabase.auth.currentUser?.id 전달.router /book/:id — ?sender=<uid> 쿼리 파싱 → BookDetailScreen.sender prop._SharedBanner ConsumerWidget으로 승격 — sender uid를 friendProfileProvider로 watch. 공개 프로필 OR 본인이면 row, 그 외 RLS 0 row → null 자연 분기. 공개면 “지윤님이 이 책의 한 줄을 보냈어요.” + [이 사람 서재 보기 ▸] TextButton → context.push('/u/<sender>'). 비공개/본인/sender 없음 → 익명 카피 “누군가 이 책의 한 줄을 보냈어요.” (위변조 deep link 대비). isSupabaseReady 가드로 테스트 환경 호환.PR20-B 산출 (2026-05-19, 인용구 텍스트 검색 — UX#2):
PR20-B 산출 (2026-05-19, 인용구 텍스트 검색 — UX#2):
QuoteRepository.searchMyQuotesWithBook(query, limit) — text + manual_book_text ilike '%query%' OR + ilike escape(%/_/\) + created_at desc/id desc. RLS가 본인 것만 게이트. 잠금 인용구(text=null)는 NULL ilike 비매칭 → 자연 제외(서버에 평문 0 원칙 일관).quoteSearchProvider(query) autoDispose family (flutter_riverpod).QuoteSearchDelegate extends SearchDelegate<void> — Material showSearch 표준 사용. ValueNotifier<String> _debounced + Timer(300ms) 디버운스. _Hint/_ZeroResult/_ErrorView + 결과는 QuoteListCard 탭 → /quote/:id/share(검색은 hot 컨텍스트 → 1탭 = 바로 공유. 전문가 #3 R4 retention 의도).IconButton(search_rounded) → showSearch(context, QuoteSearchDelegate()). 재사용 1 delegate.PR20-A 산출 (2026-05-19, 저장→공유 액션 모델 통일 — UX#1):
매니저 모드 UX 종합 (2026-05-19, 모바일 UX 전문가 3명 병렬 점검 — Day 0 onboarding / IA·네비게이션 / D1-D30 retention):
/me/lock-password·/me/friend-search top-level 승격(Me 서브트리 안티 패턴) · OCR 코치마크(클립보드 빈 상태 OS Live Text 안내) · Self-set local reminder · 홈 무드 다시보기 칩.PR20-A 산출 (2026-05-19, 저장→공유 액션 모델 통일 — UX#1):
quote_input_screen._submit thenCard 파라미터 제거 → 단일 흐름. CTA 두 버튼([카드 만들기 →]/[저장만 하기])을 단일 저장로 통합.[바로 공유](accent400) + [카드 디자인] + (책 연결 시) [이 책에 한 줄 더](PR15-A 흡수). 홈/책상세 어디서 진입하든 방금 만들었다 hot 컨텍스트 안에서 1탭으로 다음 행위 선택.PR18-D 산출 (2026-05-19, 책 상세 “이 책을 담은 친구 N명” 행 + 시트):
PR18-D 산출 (2026-05-19, 책 상세 “이 책을 담은 친구 N명” 행 + 시트):
user_books_friends_read RLS가 이미 가시성 게이트. 클라이언트 2-step(user_books.eq(book_id).neq(user_id=self).count(exact) → profiles.inFilter('id', ids))로 RLS 그대로 활용. RPC followers_count_for_book + 인덱스는 V1 미사용(측정 후 hotfix 슬롯).FollowRepository.countFriendsWithBook(bookId) — RLS 통과 row 카운트 + 본인 제외(neq(user_id, uid)). 미로그인은 0. friendsWithBook(bookId, limit) — 2-step 프로필 목록, added_at desc 정렬 보존.friendsWithBookCountProvider(bookId)(헤더에서 즉시 watch) + friendsWithBookProvider(bookId)(시트 열릴 때 lazy fetch).book_detail_screen._FriendsWithBookRow — 헤더 행 다음, “이 책에서 모은 구절” 위. count <= 0이면 자체 숨김(빈상태 회피, 디자인 결정). 탭 → _FriendsWithBookSheet(DraggableScrollableSheet 0.5/0.9, 아바타+display_name ListTile, 탭 시 /u/:userId로 push + 시트 닫힘).friendsCount=0 → 행 숨김 / N≥1 → "N명" 행 노출). 신규 13건 누계(PR18-C 12 + PR18-D 2 — 사실상 +2). 전체 241/241 통과. flutter analyze clean. release APK 빌드 검증.PR18-C 산출 (2026-05-19, /u/:userId 친구 프로필 풀스크린):
20260519120000_follows_public_read.sql — follows SELECT RLS 확장: 두 endpoint 모두 is_library_public=true면 누구나 read 가능. 친구 프로필 헤더 “팔로워 N · 팔로잉 N” 카운트 + 카운트 시트 노출 가능하게(트위터식 social proof, friend-profile.md §7). 비공개 프로필 follow 그래프는 본인 외 0 row 유지. 원격 push 완료(project ndbvptxwznogcuuumzzh).ProfileRepository.getById(userId) — RLS상 공개 프로필 OR 본인이면 row, 그 외 nullFollowRepository.listFollowing/listFollowers(userId, limit) + countFollowing/countFollowers(userId) — 2-step (follows rows → profiles inFilter('id', ids)로 N+1 회피). follow 순서 보존 reorder.BookRepository.listFriendBooks(userId, limit) — user_books_friends_read RLS 게이트(클라 쿼리는 단순 eq('user_id', userId))QuoteRepository.listFriendQuotesWithBook(userId, after, limit) — quotes_friends_read RLS(is_private=false) 게이트, cursor-after (created_at desc, id desc)lib/features/profile/presentation/friend_profile_screen.dart + state/friend_providers.dart — 헤더(아바타·display_name·카운트·팔로우 버튼 낙관 토글 + 언팔로우 확인 다이얼로그) + 세그먼트 [책 ↔ 인용구] + 잠긴 서재 빈상태(FollowState 분기 카피: 팔로우 전 “공개 설정을 켜면 보여요” / 팔로잉 중 “팔로우 중이에요…”) + _NicknameGateView(.·_ 포함 또는 빈 닉네임 풀스크린 봉쇄) + _FollowersSheet(DraggableScrollableSheet → 아바타+display_name ListTile → 무한 깊이 /u/:uid push) + _NotFoundView(profile null 시 [홈으로])./u/:userId top-level GoRoute + _redirect 본인 진입 시 /me로 redirect(1프레임 흰 화면 회피, 라우터 가드 단계 처리). 인증 게이트는 기존 loggedIn 분기에 자연 흡수(from=/u/:userId 보존).quote_list_card.dart readOnly + onOpenBook prop 추가 — 친구 인용구 카드는 [공유]·[카드 디자인]·[삭제] 모두 숨김, 펼침 시 [책 보기]만(book_id null이면 그것도 숨김).friend_search_screen.dart ListTile onTap → /u/:userId 라우팅 + avatar_url 폴백(이니셜 → NetworkImage).quote_list_card_test readOnly 그룹 3개 + friend_profile_screen_test 9개 신규 = 신규 12개. 전체 239/239 통과. flutter analyze clean. release APK 빌드 검증(feedback_release_only_traps 패턴 통과). 남은 V1.0 작업 = PR18-E(RLS 침투 회귀 테스트 — 잠금 인용구·비공개 프로필·비팔로워 0 row 단언) + Stage 5 출시 본 작업.PR18-C 산출 (2026-05-19, /u/:userId 친구 프로필 풀스크린):
20260519120000_follows_public_read.sql — follows SELECT RLS 확장: 두 endpoint 모두 is_library_public=true면 누구나 read 가능. 친구 프로필 헤더 “팔로워 N · 팔로잉 N” 카운트 + 카운트 시트 노출 가능하게(트위터식 social proof, friend-profile.md §7). 비공개 프로필 follow 그래프는 본인 외 0 row 유지. 원격 push 완료(project ndbvptxwznogcuuumzzh).ProfileRepository.getById(userId) — RLS상 공개 프로필 OR 본인이면 row, 그 외 nullFollowRepository.listFollowing/listFollowers(userId, limit) + countFollowing/countFollowers(userId) — 2-step (follows rows → profiles inFilter('id', ids)로 N+1 회피). follow 순서 보존 reorder.BookRepository.listFriendBooks(userId, limit) — user_books_friends_read RLS 게이트(클라 쿼리는 단순 eq('user_id', userId))QuoteRepository.listFriendQuotesWithBook(userId, after, limit) — quotes_friends_read RLS(is_private=false) 게이트, cursor-after (created_at desc, id desc)lib/features/profile/presentation/friend_profile_screen.dart + state/friend_providers.dart — 헤더(아바타·display_name·카운트·팔로우 버튼 낙관 토글 + 언팔로우 확인 다이얼로그) + 세그먼트 [책 ↔ 인용구] + 잠긴 서재 빈상태(FollowState 분기 카피: 팔로우 전 “공개 설정을 켜면 보여요” / 팔로잉 중 “팔로우 중이에요…”) + _NicknameGateView(.·_ 포함 또는 빈 닉네임 풀스크린 봉쇄) + _FollowersSheet(DraggableScrollableSheet → 아바타+display_name ListTile → 무한 깊이 /u/:uid push) + _NotFoundView(profile null 시 [홈으로])./u/:userId top-level GoRoute + _redirect 본인 진입 시 /me로 redirect(1프레임 흰 화면 회피, 라우터 가드 단계 처리). 인증 게이트는 기존 loggedIn 분기에 자연 흡수(from=/u/:userId 보존).quote_list_card.dart readOnly + onOpenBook prop 추가 — 친구 인용구 카드는 [공유]·[카드 디자인]·[삭제] 모두 숨김, 펼침 시 [책 보기]만(book_id null이면 그것도 숨김).friend_search_screen.dart ListTile onTap → /u/:userId 라우팅 + avatar_url 폴백(이니셜 → NetworkImage).quote_list_card_test readOnly 그룹 3개 + friend_profile_screen_test 9개 신규 = 신규 12개. 전체 239/239 통과. flutter analyze clean. release APK 빌드 검증(feedback_release_only_traps 패턴 통과). 남은 V1.0 작업 = PR18-D(책 상세 친구 미니리스트 + followers_count_for_book RPC) + PR18-E(RLS 침투 회귀 테스트 — 잠금 인용구·비공개 프로필·비팔로워 0 row 단언) + Stage 5 출시 본 작업.상태: Stage 0~1 완료 + 화면 설계 완료 + Stage 2 본 작업 종료(PR1~6 + 별점) + Stage 3 전체 완료 — PR7~PR12(+PR10.5 1탭 공유, +인용구 편집 모드, +출시 약관 페이지) + PR13 출시 직전 P0 fix 2건(F1·B11) + PR14 출시 직전 P1 15건 6 sub-PR 완료(A 아웃박스 안전성·B 인용구 입력 검증·C 카드 lifecycle·D UX·접근성·E Markdown XFile + draft 시점·F 매직링크 타임아웃 안내). 다음은 Stage 5 본 작업(스토어 등록·PostHog·인스타·커뮤니티 게시) + B9 검증. flutter analyze clean, flutter test 127개 통과. 마이그레이션 5개(quotes, user_books.rating, my_quote_mood_counts, cards, +Stage1 4개) 원격 적용 완료. main에 push됨. 실기기(SM F956N) PR10 검증 통과 — 카카오톡/인스타 공유 OK. PR10 hotfix 2건(2026-05-16): ① main/AndroidManifest.xml INTERNET 권한 누락(debug/profile에만 있어 release APK는 모든 네트워크 호출 SocketException) ② card_renderer.dart의 boundary.debugNeedsPaint 사용 — SDK 내부에서 assert로만 초기화되는 late bool 반환이라 release/profile에서 LateInitializationError. 둘 다 PR5/PR10 시점부터 잠재해 있던 release-only 버그. 향후 모든 release 빌드는 이 두 함정 인지하고 동작.
지금 동작하는 플로우: 로그인 → 홈(내 인용 피드: 무한스크롤·당겨새로고침·빈상태 CTA·카드 탭 펼침→[📤 바로 공유 ↗]/[카드 디자인]/[삭제] — 바로 공유는 draft 또는 추천 디자인으로 즉시 PNG 렌더 + 공유 시트) → + → 인용구 입력(본문/클립보드 붙여넣기/책 연결/페이지/무드/draft/오프라인 큐잉) → 저장 → 홈 반영 / 서재 [책↔인용구] 세그먼트 — “인용구” 탭 무드별 다시보기 / 책 상세(별점·”이 책에서 모은 N구절” 미니리스트·”이 책 인용구 추가” CTA·isInLibrary면 ✓칩 아니면 [서재에 담기]·⋮[서재에서 빼기]·설명 점진적 공개·?from=share deep link면 공유 배너 + “내 서재에 담기” 1급 CTA) / 내 정보(프로필·인용/서재 count·Markdown 내보내기·약관/개인정보/문의 링크·로그아웃[아웃박스 경고]·회원 탈퇴 2단계) / 책 검색·로그인은 Stage 1. (“카드 만들기 →”는 카드 에디터 스텁으로 감 — Stage 3.) deep link: ://book/:id?from=share → 핸들러가 GoRouter로 라우팅(콜드스타트는 스플래시가 보류 경로 소비, 워밍은 즉시 router.go, URI 1회 consume). 미로그인 “담기” 탭 → /auth/login?from= 경유 복귀(payload 보존).
Designer + Planner walkthrough 산출 (2026-05-17, 6 페르소나 S1·S2·S6 designer + S3·S4·S5 planner 병렬 위임). 신규 발견 P1 9건(designer W1·W2·W3·W4·W5·W7·W9 + planner 4건). 즉시 처리 1건 — PR14-G(aa68d8e) QuickShareScreen _openEditor가 context.go로 quick_share 스택을 교체해 카드 에디터 뒤로가기 시 홈 직행 → S6 “디자인 편집 → 다시 공유” 시나리오 단절(④ 막다른 골목 금지 무력화). 1줄 push 전환으로 에디터 뒤로 = quick_share 복귀, _autoSheetTriggered=true 덕분에 자동 시트 재발 없음, 사용자가 [다시 공유] 트리거. 검증 3건 grep 확정 — 모두 미구현(V1.0.1 hotfix 백로그): ① book_search_sheet “최근 책 5권” 섹션 부재(F7·S3 5번 반복 검색 마찰) ② share_service PNG 캐시 윈도우 부재(S4 4단톡 매 사이클 1080×1920 재생성) ③ 홈 AppBar 검색 부재(S5 47개 컬렉션 “그 구절 어디” 불가). 나머지 6건 출시 후 hotfix 묶음: W1 BookSearchSheet 키보드/포커스 충돌, W2 PasteBanner 엄지 도달 외, W4 카드 에디터 진입 컨텍스트 단절, W5 본문 수정 X-닫기 미저장 경고 부재 + 복귀 invalidate noise, W7 quick_share 미리보기 노출 200ms 딜레이, W9 카드 접힘 상태 공유 아이콘. planner 권고 — 차별화 강화 hotfix 후보: 저장 후 SnackBar action [이 책에 한 줄 더](축적), share_sheet 첫 공유 후 “다른 방에도?” 카피(반복 closure), 홈 상단 “이번 주 회고” 1행(차별화 ④ 진입성), Markdown 내보낸 후 정보성 BottomSheet 1회(차별화 ③ 감정 모멘트).
PR14 산출(2026-05-17, P1 15건 6 sub-PR — 시나리오 워크 후속): PR14-A(outbox/b7b473a) QuoteOutbox에 static _isFlushing 가드(B1) + QuoteRepository.createQuote에 PostgrestException code ‘23503’ → 'FK_VIOLATION' 별도 코드, flush에서 discarded 분류(B2) + OutboxBanner ConsumerWidget 신규 + pendingOutboxCountProvider + 홈·인용목록 상단 배너(F13) + home _flushOutbox에 discarded 안내 SnackBar + enqueue 후 ref.invalidate(quoteOutboxProvider). flush에 named optional uid 추가(테스트 가능성). test/features/quote/quote_outbox_test.dart 신규 4개. PR14-B(quote_input/0c76d1a) _submit 진입부에 page <= 0 차단 + SnackBar(B5) + _pasteFromClipboard에 2000자 runes 기준 truncate + 안내 SnackBar(B6). quote_input_screen_test에 widget test 2 + 클립보드 mock helper 추출. PR14-C(card-editor/518008e) _onEditQuoteTap 복귀 시 _initialized=false + _skipDraftDialog=true → _initializeFromData가 다이얼로그 없이 draft 적용 또는 새 추천(B3) + quick_share _share 진입부에 _captureKey.currentContext.size.isEmpty 가드 + endOfFrame 1회 재시도(B12). PR14-D(card-editor·a11y/0554c10) AppBar FilledButton.icon "공유" 제거 + bottomNavigationBar Full-width FilledButton(높이 52)(F4) + CardEditorController.setTemplate에 fontStep:0 명시 + _Editor에 selectTemplate/cycleTemplate wrap + 전환 직전 step ≠ 0이었으면 SnackBar(F8) + mood_chips._MoodChip + quote_list_view._Chip 양쪽에 MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.15)(F9) + 워터마크 토글 후 ScaffoldMessenger SnackBar “워터마크를 켰어요/껐어요”(F10) + _RatioSegment.style.textStyle override 제거(B16). card_editor_controller_test 3개(F8 회귀 가드). release APK 빌드 sanity 통과. PR14-E(me·quote/ed1e302) quote_export.dart를 getTemporaryDirectory + 임시 .md 파일 + ShareParams(files: [XFile(mimeType: 'text/markdown')], text, subject) 첨부 전환(B4 — Android Binder ~500KB 한도 회피, share_plus가 FileProvider 처리 → AndroidManifest 변경 불필요) + QuoteDraftStore 저장 포맷 v2 wrapper({input, savedAt: ISO8601}) + 구 포맷(QuoteInput JSON 단독) 호환 + quote_input_screen._maybeRestoreDraft SnackBar에 _relativeTime “N분/시간/일/주 전”(F5). quote_draft_test 신규 5개. PR14-F(auth/6736b2e) AuthCallbackScreen에 _timedOut state — 10초 타임아웃 시 즉시 navigate 대신 사유 안내 + [로그인 화면으로] 버튼(B8). 사용자가 명시적으로 탭해야 /auth/login으로. _LoadingNotice/_TimeoutNotice 분리. 합산: 6 commit, 146/146 테스트 통과(PR14 신규 14개), flutter analyze clean.
바로 이어서 할 것 → PR16-C E2EE 입력 UI + PR18 친구 서재 탐험(2026-05-17 합류 결정) + Stage 5 출시 준비 본 작업 + B9 검증. PR16 → PR18 순서(PR18 RLS의 is_private=false 게이트가 PR16-B 컬럼 의존). 출시 한 달 패키지 = PR16-C/D/E + PR18-A/B/C/D/E + Stage 5. PR5 남긴 출시 블로커 2건 처리 완료(2026-05-16): ① delete-account Edge Function 운영 배포(project ndbvptxwznogcuuumzzh, version 1 ACTIVE, 인증 없는 POST → 401 게이트웨이 응답 확인) ② GitHub Pages 활성화(Source = main /docs, https://tgparkk.github.io/bookquote/{terms,privacy}/ 둘 다 HTTP 200 + 컨텐츠 검증 OK — 스토어 등록 폼에 이 URL 그대로 사용). 남은 Stage 5: 앱스토어·플레이스토어 등록·심사, PostHog funnel(PII 미전송), 인스타 매일 1개 카드(W-4부터), 디스콰이엇·긱뉴스 게시. B9 검증 대기: 저사양 카드 에디터 OOM(release-only 의심) — Android 8/API 27 + 1.5GB RAM AVD 또는 친구 저사양 폰에서 카드 에디터 진입 + 템플릿 5회 전환 + PNG 캡처 + 공유 시나리오. 재현되면 P0 → _MiniCard 절대 1080 위젯을 56×96dp 경량 위젯으로 대체. 미재현이면 P1로 강등하고 백로그. PR12 산출: A=언두 ≥20 + state.canUndo+⤺ AppBar. B=fontStep ±3 + [A−][A+] + 따뜻 카드 ensureContrast 대비 보강(어두운 표지 + lightenToBackground 후 가독성 회복). C=5스와치(paletteSlotIndex 0~4) + “다른 느낌 ↻”(기존 cycleTemplate 명시 노출) + applyPaletteSlot helper. D=auto-fit 경고(비율별 charCount 휴리스틱 feed=300/post=450/story=600, 추천 비율 1탭 적용). E=접근성(_Swatch hit area 28→48dp, 카드 미리보기·_MiniCard Semantics 라벨, IconButton tooltip 보장). 골든 12장(4종×3비율, T4 cover URL 의존이라 별도 fixture 시 추후). PR7 산출: lib/features/card_editor/{domain,data,presentation/widgets}/* — sealed CardTemplate ×5 + supports/recommended/byId/all + QuoteCardData + 5종 위젯(1080 절대 캔버스 — “미리보기=export”) + QuoteCard 디스패처 + CardWatermark + color_utils.{lightenToBackground,toMidTone} + splitIntoPoetryLines(chunk별 강제 줄바꿈 보존)/getTypographyFontSize. PR8 산출: color_utils에 WCAG 2.1 4종(relativeLuminance/contrastRatio/ensureContrast(4.5)/getTextColorForBackground 임계 0.18). palette_service.dart — LinkedHashMap LRU(maxCacheSize 100, 타임아웃 3s) + getPaletteWithFallback(coverUrl?, templateId) + PaletteGeneratorFactory 주입. palette_providers.dart — paletteServiceProvider(앱 1 인스턴스) + extractedPaletteProvider family(키 = Dart record). screen _PreviewBox/_MiniCard → ConsumerWidget 전환, .value ?? fallbackFor + AnimatedSwitcher 200ms cross-fade. PR9 산출: state/quote_card_data_provider.dart — quoteByIdProvider + bookByIdProvider 합성(book 없으면 manualBookText 폴백) AsyncValue<QuoteCardData?>. state/card_editor_controller.dart — non-family Notifier<CardEditorState> + attach(quoteId) 1회 + 500ms 디버운스 shared_preferences 영속화(키 card-editor-draft:{quoteId}) + setTemplate/setRatio/toggleWatermark/applyRecommended/cycleTemplate/readDraft/applyState/clearDraft. CardEditorState(templateId, ratio, watermarkEnabled) JSON round-trip. screen = ConsumerStatefulWidget — mock 제거, quoteCardDataProvider watch, 진입 시 readDraft → “이어서 만들기 / 새로 시작” 다이얼로그 → 새로 시작은 applyRecommended(charCount, hasCover). AppBar action에 워터마크 토글(copyright_rounded 아이콘) + 비율 세그먼트. quote 없음(PGRST116)·로드 실패 별도 view. PR10 산출: data/card_renderer.dart — renderCardPng({GlobalKey boundaryKey, CardRatio ratio}) → RenderRepaintBoundary.toImage(pixelRatio = ratio.size.width / boundary.size.width) + 캡처 전 endOfFrame 2회(폰트 atlas + paint 안전망. 초기엔 debugNeedsPaint로 1회 재시도 분기였으나 release에서 LateInitializationError 던져서 hotfix로 제거) + ui.Image.dispose() + path_provider 임시파일. data/share_service.dart — shareCardImage({XFile file, String? subject}) = SharePlus.instance.share(ShareParams(files:[file])) 단일 wrapper, CardShareException 메시지 래핑. presentation/widgets/share_sheet.dart — showCardShareSheet(context, file, shareText) showModalBottomSheet(isScrollControlled:true) + 드래그 핸들 + 4버튼(카카오 #FEE500 / 인스타·저장·다른앱 outlined) — V1은 모두 동일하게 shareCardImage 호출, 권한/SDK 분기는 V1.1. card_editor_screen AppBar에 accent500 FilledButton “공유”(_isSharing 토글 → CircularProgressIndicator), _PreviewBox 안 AspectRatio child를 RepaintBoundary(key:_captureKey) 래핑(“미리보기=export” 그대로 캡처). pubspec.yaml path_provider: ^2.1.5. android/app/src/main/AndroidManifest.xml에 <uses-permission android:name="android.permission.INTERNET" /> 추가(hotfix). PR10.5 산출 (디자이너 권고 — 2026-05-16): lib/features/card_editor/quick_share_screen.dart 신규 — /quote/:id/share 풀스크린 route, 진입 즉시 quoteCardDataProvider 로드 + card_editor_controller.readDraft 분기(있으면 그대로, 없으면 applyRecommended) + endOfFrame 2회 → renderCardPng + showCardShareSheet 자동 호출. 시트 dismiss 후 화면 유지([다시 공유]/[디자인 편집] 출구, ④ 막다른 골목 금지). quote_list_card.dart 펼침 액션 위계 재조정: [📤 바로 공유] FilledButton accent500 / [✏ 카드 디자인] OutlinedButton primary200 / [삭제] TextButton semanticError, Wrap으로 좁은 폰 대비. home/library/book_detail 3곳 callsite에 onShare 배선. router에 /quote/:id/share 추가. PR11 산출 (cards 테이블 + 비차단 INSERT — 2026-05-16): supabase/migrations/20260516120000_cards.sql 신규 — cards 테이블(user_id on delete cascade auth.users 탈퇴 정합, quote_id on delete cascade quotes, book_id on delete set null books, design jsonb(templateId/ratio/watermarkEnabled), shared_at timestamptz, RLS 본인만 select/insert, update/delete 정책 없음 — V1 immutable), 원격 push 완료. lib/features/card_editor/data/card_repository.dart 신규 — recordShare({quoteId, bookId?, design}) fire-and-forget INSERT, Supabase 미초기화·미로그인 환경 no-op, 실패 silently swallow(공유는 이미 OS 시트로 끝남). CardEditorState.toJson()을 design jsonb로 그대로 INSERT(별도 도메인 모델 없이 controller state 재사용). QuoteCardData에 bookId 필드 추가. card_editor_screen._onShareTap + quick_share_screen._share 둘 다 showCardShareSheet 직전 unawaited(recordShare(...)) 비차단 호출. PR12 부분 산출 (골든 스냅샷 — 2026-05-16): test/features/card_editor/golden_card_test.dart 신규 — 5종 × 3비율 매트릭스에서 supports 게이트 통과 케이스만 자동 생성 = 12장(T4 CoverExtract는 cover URL 의존이라 별도 fixture 필요 시 추후). setUpAll에서 FontLoader로 NotoSerifKR + Pretendard 명시 등록(flutter_test 기본 Ahem 폰트 회피). setSurfaceSize + view.physicalSize = ratio.size로 카드 위젯의 절대 1080×N 픽셀 그대로 캡처 — 실 export와 픽셀 동일. matchesGoldenFile로 회귀 단언. --update-goldens로 재생성 워크플로우. 테스트: PR7 19개 + PR8 24개 + PR9 15개 + PR10 3개 + PR10.5 4개 + PR11 3개 + PR12-A 7개(undo) + 골든 12장 + PR13 5개(login 2 + submit_update 3). 총 132/132 통과. PR13 산출(2026-05-17, 매니저 모드 시나리오 워크 후 P0 fix): docs/discovery/scenario-review-2026-05-17.md — 14 시나리오 × 가상 팀(기획자·UI/UX·QA) 병렬 협의로 UX 마찰 17 + 엣지·실패 18 = 35건 발견. P0 후보 6건을 매니저 코드 대조로 재판정 → F1·B11만 P0 유지, B9는 검증 대기, 나머지는 P1/P2. F1: lib/features/auth/login_screen.dart _SentNotice에 onResetEmail 콜백 + [이메일이 다른가요? 다시 입력] TextButton 추가 → 부모가 _linkSent=false 토글해 Form 입력 화면 복귀(이메일 오타·도메인 오타 시 앱 재시작 외 탈출구 부재 해소). B11: lib/features/quote/state/quote_providers.dart submitUpdate(clearBook=false) 명시 파라미터 — 기존 clearBook: input.bookId == null 자동 추론 제거. quote_repository.updateQuote의 ?bookId null-aware map literal이 bookId null이면 patch에서 키 제외하므로 clearBook=false면 책 연결 자연 유지. V1엔 책 해제 UI 없으므로 호출자가 명시하지 않으면 항상 false → prefill 실패(저속 회선·일시 미응답)에서 silent 데이터 손실 차단. V1.5 책 해제 액션 추가 시 명시 clearBook: true 전달.
PR17 캘린더 + UX 학습 결정 (2026-05-17): 왓챠피디아 도서 기능 조사 + 글로벌 UX 학습(Letterboxd·StoryGraph) 통합 결과 — V1.0에 PR17 추가(독서 시작·완독일 입력 + 서재 [캘린더] 세그먼트). 17-A 스키마(user_books에 started_at/finished_at date + CHECK + partial index 2개) + book_repository.setReadingDate · 17-B book_detail_screen._ReadingDatesRow(오늘/어제/직접선택 칩, 재탭=지우기, started 없이 finished 누르면 둘 다 today + Toast) · 17-C library_screen 3 세그먼트화 + calendar_segment.dart(table_calendar ^3.x) + [인용구] 세그먼트(원래 V1.5 보강이었으나 묶어 끌어올림) · 17-D 골든 + release APK 검증(feedback_release_only_traps 강제). PR16(E2EE)과 같은 한 달 패키지. 채택 UX 3건: ① 날짜 기본값=오늘+DatePicker 숨김(Letterboxd) ② 별점 재탭=해제(이미 StarRating 구현 — 캘린더 칩에도 일관) ③ 표지 long-press 액션시트는 V1.0.1 후속 PR로 분리. 설계 = docs/DECISIONS.md 2026-05-17 항목 + docs/design/screens/library-calendar.md(신규) + book-detail.md·library.md 갱신.
PR17 산출 — V1.0에 독서 시작·완독일 캘린더 합류 (2026-05-17, 4 sub-PR 완료): 17-A(e908f3d) 마이그레이션(user_books에 started_at/finished_at date + CHECK finished >= started + partial index 2개) + ReadingDates 도메인 + book_repository.setReadingDate/getReadingDates(upsert + auto-add-to-library 패턴 재사용). 원격 push 완료(project ndbvptxwznogcuuumzzh). 17-B(8fe821e) book_detail._BookRatingRow 다음에 ReadingDatesRow — 별점 행 아래 [오늘][어제][직접] 칩, 입력 후 〔M월 D일〕 칩 + 지우기. started 없이 finished [오늘] 누르면 둘 다 today로 저장 + Toast “함께 시작일도 오늘로 저장했어요”(StoryGraph 자동 기입 패턴). 공용 위젯 분리(presentation/widgets/reading_dates_row.dart) + 단독 위젯 테스트 6건. _FakeRepo는 extends Mock implements BookRepository(mockito 패턴 — SupabaseClient의 GoTrue periodic timer가 widget dispose 후에도 남는 이슈 회피). 17-C(8efd829) library_screen 2→3 세그먼트 + ?tab=calendar 분기. pubspec.yaml에 table_calendar ^3.1.3(실제 3.2.0). user_book_on_day.dart 신규 도메인(ReadingMarkKind started/finished/both + buildCalendarMarkers top-level 헬퍼 — 다른 달 무시·같은 날 시작+완독은 both) + userBooksCalendarProvider((year, month)) FutureProvider.family + book_repository.listCalendarMarkers(or() 조건 + partial index 활용). calendar_segment.dart — table_calendar 래퍼 + 두 색 분리 마커(시작 accent200 outline / 완독 accent500 채움 / 둘 다 합침, ≥4권은 점 3개+”···”) + 선택 셀 상세 리스트(부 텍스트 “읽기 시작”/”다 읽음 ✓” 명시 — 접근성). 캘린더는 읽기 전용 — 입력·수정은 책 상세의 ReadingDatesRow에서. 선택 날짜는 위젯 state(Riverpod 3.x StateProvider legacy 분리 이슈 회피). 17-D 골든 생략(ReadingDatesRow는 기능 행이지 export 산출물 아님 → PR17-B 위젯 테스트 6건이 회귀 가드 역할, 골든 baseline 비용 대비 가치 낮음) + release APK 빌드 sanity 통과(66.3MB, 72.5s — INTERNET 권한 OK·debugNeedsPaint 없음·feedback_release_only_traps 패턴 통과). 합산: 4 commit, flutter analyze clean, flutter test 171/171(PR17 신규 23개 = reading_dates 10 + reading_dates_row 6 + calendar_markers 9, 단 일부 cross-count). 캘린더 진입 = /library?tab=calendar. 남은 V1.0 작업 = PR16(E2EE) + Stage 5 출시 본 작업(스토어 등록·PostHog·B9 검증).
PR16-A 산출 — E2EE 스키마 2장 + 크립토 코어 (2026-05-17, 26f5a39): 마이그레이션 2장(20260517130000_quotes_e2ee.sql quotes에 is_private/text_encrypted/manual_book_text_encrypted/crypto_version 추가 + text NOT NULL 해제 + quotes_text_xor_encrypted CHECK + 잠금 본인 조회 partial index, 20260517140000_user_crypto_envelopes.sql envelope 테이블 + RLS 본인만) — git에만, 원격 DB push 보류(다음 세션 PR16-B 시작 시 첫 작업으로 push). 크립토 코어 lib/features/crypto/: KeyDerivation(PBKDF2-HMAC-SHA512 600k iters) + QuoteCipher(AES-256-GCM, 저장 형식 nonce(12)||cipher||tag(16)) + CryptoEnvelope value class + KeyService(flutter_secure_storage 캐시 + createEnvelope/openEnvelope/rewrap). 의존성 cryptography ^2.7.0 + flutter_secure_storage ^10.2.0(^9.x는 win32 ^5와만 호환 → package_info_plus ^10.x와 충돌, ^10.x로 상향). AndroidManifest allowBackup="false" + fullBackupContent="false"(마스터키 Google Drive autoBackup 누수 차단 — 안 하면 “키를 서버가 모름” 보장 무력화). 단위 테스트 9건(KDF wrap/unwrap round-trip · 잘못된 비번 거부 · salt 격리 · AES round-trip · 잘못된 키 거부 · blob 변조 거부 · nonce 랜덤성 · 빈 문자열 · 2000자). flutter analyze clean, flutter test 180/180, release APK 빌드 66.4MB·71.4s 통과(secure_storage native + cryptography release-only 함정 없음).
PR18 친구 서재 탐험 V1.0 합류 결정 (2026-05-17): 매니저 모드 가상 팀(Dart 2·UI/UX 2·기획 2·QA 2) 협의 결과 DECISIONS 2026-05-12 “follow/타임라인 V1.5+” 결정을 부분 뒤집어 V1.0 패키지에 합류. 단 풀-소셜 X: ① 단방향 follow(트위터식, request-accept 없음) ② 공개 정책 = profiles.is_library_public bool default false(프로필 단위 토글, per-quote is_public X) ③ 잠금 인용구(quotes.is_private=true, PR16)는 RLS에서 hard exclude — 친구 화면 노출 0% 보장 ④ 친구 발견 = display_name ilike 검색 + 카드 공유 deep link ?sender=<uid> 두 경로(카톡 매칭은 카카오 로그인 V1.5와 묶음) ⑤ 화면 신설 1개(/u/:userId 친구 프로필 read-only) + 기존 화면 갱신 2건(Me “친구 찾기” 활성화 + 책 상세 “이 책을 담은 친구 N명” 1줄). BottomNav 슬롯 추가 X(4탭 위계 사수), 홈 피드에 친구 인용구 섞기 X(“내 인용 피드” 정체성 사수). 본인 진입은 /me redirect. PR18 분할 = 18-A 마이그레이션 1장(follows + profiles.is_library_public + RLS 정책 3종) + 도메인 + follow_repository 코어 · 18-B 검색·카운트 + Me “친구 찾기” 활성화 + “내 프로필 공개” Switch + 공개 닉네임 편집(prerequisite — 본명 노출 사고 차단) · 18-C /u/:userId 친구 프로필 화면 · 18-D 책 상세 “친구 N명” 1줄 + 미니리스트 시트 + share_service.dart deep link sender + deep_link_handler 파싱 + sender 배너 [이 사람 서재 ▸] · 18-E 골든 + RLS 침투 테스트(잠금·비공개·비팔로워 모두 0 row) + release APK 검증. PR16-A 마이그레이션 선행, PR16-B(quotes 읽기 측 wiring) 완료 후 PR18 진입. 세부 = docs/DECISIONS.md 2026-05-17 “친구 서재 탐험 V1.0 합류” + docs/design/screens/friend-profile.md(신규) + docs/db-schema.md §2.5 follows + me.md/book-detail.md 갱신.
PR16-B 산출 — E2EE wiring (2026-05-17): ① 마이그레이션 2장 운영 DB push 완료(npx --yes supabase db push로 20260517130000_quotes_e2ee + 20260517140000_user_crypto_envelopes 적용, project ndbvptxwznogcuuumzzh). ② CryptoEnvelope.fromRow/toInsertRow/toRewrapPatch + 헬퍼 encodePgBytea/decodePgBytea(PostgREST \\x prefix hex 양방향). ③ lib/features/crypto/data/envelope_repository.dart 신규 — user_crypto_envelopes CRUD(getMine/insert/updateWrap). ④ Quote 모델 — text nullable로 전환 + isPrivate(bool, default false) + cryptoVersion(int?) freezed 필드 추가. QuoteInput도 isPrivate 추가. ⑤ QuoteRepository — KeyService + QuoteCipher 주입. createQuote(input.isPrivate) 분기(잠금이면 본문 + manualBookText 둘 다 AES-256-GCM 암호화 후 평문 컬럼 NULL + *_encrypted 채움, is_private=true, crypto_version=1). updateQuote(isPrivate:bool=false) 동일 분기. insertPrivatePayload(...) 신규 — outbox flush 시 pre-encrypted bytes 그대로 INSERT. 읽기 측 _decryptIfPrivate(row) — getById/listMyQuotes/listMyQuotesWithBook 모두 row 단위로 캐시 마스터키로 복호화해 Quote.text/manualBookText 채움. 키 없거나 mac mismatch면 text=null(UI는 PR16-C에서 isPrivate로 잠금 view 분기). ⑥ QuoteOutbox — KeyService + QuoteCipher 주입. enqueue(input)가 input.isPrivate이면 즉시 암호화 후 {kind:'private', text_encrypted_hex, manual_book_text_encrypted_hex, book_id, page, source, moods, crypto_version} JSON으로 저장 — prefs에 평문 일절 없음(테스트로 contains 검증). flush는 sealed _OutboxEntry로 dispatch — _PlainEntry→createQuote, _PrivateEntry→insertPrivatePayload. legacy ‘kind’ 없는 entries는 평문으로 해석(PR3 이전 호환). pending()은 평문만 노출(테스트/디버그), pendingCount()는 전체 — pendingOutboxCountProvider는 pendingCount()로 갱신. ⑦ cards.design jsonb 검증 완료 — CardEditorState.toJson()은 templateId/ratio/watermarkEnabled/fontStep/paletteSlotIndex만 들어가 quote text 사본 없음(grep 확인 — card_editor/data/card_repository.dart:40). ⑧ wiring callsite 패치(Quote.text nullable 전환) — quote_input_screen prefill·quote_card_data_provider·quote_list_card.text·markdown_exporter(잠금 인용구는 [잠금된 인용구] placeholder). 신규 providers lib/features/crypto/state/crypto_providers.dart — keyServiceProvider + quoteCipherProvider(앱 1 인스턴스). 테스트: PR16-B 신규 15건(envelope serialization 9 + outbox private 6). flutter analyze clean, flutter test 195/195. release APK 66.7MB·58s 통과. 다음 세션 = PR16-C 시작 동선: 입력 화면 잠금 토글 + 첫 잠금 모달(영구 손실 경고 + [잠금 비밀번호 설정] 분기) + 잠금 인용구 공유 직전 확인 모달(“이미지엔 평문이 박힙니다”) + quote_list_card/recall_card 🔒 배지 + card_editor_screen/quick_share_screen 잠금 fallback view(“이 기기에서 잠긴 인용구”). PR16-D는 lock_password_screen(설정/변경/QR 백업/import) + 새 기기 첫 잠금 인용구 접근 시 패스프레이즈 모달. PR16 종료 후 → PR18 친구 서재 탐험 진입(PR18-A 마이그레이션 = follows + profiles.is_library_public + RLS 정책 3종 — quotes_friends_read의 is_private=false 게이트로 잠금 인용구 자동 제외, 한 줄로 보장. 세부 = friend-profile.md).
PR16-C-1 산출 — 입력 화면 잠금 토글 + 첫 잠금 모달 + 편집 모드 잠금↔평문 전환 (2026-05-17): ① lib/features/crypto/presentation/lock_toggle_row.dart 신규 — OFF/ON 아이콘(lock_open/lock_outline)·문구·Switch.adaptive(activeThumbColor: accent500). enabled=false 분기 회색 + onChanged=null. Semantics(toggled, label, hint) 접근성. ② lib/features/crypto/presentation/lock_dialogs.dart 신규 — FirstLockDialog(영구 손실 경고 + 비밀번호 6자이상 runes 기준 + 확인 입력 + [취소]/[잠금 설정] → KeyService.createEnvelope + EnvelopeRepository.insert + cacheMasterKey 3콤보), UnlockDialog(다른 기기 — 비밀번호 1개 + [취소]/[잠금 해제] → openEnvelope + cacheMasterKey 2콤보, SecretBoxAuthenticationError → “비밀번호가 달라요”), helper ensureMasterKeyReady(context, ref) — envelope/캐시 3분기(캐시 있음 즉시 true / envelope 없음 → FirstLockDialog / envelope 있음 → UnlockDialog). ③ quote_input_screen — _isPrivate state + _onLockToggle(잠금→평문 전환 시 편집 모드면 확인 다이얼로그 “본문이 평문으로 저장돼요” + [잠금 해제]/[취소], 평문→잠금은 ensureMasterKeyReady) + _buildInput()에 isPrivate 전달 + _loadExistingQuote에서 quote.isPrivate prefill + draft 누수 차단(_isPrivate=true면 _saveDraft early return + 잠금 ON 전환 시 _clearDraft 즉시 호출 — prefs에 평문 본문이 남는 폰 분실·플래시 덤프 누수 차단). LockToggleRow는 CTA 위 항상 노출(편집 모드 포함). ④ quote_repository.updateQuote 분기 정리 — isPrivate flag를 항상 patch(이전엔 본문 변경 있을 때만), 평문→잠금 시 is_private=true+crypto_version=kKdfVersion+text_encrypted(평문 NULL), 잠금→평문 시 is_private=false+crypto_version=null+text_encrypted=NULL+평문 patch. 잠금↔평문 전환 사고(데이터 컬럼 불일치) 완전 차단. ⑤ 테스트 신규 12건 — lock_toggle_row_test 6건(OFF/ON 시각·탭·Switch 직접 탭·enabled=false 차단·onChanged=null Switch 비활성), lock_dialogs_test 6건(FirstLockDialog 6자 미만·불일치·취소·한국어 runes / UnlockDialog 빈 입력·취소). 합산: flutter analyze clean, flutter test 207/207 통과(PR16-B 195 + 신규 12). release APK 67.1MB·42.4s 통과. 실기기(SM F956N) sanity 3 시나리오 통과 — 신규 잠금 생성·편집 모드 잠금 해제(확인 다이얼로그 OK)·편집 모드 잠금 추가. main 직접 push.
PR19 산출 — 디자인 회의 P0+P1 묶음 (2026-05-18): 매니저 모드 가상 팀(타이포 마스터·레이아웃·UX·에디토리얼 4 디자이너 병렬 위임)으로 글자크기·카드크기·글자배치 토론 후 P0 1건 + P1 2건 합의·실행. P0: lib/core/theme/tokens.dart getQuoteFontSize 500자 끝점 11→13px + 200~500 보간식 재계산(*4.0 → *2.0, 15→13으로 좁힘) + getEffectiveQuoteFontSize.clamp(9,36)→(13,36). 사유 = NotoSerifKR w500의 한글 가로획 두께가 11px에서 0.5~0.7px로 떨어져 판독 한계 초과(타이포 마스터) + 인스타 피드 1/3 축소 시 실효 ~3.7pt로 인지심리 읽기 속도 0 수렴(UX 전문가) 공동 합의. T4 CoverExtract는 자체 math.max(rawSize,15.0), T5 Typography는 별도 함수(getTypographyFontSize clamp 15,48)라 영향 없음. AppFontSize.xs 11px는 출판사·ISBN 메타용으로 의미 재정의(주석 갱신). P1-A: lib/features/quote/presentation/widgets/quote_list_card.dart 인용구 폰트 NotoSerifKR 13px → Pretendard 15px w500. 사유 = 앱 내부 리스트는 스캔 컨텍스트, 세리프 소형은 인지 부하 ↑. 세리프(공유 카드 = 감상 모드) ↔ 산세리프(앱 내부 = 스캔 모드) 시각 구분 확립. maxLines=3 노출 글자 수 약 15% 감소 → 펼침 탭 유도 ↑. P1-B: lib/features/card_editor/presentation/widgets/warm_card.dart T2 Warm CardRatio.story를 sideBySide → topBottom으로 분기 + coverPanelSize 380→480 + paddingTop 240→144. 사유 = 9:16 캔버스에서 좌측 380px 표지 패널이 세로 스트립처럼 압축돼 표지 홍보 효과 약화(레이아웃 전문가). _Variant에 coverWidth/coverHeight 토큰화 — _CoverPanel의 isHorizontal ? 300 : 240 매직 넘버 제거, 비율별 표지 치수 한 곳에서 관리. story-topBottom 표지 320×448(기존 sideBySide 300×420보다 살짝 크게). 검증: flutter analyze 3 파일 모두 clean. flutter test 206개 중 warm × 9:16 골든 1건만 깨짐(예상 회귀) → --update-goldens로 test/features/card_editor/golden/warm_story.png 갱신. 다른 카드 골든(minimal·mono·typography 9비율) 모두 통과 = P0 폰트 변경이 기존 골든 테스트 케이스(200자 이하)를 깨지 않음 확인. release APK 67.1MB·83.0s 통과(INTERNET 권한 + debugNeedsPaint 없음 — feedback_release_only_traps 패턴 회피). 보류 (V1.0.1 후보): ① T1 Minimal 인용부호 추가(에디토리얼 권고, “미니멀과 충돌” 우려로 보류) ② T5 강제 줄바꿈 → 사용자 줄바꿈 우선 fallback(에디토리얼 권고, “원문 작가 호흡 보존” 명분이지만 기존 사용자 카드 렌더 변경 영향 큼). 출시 후 사용자 피드백 본 뒤 재논의.
PR18 P0/P1 흡수 설계 결정 (2026-05-18, 설계만 — 구현은 PR16-C-2 닫은 다음 진입): 매니저 모드 가상 팀(critic·planner·designer·qa-tester 4명 병렬) 검토로 PR18 친구 서재 탐험 분할 갱신. 핵심 모델(단방향 follow·프로필 토글·hard exclude·BottomNav 4탭·홈 미섞기)은 유지하되 4명 중 2명 이상 독립 발견한 약점 3건(닉네임 회피 ↔ ilike 검색 silent killer · PR18-B prerequisite 강제 게이트 미비 · 친구 진입점 전무 + 선행 신호 부재)을 P0로 격상 + 흡수. P0 (출시 전 반드시): profiles SELECT RLS를 using(true) → using(is_library_public = true OR id = auth.uid())로 좁힘 + searchByDisplayName에 클라 단 .eq('is_library_public', true) 이중 방어 + PR18-B/C 강제 게이트 (_NicknameGateView 풀스크린 — 닉네임 미설정/email local-part 의심 패턴 봉쇄) + 닉네임 패턴 감지 다이얼로그. P1 (강력 권고, 분할 내부 흡수): profiles.public_handle text unique 컬럼을 PR18-A 마이그레이션에 미리 박기(V1.0 미사용, V1.0.1 hotfix 비용 1/3) + Me Switch 옆 현재 노출 상태 카피 + 홈 빈 상태 CTA “친구 찾기 →” 1줄(인용구 ≥1개 + 친구 0명 조건부) + FollowState enum 분기 카피(비공개 빈상태 인지 부조화 회피). P2 (지표·테스트): PostHog 선행 이벤트 3종(friend_search_zero_result_exit·library_public_toggle_unchanged·book_detail_friend_count_zero, 가족 5명 가입 단계 임계 ≥40%면 출시 전 재검토) + PR18-E에 X-feature 매트릭스(잠금×친구×캘린더 3축 8조합 중 4 골든) + 본인 진입 redirect를 라우터 _redirect로 끌어올림(initState → 라우터 가드). deep link sender(PR18-D) 유지 — critic 손, 친구 발견 funnel 두 다리 사수. 갱신된 PR 분할: 18-A(마이그레이션 + public_handle + RLS 좁힘) → 18-B(검색·토글·닉네임 편집 + 패턴 감지 + Switch 카피 + 홈 CTA + PostHog) → 18-B/C 게이트(신설) → 18-C(/u/:userId + FollowState 분기 + 라우터 가드) → 18-D(유지) → 18-E(X-feature 매트릭스). 일정 양보: 한 달 → 5~6주(2026-06-22 ~ 06-29 출시 범위). 의존 순서: PR16-C-2(잠금 password 화면) → PR16-D/E → PR18-A. E2EE 트랙 완전히 닫고 PR18 진입(planner — 회귀 베이스 흔들림 회피). 산출 = docs/DECISIONS.md 2026-05-18 항목 + friend-profile.md §1/3/6 + 변경 이력 + db-schema.md §2.1 profiles + §2.5 follows + 마이그레이션 표 갱신.
PR16-C-2 산출 — 잠금 인용구 공유 평문 경고 + 🔒 배지 + 잠금 fallback view (2026-05-18): ① lock_dialogs.dart에 showPrivateShareWarningDialog(context) 헬퍼 — 잠금 인용구 카드 공유 직전 1회 노출. “본문은 잠겨 있지만 카드 이미지에는 인용구가 평문으로 박혀요” 경고 + [취소]/[그래도 공유] · barrierDismissible: false로 외부 영역 탭 닫힘 차단. ② QuoteCardData에 isPrivate 필드 + isLockedAndUnreadable getter(isPrivate && quoteText.isEmpty) 추가 — quote_card_data_provider가 quote.isPrivate 전달. ③ card_editor_screen — _onShareTap 진입부 잠금 분기 → 경고 모달 → [취소]면 공유 흐름 중단(_isSharing 토글 전). isLockedAndUnreadable이면 _Editor 대신 _LockedView 표시 + bottomNavigationBar(공유 버튼) 숨김. ④ quick_share_screen — _share에 동일 경고 분기 · _bootstrap에서 isLockedAndUnreadable이면 _autoSheetTriggered=true 설정해 자동 시트 차단 + _buildBody에서 _LockedView early return. ⑤ quote_list_card — quote.isPrivate면 본문 위 🔒 + “잠금” 칩(accent600 12px xxs). 잠금 + text 비어있음(키 없음)이면 본문 자리에 “이 기기에서 잠겼어요” italic placeholder(primary400). 평문 인용구는 칩 미노출. ⑥ _LockedView 위젯 — card_editor_screen/quick_share_screen 둘 다 file-private으로 정의(공유 위젯 분리 vs 30 LoC × 2 트레이드오프 — 일단 file-private 유지). 메시지 “이 기기에서 잠긴 인용구. 인용구 입력 화면에서 잠금을 해제하거나 다른 기기의 잠금 비밀번호로 풀면 카드로 만들 수 있어요.” ⑦ recall_card는 무드 카운트만 표시(텍스트 노출 X) — 🔒 배지 불필요 확인. STAGES.md/메모리의 “recall_card 🔒 배지” 표기는 부정확(quote_list_card만 처리). 테스트: lock_dialogs_test에 showPrivateShareWarningDialog 그룹 3건(경고 카피 + [취소]/[그래도 공유]) + quote_list_card_test에 잠금 케이스 3건(🔒 + 본문 / placeholder / 평문 미노출). 합산: flutter analyze clean, flutter test 213/213 통과(PR16-C-1 207 + 신규 6). release APK 67.1MB·71.7s 통과(feedback_release_only_traps 패턴 회피). 다음 세션 = PR16-D 시작 — lock_password_screen(설정/변경/QR 백업/import) + 새 기기 첫 잠금 인용구 접근 시 패스프레이즈 모달.
PR16-D 산출 — 잠금 비밀번호 관리 화면 + 잠금 해제 진입점 + 비밀번호 변경 (2026-05-18): ① lib/features/crypto/presentation/lock_password_screen.dart 신규 — Me “잠금 비밀번호” 진입. envelope/캐시 동시 조회(_lockSnapshotProvider autoDispose) → LockPasswordState 3분기(notConfigured/configuredAndUnlocked/configuredButLocked) + 로딩/에러. 상태별 아이콘·제목·본문·CTA 매핑(lock_open_outlined → 잠금 비밀번호 설정 / lock_outline → 비밀번호 변경 / lock_clock_outlined → 잠금 해제) + 종이 백업 권장 경고 카드(semanticWarningLight, “비밀번호는 책귀 서버가 모릅니다”). ② lock_dialogs.dart에 ChangePasswordDialog 추가 — 현재/새/확인 3필드. submit = openEnvelope(현재) → rewrap(K, 새) → updateWrap → cacheMasterKey. K는 그대로라 인용구 재암호화 0. SecretBoxAuthenticationError → “현재 비밀번호가 달라요” · 새 비밀번호 6자 runes + 일치 검증(FirstLockDialog 패턴 일관). ③ _LockedView에 onUnlock 옵션 콜백 — card_editor_screen/quick_share_screen 양쪽 [잠금 해제] FilledButton accent500. 콜백 = ensureMasterKeyReady(PR16-C-1 헬퍼) → 성공 시 quote provider invalidate + 화면 재로드. quick_share는 _bootstrap 재실행으로 자동 시트 흐름 정상 진입(_data=null+_ready=false+_autoSheetTriggered=false). ④ router.dart — /me/lock-password GoRoute 추가(셸 /me 하위 + parentNavigatorKey: _rootNavigatorKey로 BottomNav 외부 풀스크린). ⑤ me_screen 설정 섹션 첫 항목 _ActionTile(Icons.key_outlined, '잠금 비밀번호', context.push('/me/lock-password')) — loggedIn 조건부. key_outlined로 “개인정보처리방침”의 lock_outline과 시각 구분. 보류 (V1.0.1): ① QR 백업·import (qr_flutter 의존 + 카메라 권한 + 심사 리뷰 위험 — 수동 비밀번호로 충분, 종이 백업 카피로 영구 손실 대비) ② 잠금 기능 끄기(envelope 삭제 + 잠금 인용구 평문화 batch — 데이터 변환 복잡). DECISIONS 2026-05-18 PR16-D 범위 축소 결정. 테스트: lock_dialogs_test에 ChangePasswordDialog 그룹 5건(현재 빈/새 6자 미만/불일치/취소/한국어 runes 6자). lock_password_screen 4상태 위젯 테스트는 KeyService·EnvelopeRepository 양쪽 mock 필요 — 작업 대비 가치 낮아 PR16-E 침투 테스트로 묶기. 합산: flutter analyze clean, flutter test 218/218 통과(PR16-C-2 213 + 신규 5). release APK 67.1MB·55.3s 통과(feedback_release_only_traps 패턴 회피). 다음 세션 = PR16-E — RLS 침투 테스트(잠금×친구×캘린더 3축, PR18 X-feature 매트릭스와 묶기) + lock_password_screen 4상태 골든 + 잠금 hard exclude 회귀 가드 + PR18-A 진입 준비.
PR16-E 산출 — 클라이언트 단 잠금 회귀 가드 + RLS 침투 PR18-E 묶음 결정 (2026-05-18): ① crypto_test.dart에 “비밀번호 변경 (rewrap) round-trip” 그룹 2건 — KeyDerivation 패턴(_fastKd 1000 iter)으로 wrap1 → unwrap(cur) → K · 새 salt + 새 nonce + 새 wrap_key로 K 재포장 → unwrap(new) → 같은 K 검증. 이전 비밀번호로 새 envelope unwrap 시도 → SecretBoxAuthenticationError. 회귀 가드 핵심 = “비밀번호 변경 후에도 K 동일 = 인용구 재암호화 0”. KeyService.openEnvelope이 envelope.kdfIters로 새 Pbkdf2 만들어 600k iter 회피 위해 KeyService 직접 호출 안 함(crypto_test.dart의 기존 패턴 일관). ② quote_card_data_provider_test.dart 신규 — quoteByIdProvider.overrideWith 패턴으로 4 케이스: isPrivate=true + text=null → isLockedAndUnreadable=true / 잠금 + 복호화 성공 → false / 평문 일반 흐름 / quote null → null. PR16-B의 nullable text 흐름 + PR16-C-2의 isLockedAndUnreadable getter 회귀 가드. ③ lock_password_screen_test.dart 신규 — LockPasswordBody 위젯에 fake LockSnapshot 3상태(notConfigured/configuredAndUnlocked/configuredButLocked) 주입해 제목·CTA 라벨·아이콘 회귀 가드 + 종이 백업 권장 카드 노출. _Body → LockPasswordBody public + _LockSnapshot → LockSnapshot public (테스트 가능성 확보). ④ RLS 침투 테스트는 PR18-E로 묶음 — V1.0 단일 사용자 환경에서는 다른 사용자 컨텍스트가 없어 클라이언트 단위 테스트로 RLS 침투 본격 검증 불가. PR18-A follows + 친구 사용자 컨텍스트 생긴 뒤 PR18-E의 X-feature 매트릭스(잠금×친구×캘린더 3축, DECISIONS 2026-05-18 P2)에 묶어 통합 검증. V1.0 출시 전 수동 운영 확인 필수 — Supabase 대시보드 SQL 에디터에서 (a) 본인 토큰 SELECT 잠금 quote 정상 (b) anon 키 SELECT 0 row (c) 잠금 quote의 text 컬럼은 NULL + text_encrypted 컬럼은 NOT NULL. 합산: flutter analyze clean. flutter test 228/228 통과(PR16-D 218 + 신규 10). release APK 67.1MB·55.6s 통과(feedback_release_only_traps 패턴 회피). 다음 세션 = PR18-A 진입 — follows 테이블 + profiles.is_library_public + profiles.public_handle text unique + RLS 정책 3종 + profiles SELECT RLS 좁힘 마이그레이션 1장 + follow_repository 코어. E2EE 트랙(PR16 시리즈) 완전 마무리, 친구 서재 탐험 트랙 시작.
PR18-A 산출 — follows 마이그레이션 + follow_repository 코어 (2026-05-18, git only, push 보류): ① 마이그레이션 1장 supabase/migrations/20260518120000_follows_and_public_profile.sql 작성 — git only / 원격 DB push 보류, PR18-B 시작 시 첫 작업 (PR16-A 패턴 일관). 내용: profiles.is_library_public boolean not null default false 추가 / profiles.public_handle text unique 추가(V1.0.1 hotfix 슬롯, DECISIONS 2026-05-18 P1) / profiles SELECT RLS 좁힘 — using(true) → using(is_library_public = true OR id = auth.uid()) (본명 노출 원천 차단) / follows(follower_id, followee_id, created_at, PK(follower, followee)) + CHECK follower_id <> followee_id + cascade × 2 / follows_followee_idx (followee_id) 역방향 인덱스 / follows self-only RLS 3종(SELECT 본인 관련 row, INSERT auth.uid()=follower_id, DELETE auth.uid()=follower_id, UPDATE 없음) / quotes_friends_read SELECT 정책 OR 추가(친구 + is_library_public=true + is_private=false — 잠금 hard exclude DB 단 강제) / user_books_friends_read SELECT 정책 OR 추가(친구 + 공개 프로필, 책은 별도 게이트 컬럼 없음). ② lib/features/follow/data/follow_repository.dart 신규 — V1.0 코어 3개만: follow(uid) upsert idempotent + 자기 자신 SELF_FOLLOW 차단 / unfollow(uid) DELETE(없는 row도 무해) / isFollowing(uid) → bool (미로그인 시 false, 정책 거부도 false — UI 단순화). 검색·카운트·리스트는 PR18-B. FollowRepositoryException 코드(NOT_AUTHENTICATED·SELF_FOLLOW·INSERT_FAILED·DELETE_FAILED). ③ lib/features/follow/state/follow_providers.dart 신규 — isFollowingProvider(userId) autoDispose family(PR18-C 친구 프로필 헤더에서 사용) + followRepositoryProvider(앱 1 인스턴스, repository 파일에 정의). ④ Follow 도메인 클래스는 생략 — PR18-A 코어엔 read-back이 없어 직렬화 불필요. PR18-B 검색·카운트 결과에서 List<Follow> 필요할 때 도입. 테스트 생략 — repository 메서드 3개가 모두 SupabaseClient 직접 호출이라 mock 작업 대비 가치 낮음. PR18-B에서 search·count 추가 시 mock 패턴 갖춰 통합 테스트로 묶음. 합산: flutter analyze clean. flutter test 228/228(기존 그대로 — PR18-A 테스트 생략 결정). release APK 67.1MB·9.5s incremental 통과(release-only 함정 회피). 다음 세션 = PR18-B 첫 작업: npx --yes supabase db push로 마이그레이션 원격 적용 → 도메인 Follow + repository 검색·카운트·리스트 확장 → Me “친구 찾기” 활성화 + “내 프로필 공개” Switch + 닉네임 편집 다이얼로그(email local-part 패턴 감지) + Switch 상태 카피 + 홈 빈 상태 친구찾기 CTA + PostHog 선행 이벤트 3종 등재 (DECISIONS 2026-05-18 P1·P2).
PR18-B 산출 — DB push + 검색·토글·닉네임 편집·홈 CTA + 친구 찾기 화면 (2026-05-18): ① npx --yes supabase db push로 20260518120000_follows_and_public_profile.sql 운영 적용 — project ndbvptxwznogcuuumzzh, NOTICE는 기존 정책 drop skip(정상). E2EE 트랙 종료 후 친구 트랙 본격 시작. ② lib/features/profile/{domain,data,presentation}/* 신규 — Profile(id/displayName/avatarUrl/publicHandle/isLibraryPublic) + profile_repository.getMine/updateMine(부분 갱신) + myProfileProvider autoDispose + looksLikeEmailLocalPart 헬퍼(.·_ 감지). ③ follow_repository에 searchByDisplayName 추가 — ilike '%query%' + 클라 .eq('is_library_public', true) 명시 필터(defense in depth, DECISIONS 2026-05-18 P0), ilike 와일드카드(%/_) 사용자 입력 escape. myFollowingCount 추가(홈 CTA 조건부 노출용). friendSearchProvider(query) + myFollowingCountProvider 추가. PR18-B 단계는 도메인 Follow 클래스 생략(read-back 없는 search·count만 — PR18-C에 listFollowing/Followers 추가 시 도입). ④ lib/features/profile/presentation/profile_settings_tiles.dart 신규 — ProfilePublicToggleTile(Switch + 현재 노출 상태 카피 subtitle “현재 공개 — "닉네임"로 검색됨” / “현재 비공개 — 검색에 표시 안 됨”) + 토글 OFF→ON 직전 닉네임 패턴 감지 시 강제 확인 다이얼로그(본명/직장 이메일 노출 사고 방지). DisplayNameTile + _DisplayNameEditDialog(30 runes 제한). ⑤ me_screen — _SectionHeader('친구') 신설 + _ActionTile(Icons.people_outline, '친구 찾기')(loggedIn) + 설정 섹션에 ProfilePublicToggleTile/DisplayNameTile(loggedIn) + 잠금 비밀번호 ListTile(loggedIn) 묶음. ⑥ lib/features/follow/presentation/friend_search_screen.dart 신규 — /me/friend-search 풀스크린 (셸 외 parentNavigatorKey). 400ms debounce + 검색 전 빈 카피 + 0결과 카피(“"
✅ PR5 남긴 출시 블로커 — 처리 완료 (2026-05-16):
delete-account Edge Function 운영 배포 완료. project ndbvptxwznogcuuumzzh, version 1 ACTIVE. 인증 없는 POST → HTTP 401 UNAUTHORIZED_NO_AUTH_HEADER(Supabase 게이트웨이가 JWT 1차 검증). 로그인된 사용자 JWT로 호출 시 함수 내부 로직 진입 → auth.admin.deleteUser → cascade로 quotes/user_books/profiles/cards 삭제. Apple 5.1.1(v)/Google Play 요구 충족.main /docs), https://tgparkk.github.io/bookquote/terms/ + /privacy/ 둘 다 HTTP 200 + <title> 검증 OK. 스토어 등록 폼·앱 내 [이용약관]/[개인정보처리방침] 외부 링크에서 이 URL 그대로 사용.문서 지도 (2026-05-14 정리): docs/app-scenarios.md(현재 V1 동선 — discovery/flows.md 초안 대체) · docs/db-schema.md(현재 DB 설계서 — discovery/api-design.md·architecture.md 초안 대체) · docs/design/screens/README.md(화면 13개 인덱스 + 구현 상태 + 실제 파일 경로) · docs/design/screens/*.md(화면별 7섹션 명세). discovery/의 architecture·api-design·flows는 시점 고정 초안(상단 배너).
작업 방식 메모: 각 PR = main에 직접 commit+push(Stage 1 패턴), 매 PR마다 flutter analyze + flutter test 통과 + 위젯/유닛 테스트 추가, 마이그레이션은 작성 후 npx supabase db push(supabase 명령은 PATH에 없음 — npx --yes supabase ... 사용, printf 'y\n' |로 프롬프트 통과). 매니저 모드(가상 팀)는 설계 단계용 — 구현 PR은 설계 문서(docs/design/screens/*.md)가 충분히 상세해 직접 구현. 빌드 명령 표준 — flutter run·flutter build apk·flutter build apk --release 모두 항상 --dart-define-from-file=.env.json 동반(빠뜨리면 Env.supabaseUrl/anonKey 빈 문자열 → initSupabase 가 _ready=false로 silent skip → 로그인 버튼·DB 호출 전부 무반응으로 보임. 토스트도 안 뜸). 폰 install은 flutter install 대신 adb install -r build/app/outputs/flutter-apk/app-release.apk로 데이터 보존(adb는 C:/Users/sttgp/AppData/Local/Android/Sdk/platform-tools/adb.exe, -s R3CXA0PANWX로 폰 지정).
secondary100(#FDFCFB)이 화면 배경 secondary200(#FAFAF8)보다 오히려 밝아 가장자리가 사라짐. 결정: app_theme.dart의 cardTheme.color를 secondary300(#F5F1EB)으로 한 톤 다운(테마 1줄). 항목 간격 s3(12)은 유지(사용자 결정 — 색 대비가 경계 역할, 라운드 모서리 노치 회피). 테두리·그림자는 안 씀 — AppShadows.card 토큰은 V1.5 다크모드 때 도입. 실제 문제 위젯은 QuoteListCard(홈 피드·서재 인용목록·친구 프로필 인용구탭) 하나 — cardTheme 한 곳으로 세 화면 동시 해결. 무드 hub·RecallCard·CTA·책 목록은 이미 테두리/틴트/구분선이 있어 손대지 않음.내 친구 N명 타일(myFollowingCountProvider) → 풀스크린 /me/following(MyFollowingScreen + myFollowingProvider, FollowRepository.listFollowing 재사용). ② 빈상태 카피 버그 fix — 공개 프로필 비팔로워는 friend_profile_screen.dart의 _EmptyBooksView/_EmptyQuotesView가 isFollowingProvider 교차해 “팔로우하면 책/인용구를 볼 수 있어요”로 분기(friend-profile.md §7③ 정합). 신규 테스트 4건, 260/260 통과.friend-profile.md §2/§6 명시됐으나 미구현) + 친구 프로필 세그먼트 카운트 라벨(책 N/인용구 N — 같은 RPC 재사용). 1~2일./book/:id?friendId=) / discovery 부분일치 검색(문서 기결정).delete-account Edge Function)는 이미 충족. 한국은 안드로이드 점유율이 높아 안드로이드 단독으로 V1.0 충분 — iOS는 매년 고정비라 반응 확인 후 결정.buildBookPurchaseUrl·buildShareMessage in share_sheet.dart). Book.isbn13 → search.kyobobook.co.kr/search?keyword= 검색 URL(ISBN으로 직접 상품 URL은 불가 → 검색 결과). 직접 입력 책(manual_book_text, isbn13 없음)은 링크 생략. 제휴 ID/UTM은 출시 후 가입해 파라미터만 추가하는 비차단 항목. 제약: 카카오톡은 이미지만 받고 텍스트를 버려 카톡엔 미도달(SMS·메일·다른 앱엔 도달) — 카톡 SDK 메시지 전송은 V1.1.sttgpark@gmail.com 본인 계정 외 수신 안 됨 — 모든 신규 가입 사실상 차단. 추정 원인: ① Supabase 빌트인 SMTP의 무료 플랜 제한(프로젝트 owner 이메일만 수신) ② Resend 등 third-party SMTP 미연동 / sandbox 모드 ③ 이메일 발송 quota 초과. 해결 = Resend·SendGrid·AWS SES 등 SMTP 제공자 연결 + 도메인 검증 + Supabase Authentication > Email Provider 연결. V1.0 출시 전 반드시 해결(이 블로커 미해결 시 스토어 등록·베타 테스트 모두 불가).account_email scope에 카카오 비즈 인증 필요). 본인 카카오 계정 + 비즈 앱 전환·검수 완료 시 V1.0.1로 끌어올림. 카톡 친구 매칭(PR18 친구 발견 funnel의 두 번째 다리 후보)도 카카오 로그인 의존이라 묶어 검토.connectivity_plus 연결-회복 트리거(현재 포그라운드 복귀 시만) + 홈/인용목록에 “동기화 대기 N개” 배너/quote/new?quoteId= 편집 모드) · 카드/목록의 인라인 [무드 변경]ilike) · 홈/책상세 무드 칩 탭 → /library?tab=quotes&mood= navigation.md 파일 첨부(XFile) — 컬렉션 큰 경우 안드로이드 인텐트 한도 회피 / 다크모드 토글([시스템/라이트/다크] + darkTheme 정의) = V1.5 / 섹션 사이 Divider 시각 구분 / 카운트 trailing 변경 후 invalidate(myQuoteCountProvider) 동선(인용구 추가/삭제 시)?from= 보존, 콜백 타임아웃 사유 안내, 책 검색 시트 검색-전 빈결과·ISBN 직접 등록·오프라인 캐시-우선, 스플래시 워드마크·안전망 시간 실측flutter build apk --release에 --dart-define-from-file=.env.json을 빠뜨리면 Env.supabaseUrl/anonKey가 빈 문자열 → initSupabase가 _ready=false로 통과 → 로그인 화면 [이메일로 시작] 버튼이 silent fail(토스트도 안 뜸). Resend SMTP나 이메일 한도 문제 아님. 빌드 명령에 항상 dart-define 동반(작업 방식 메모 참조). 개선 백로그: kReleaseMode && !isSupabaseReady면 스플래시/로그인 화면에 “환경 설정 누락” 진단 배너를 표시해 다음에 헷갈리지 않게.android/app/build.gradle.kts의 release config가 signingConfigs.debug를 그대로 사용해 release/debug 양쪽 서명 키가 같음(개인 빌드 한정). 따라서 빌드 타입 무관 adb install -r로 reinstall하면 데이터 유지. 단 다른 머신에서 빌드한 APK는 debug.keystore가 다르므로 서명 mismatch 가능 → fresh install되어 세션 날아감. 스토어 배포는 별도 release 키를 묶을 때 동일성 자동 보장.flutter_secure_storage 세션이 날아간 사건 — 위 dart-define 누락 + install -r 자체는 성공했으나 Supabase 미초기화로 세션 read 자체 무의미. 재발 시 위 빌드 명령 표준 확인.코드 한 줄 쓰기 전에 시장 검증. 신호 미달 시 컨셉 피벗 또는 폐기 가능해야 함.
docs/discovery/virtual-interviews-2026-05-09.md)docs/discovery/competitor-evaluation.md)docs/discovery/real-interview-guide.md)docs/discovery/landing-page/index.html)Gate: 5명 중 3명 이상이 비슷한 행동을 이미 하고 있고, 2명 이상이 베타 자발적 요청
docs/design/design-system.md)docs/design/tokens.md, docs/design/tokens.ts, lib/core/theme/tokens.dart)docs/design/color-extraction.md)docs/design/templates/01~05-*.md)docs/design/mockups/all-templates.html)Gate: 카드 5개를 인스타에 올렸을 때 본인이 부끄럽지 않은 수준
sessions/2026-05-10-stage1.md)C:\GIT\bookquote, Bundle ID io.github.tgparkk.bookquote)lib/core/theme/tokens.dart)ProviderScope + placeholder 화면, Chrome 빌드 sanity checkAppTheme (ThemeData·TextTheme) 본격 구성 (lib/core/theme/app_theme.dart, app_text_styles.dart)assets/fonts/, pubspec.yaml fonts 섹션)lib/app/router.dart) — StatefulShellRoute 4탭 + auth gate(refreshListenable) + /splash cold-start, placeholder 화면 7개. 위젯 테스트는 cold boot → /auth/login 자동 이동 검증cached_network_image 도입 — lib/features/book/presentation/widgets/book_cover.dart 일원화 wrappersupabase/functions/aladin-search/, JWT 강제, 통일 에러 envelope.env.json (gitignored)에 저장, lib/core/config/env.dart로 로드. 빌드 시 --dart-define-from-file=.env.json 필요ndbvptxwznogcuuumzzh). 초기 스키마는 별도 작업supabase_flutter 초기화 (lib/core/supabase/supabase_init.dart, main()에서 호출, 키 누락 시 graceful skip)lib/features/auth/, supabase/migrations/*profiles* + *handle_new_user_oauth*). 카카오는 V1.5로 미룸 — Supabase GoTrue가 account_email scope를 강제 요청하는데 카카오 개인 앱은 비즈 인증 없이 받을 수 없음 (DECISIONS 2026-05-10 항목)BookSearchSheet(BottomSheet), 캐시 사전조회 + Edge Function, 400ms debounce, 자동 ISBN 분기 토대bookByIdProvider로 실제 데이터 fetch, BookCover 위젯user_books 테이블 + RLS, LibraryScreen이 책 카드 리스트 + pull-to-refresh + FAB → 검색 시트 → addToLibrary + SnackBar 피드백 + invalidateAndroidManifest.xml deep-link intent filter (io.github.tgparkk.bookquote://auth/callback) + iOS Info.plist URL Types + app_links + lib/app/deep_link_handler.dart. 첫 debug APK 빌드 검증supabase init + link + db push + secrets set ALADIN_TTB_KEY + functions deploy aladin-search 모두 통과. 마이그레이션은 YYYYMMDDHHMMSS 14자리 timestamp로 표준 명명docs/discovery/competitor-screen-analysis-2026-05-11.md + competitor-references.html)docs/design/screens/*.md — 그룹 1: 인용입력·인용목록·카드에디터·카드공유·deep link받기 / 그룹 2: 홈·Me·책상세 / 그룹 3 역정리: 스플래시·로그인·콜백·서재·책검색시트). 7섹션 구조(목적·와이어프레임·상태·인터랙션·토큰·재사용·엣지/접근성)docs/design/mockups/screens.html — 전 13화면 와이어프레임 (그룹 1·2·3)flows.md·client-architecture.md 상단에 V1.5 범위 정정 배너 — follow timelineProvider/follows/useTimelineRealtime/publish to followers는 V1.5(코드엔 0), V1 홈 = myQuotesProvider 기반·Realtime 없음, Flow C는 V1.5(deep link 받는 쪽 1탭 담기만 V1), OCR은 폰 기능+클립보드quotes.moods text[] + 앱 enum QuoteMood. 구현 전 최종 확정 가능구현 순서: quotes 테이블 마이그레이션 → quote.dart(@freezed)/quote_repository(listMyQuotes cursor 시그니처)/quote_providers/createQuoteController/quote_outbox → quote_input_screen 재작성 → home_screen 재작성(“내 인용 피드”) → quote_list_view(서재 탭 세그먼트) → me_screen 보강 → book_detail_screen 보강.
supabase/migrations/20260512120000_quotes.sql(book_id nullable on delete set null, manual_book_text, text CHECK 1~2000, page CHECK >0, source manual/clipboard, moods text[], RLS 4정책, 인덱스 3개) remote 적용 완료. features/quote/{domain,data,state} — Quote/QuoteInput/QuoteSource/QuoteMood + QuoteMoodListConverter, QuoteRepository(create/update/delete/getById/listMyQuotes cursor-after + moods overlaps), QuoteOutbox(SharedPreferences, 사용자별 키), bookQuotesProvider/quoteByIdProvider/createQuoteControllerProvider. pubspec: shared_preferences·connectivity_plus. quote_model_test 7개/quote/new[?bookId=]) — 본문 멀티라인 + 글자수 카운터 + 클립보드 붙여넣기 감지 배너(Clipboard.hasStrings) + 책 연결(showBookSearchSheet 재사용 — _onPick의 잘못된 “서재 추가” 토스트 제거) + 페이지·무드 칩(최대 3개) + draft 자동저장/복원 + PopScope 폐기 확인 + “카드 만들기 →”(pushReplacement → /quote/:id/card) / “저장만 하기” + 오프라인 아웃박스 큐잉. presentation/widgets/mood_chips.dart(moodColors 단일 정의처), data/quote_draft.dart. quote_input_screen_test 3개quote_feed_provider(Notifier<AsyncValue<List<QuoteWithBook>>> — cursor-after 무한스크롤 누적 + removeLocal 낙관적 삭제, NotifierProvider 비-autoDispose), quote_repository.listMyQuotesWithBook(*, book:books(*) 임베드 — N+1 회피, QuoteWithBook 레코드), quote_list_card.dart(홈·인용목록 공유 위젯 — 접힘/펼침, 무드 뱃지, [카드 만들기]/[삭제]), home_screen.dart(ConsumerStatefulWidget + 스크롤 무한로드 + RefreshIndicator + 빈 상태 CTA + 에러 재시도 + 카드 탭 펼침 + 삭제 확인 다이얼로그 + 포그라운드 복귀 시 아웃박스 best-effort flush), quote_input_screen은 저장 성공 시 ref.invalidate(quoteFeedProvider). FAB 없음, Realtime 없음. home_screen_test 3개. — 설계: screens/home.md. (인용 목록 위젯 공유 / 무드 칩 navigation·”동기화 대기” 배너·undo는 PR4 또는 후속)library_screen(stub→ConsumerStatefulWidget): SegmentedButton [책]/[인용구], ?tab=quotes&mood=<name> 쿼리로 초기 탭·무드 설정(GoRouterState.of in didChangeDependencies), _ErrorView raw $error 제거 + [다시 시도], 추가 실패 메시지 userMessage화. quote_list_view.dart(ConsumerStatefulWidget, Scaffold 없음): 무드 필터 칩(전체 N + 무드별 개수) + cursor-after 무한스크롤 카드 목록(quote_list_card 재사용) + pull-to-refresh + 빈 상태(전체=”아직 인용구 없어요”++ / 무드=”이 무드 없어요”+전체보기) + 삭제 확인 다이얼로그(→ quoteFeedProvider invalidate + 카운트 갱신). my_quote_mood_counts() RPC(마이그레이션 20260512140000, remote 적용) + quote_repository.getMoodCounts/parseMoodCounts. parseMoodCounts 테스트 2개. 무드별 컬렉션 = 차별화 ④. — 설계: screens/quote-list.md. (인라인 [수정]/[무드 변경]·정렬(책별/페이지순)·검색·홈→서재 무드 칩 navigation·구절수 배지·표지색 띠는 후속)me_screen.dart 재작성(섹션형 ListView): 프로필(이니셜 아바타+이메일+”로그인됨”/”로그인 정보 없음”, 오버플로 처리) + 내 데이터(quote_repository.countMyQuotes()·book_repository.countMyLibrary() count 쿼리 → me_providers의 myQuoteCountProvider/myBookCountProvider, /library?tab=quotes·/library navigation, Markdown 내보내기=markdown_exporter.dart(순수, 책별 그룹+쪽수·무드 메타)+quote_export.dart(전체 페이지네이션 수집→share_plus 텍스트 공유)) + 설정(다크모드 “시스템 설정” 읽기전용 / 알림 “곧 추가될 기능” 비활성) + 정보(앱 버전 package_info_plus → appVersionProvider, 문의 mailto:, 이용약관·개인정보처리방침 외부 링크 url_launcher) + 계정(로그아웃 — quote_outbox.pending() 있으면 경고 다이얼로그 먼저; 회원 탈퇴 2단계=account_deletion.dart(영구삭제 경고+내보내기 권유 → “탈퇴합니다” 타이핑 → dim → delete-account invoke → signOut)). 친구 찾기 = 숨김(빈 onTap 제거). 다크모드 토글 = V1.5. meSessionInfoProvider(세션 요약 — 테스트 override용). pubspec: url_launcher·package_info_plus·share_plus 추가. AndroidManifest <queries>에 https·mailto intent 추가. Edge Function supabase/functions/delete-account/index.ts 작성(JWT로 호출자 확인 → service_role auth.admin.deleteUser → cascade) — 배포는 미완(Stage 5). markdown_exporter 5개 + me_screen 3개 테스트. — 설계: screens/me.mdbook_detail_screen.dart 재작성: _BookBody(헤더 표지·메타·ISBN guard·로그인 시 별점) + _AddQuoteButton(“이 책 인용구 추가” → /quote/new?bookId=) + _LibraryActionButton(isInLibraryProvider EXISTS → 담겼으면 _InLibraryChip ✓, 아니면 [서재에 담기]; 미로그인이면 /auth/login?from= 경유 복귀 — payload 보존; deep link 진입 시 prominent “내 서재에 담기” 1급) + _BookQuotesSection(bookQuotesProvider 재사용 — 헤더 “이 책에서 모은 구절 N” + 최대 3개 QuoteListCard(book:null) + 초과 시 [전체 보기 ▸ → /library?tab=quotes], 부분 실패 격리) + _SharedBanner(?from=share|kakao) + _DescriptionText(LayoutBuilder+TextPainter로 6줄 초과 감지 → 클램프+fade+[더 보기]/[접기]) + _NotFoundView(책 없음 → [홈으로]/[내 서재]) + _ErrorView([다시 시도]) + _OverflowMenu(담긴 책이면 ⋮[서재에서 빼기] 확인 다이얼로그). raw $e 미노출. AppBar ← = canPop ? pop : go('/'). book_repository.isInLibrary + isInLibraryProvider 신규, router.dart /book/:id builder가 ?from= 전달. deep_link_handler 일반화 — _handle(uri, cold:): auth code면 getSessionFromUrl(기존), 아니면 _routeFor(://book/:id?from= → /book/:id?from=)로 매핑 → 워밍이면 router.go, 콜드면 _pendingRoute 보류 → 스플래시 _resolve가 consumePendingRoute로 소비. _seen set으로 URI 1회 consume. BookquoteApp → ConsumerStatefulWidget, initState서 attachRouter. book_detail_screen_test 7개. — 설계: screens/book-detail.md · deep-link-receive.mduser_books.rating smallint 1~5(마이그레이션 20260512130000, remote 적용), book_repository.setMyRating/getMyRating, myRatingProvider, StarRating 위젯(읽기전용/인터랙티브, 재탭=지우기), book_detail_screen 헤더에 별점 행(로그인 시만) + raw $e 노출 제거. star_rating_test 4개. 반쪽 별은 V1.5 (DECISIONS 2026-05-13)QuoteOutbox.flush는 PR3에서 배선됨. connectivity_plus 연결-회복 트리거 + “동기화 대기” 배너는 후속(백로그)설계: screens/card-editor.md + screens/card-share.md. 텍스트 위치 앵커(상/중/하)는 V1.5(V1은 폰트 크기 ±·정렬만). 표지 없는 책에서 T4 = 비활성화. DECISIONS 2026-05-12.
sealed class CardTemplate ×5, 위젯 트리 — CustomPaint 아님)palette_generator → ExtractedPalette, palette_service 메모리 LRU 캐시, ensureContrast WCAG AA 4.5:1, 채도<10 폴백)card_renderer — RenderRepaintBoundary.toImage, pixelRatio = ratio.size.width / boundary.size.width, 폰트 로드 보장 endOfFrame, ui.Image.dispose. pubspec: path_provider 추가. gal은 V1.1)share_sheet.dart 4버튼 + V1은 모두 SharePlus.share(ShareParams(files:[XFile])) OS 시트, 카카오 SDK 메시지 카드는 V1.1)QuickShareScreen 풀스크린 route + draft/추천 자동 적용 + 자동 시트. QuoteListCard 펼침 [📤 바로 공유]/[✏ 카드 디자인]/[삭제] 위계 재조정cards 테이블 (design jsonb, on delete cascade auth.users — 탈퇴 정합) + 공유 시점 비차단 INSERT (card_repository.recordShare fire-and-forget). 마이그레이션 원격 적용 완료_undoStack+⤺ AppBar). B=폰트 ±(fontStep int±3, [A−][A+]) + 따뜻 카드 대비 보강(ensureContrast). C=5스와치(paletteSlotIndex+applyPaletteSlot) + “다른 느낌 ↻”(cycleTemplate 명시 노출). D=auto-fit 경고(비율별 charCount 휴리스틱 + 추천 비율 1탭). E=접근성(_Swatch hit area 48dp + 카드 미리보기/_MiniCard Semantics 라벨). 골든 12장은 별도 commit으로 사전 완료deep_link_handler 일반화(/book/:id 라우팅 + payload 보존 + 1회 consume) PR6에서 완료. 책 상세 “내 서재 담기” 1탭도 PR6에 있음. 잔여: 미로그인 복귀 후 자동 담기(현재는 재탭) + 받은 인용구 카드 풀스펙(quoteId는 RLS상 받는 쪽이 못 읽어 V1.5 — sender 이름·인용구 복제) + (V1.5) Universal/App Link. 설계: screens/deep-link-receive.mdV1.0 출시 전 마지막 인프라 단계 — “데이터 주권” 차별화 메시지를 기술적으로 진실하게 만든다(운영자도 못 봄). 선택적 E2EE(잠금 토글), 메타데이터는 평문 유지. 설계 근거 = DECISIONS 2026-05-17.
lib/features/crypto/{domain,data}/*(KeyDerivation·QuoteCipher·KeyService·CryptoEnvelope). 단위 테스트 9건. AndroidManifest allowBackup="false". 위 PR16-A 산출 항목 참조.CryptoEnvelope.fromRow/toInsertRow + EnvelopeRepository 신규. Quote.text nullable + isPrivate 추가. QuoteRepository + QuoteOutbox 모두 is_private 분기 — outbox는 enqueue 시점에 미리 암호화해 prefs에 평문 무. cards.design jsonb는 quote text 사본 없음 검증 완료. 위 PR16-B 산출 항목 참조.quote_input_screen 잠금 토글, 첫 잠금 모달(영구 손실 경고 + [잠금 비밀번호 설정]/[종이 키 백업] 분기), 잠금 인용구 공유 직전 확인 모달(“이미지에는 평문이 박힙니다”), quote_list_card·recall_card 🔒 배지, card_editor_screen·quick_share_screen 복호화 진입(실패 시 fallback view “이 기기에서 잠긴 인용구”).lib/features/me/lock_password_screen.dart 신규: 비밀번호 설정·변경(현재 비밀번호 검증 → wrap_key 재파생 → envelope UPDATE, 인용구 재암호화 0), K 백업 QR(qr_flutter base64+QR), K 가져오기(mobile_scanner QR 또는 base64 입력, fingerprint 비교), 새 기기 첫 잠금 인용구 접근 시 패스프레이즈 입력 모달. 카메라 권한 + ProGuard rules.delete-account 흐름에 KeyService.deleteAll() 호출 추가, AndroidManifest android:allowBackup="false" 또는 dataExtractionRules로 secure_storage 경로 제외(필수 — 안 하면 Google Drive로 키 새서 E2EE 무력), release APK로 전 동선 실기기: 토글→작성→재시작→앱 데이터 삭제→sentinel→키 import→복원. [[feedback-release-only-traps]] 패턴 적용.received_cards 테이블도 V1.5)supabase/functions/delete-account/index.ts 작성+운영 배포 완료(2026-05-16, project ndbvptxwznogcuuumzzh, version 1 ACTIVE). JWT로 호출자 확인 → service_role auth.admin.deleteUser → cascade로 quotes/user_books/profiles/cards(cards는 PR11에서 on delete cascade auth.users 챙김) 자동 삭제. Me 화면 2단계 확인 후 invoke. Apple Guideline 5.1.1(v) + Google Play 요구 충족. 향후 함수 코드 변경 시 printf 'y\n' | npx --yes supabase functions deploy delete-account 재배포.docs/terms/index.html + docs/privacy/index.html + GitHub Pages 활성화 완료(2026-05-16, 저장소 Settings > Pages, Source = main /docs). 라이브 URL: https://tgparkk.github.io/bookquote/terms/ + /privacy/, 둘 다 HTTP 200 + 컨텐츠 검증 OK. 스토어 등록 폼·앱 외부 링크에 이 URL 사용. 기존 tgparkk.github.io User Pages와 별개 Project Pages이므로 충돌 없음.