bookquote

Stage 진행 체크리스트

마스터 플랜(docs/PLAN.md)에서 추출한 단계별 작업과 현재 상태. 완료한 것은 [x], 진행 중은 [~], 폐기는 [-]로 표시한다.

총 14–21주 (3.5–5개월) 목표. 사용자의 모토는 “서두르지 않고 고득하게”.


▶ 좋아요 + 알림 + FCM 백로그 (2026-06-09 설계 확정 — DECISIONS 2026-06-09)

매니저 모드 4팀 협의 산출. 출시에 끼우지 않고 LA~NB는 출시 후 첫 마이너, PA~PC는 그다음. liker 목록 미표시(A안), 본인 콘텐츠는 카운트만.

순서: LA → {NA, LB} → {NB, LC} → PA → PB → PC.


▶ 다음 세션 시작점 (2026-05-28 기준 — V1.0.x 백로그 13 PR commit + push 완료, 마이그레이션 적용 완료, release APK 폰 검증 완료. versionCode 11 비공개 트랙 업로드 + PR4-B·PR5-B·PR7만 deferred)

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) 설치 검증.

1차 배치 — 백로그 PR0~PR8

2차 배치 — 폰 dogfood 피드백 PR9~PR12

3차 배치 — 친구 추가 디스커버리 PR13

진행 산출 (2026-05-28 마무리)

▶ 다음 세션 박태건 손 작업

  1. versionCode 11 상향 (이전 push 10) → AAB 빌드 → Play Console 비공개 트랙 수동 업로드(자동 업로드 셋업은 여전히 대기 중).

▶ 다음 세션 코딩 deferred

유지 — 출시 트랙 항목: 카카오 로그인 재오픈 (V1.0.x, 검수 의존) · iOS 출시 (안드로이드 트랙션 확인 후) · keystore 비밀번호 rotate · Supabase DPA + 키 회전 SOP · pgTAP RLS 단위 테스트 · 레이어 누수 5곳 정리 · PostHog 연동 · release 환경 진단 배너.

광고 도입 결정 (2026-05-28 4팀 협의):


이력: 2026-05-23 (책글귀 재리브랜드 + 카드 폰트 시인성 + Play 비공개 트랙 v4)

2026-05-23 산출 — V1.0 출시 직전 마지막 정렬일:

Play Console 진행 (2026-05-23):

🔑 OAuth SHA-1 2종 정리 (둘 다 Google Cloud Console 등록 완료):

▶ 다음 세션 할 일 (검토 통과 후):

  1. 검토 통과 알림 확인 — Play Console 알림 + 등록한 이메일. 보통 비공개 테스트는 수 시간~24시간.
  2. 본인 폰 검증 — 스토어 검색 “책글귀” → 설치 → 구글 로그인 → 무드 hub·카드 만들기 등 핵심 플로우 확인. Play 앱 서명 SHA-1 등록이 잘 됐는지 여기서 최종 검증.
  3. 테스터 옵트인 링크 공유 — 비공개 테스트 트랙 페이지 하단 “테스터” 탭에서 옵트인 URL 확인 → 테스터들에게 공유.
  4. Play Console 9/11 → 11/11 완성 (출시 후 수정 가능) — 앱 카테고리(도서/참고자료)·연락처(sttgpark@gmail.com) 설정 + 스토어 등록정보 확정.
  5. 데이터 보안 폼 보강 — “비정상 종료 로그·진단” 항목 추가(Crashlytics 반영).
  6. 14일 비공개 테스트 트랙 대기 (개인 개발자 계정 프로덕션 전 의무 가능성) — 그동안 피드백 수집, 필요 시 v5·v6 핫픽스.
  7. 프로덕션 트랙 승격 — 비공개 검증된 AAB를 프로덕션으로.
  8. (출시 후) V1.0.x 카카오 로그인 재오픈 — 이메일 동의항목 검수 → _kakaoLoginEnabled=true. Crashlytics 모니터링(bookquote-aa178).

이력: 2026-05-22 (북로그 리브랜드 + PR25 신고·차단 + PR26 Crashlytics + PR27 친구 둘러보기 — Play Console 앱 생성·9/11, 스토어 등록정보 입력 중)

2026-05-22 산출 — V1.0 출시 작업 집중일:

Play Console 진행 (2026-05-22):

🔑 빌드·OAuth 함정 (기록):

▶ 다음 세션 할 일:

  1. Play Console 마지막 2개 — 앱 카테고리·연락처(카테고리 “도서/참고자료”, 이메일 sttgpark@gmail.com) + 스토어 등록정보(설명·아이콘·그래픽·스크린샷 업로드) → 11/11.
  2. AAB 재빌드flutter build appbundle --release --dart-define-from-file=.env.json. PR27(둘러보기)이 직전 AAB 빌드 이후라 미반영 → 업로드 직전 1회 재빌드.
  3. AAB 업로드 → Play 앱 서명 SHA-1 확보 → Google Cloud Console 등록.
  4. 데이터 보안 폼에 “비정상 종료 로그·진단” 추가(Crashlytics 반영 — 출시 후 수정 가능).
  5. 비공개 테스트 트랙 — 테스터 12명·14일(개인 개발자 계정 프로덕션 전 의무 가능성).
  6. (정리) Supabase 테스트 유저 정리 — 아래 2026-05-21 이력 항목 참조.

이력: 2026-05-21 (PR21 OAuth 랜딩 + 카카오 V1.0 보류 — 콘솔 3종 + SM F956N 실기기 검증)

PR21 OAuth 랜딩 + 카카오 계정 모델 결정 (2026-05-21):

OAuth 후속 To-Do:


이력: 2026-05-19 (PR18-C/D · PR20-A/B/C/D · PR21 코드 · PR22~24)

PR22 + PR23 + PR24 + 잠금 해제 버그 fix 산출 (2026-05-19):

매니저 모드 UX 3팀 6명 재토론 + 사용자 시나리오 4단계(D1·D7·D30·D90) 정합 결과 — 매니저 모드 5건에 누락된 IA 충돌(홈 vs 서재 [인용구])을 박태건 님 직접 발견. 코드 4트랙 동시 진행 (PR21 stash 상태에서 매직링크 코드 위에 작업, OAuth 작업과 코드 영역 안 겹침).

▶ 다음 세션 시작점 (위 마이그레이션 push + OAuth 콘솔 + Play Console 답변 후): git stash popflutter 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 회피.

To-Do 6건 갱신 (PR21로 1·2번 흡수) — 매니저 모드 백엔드 토론 결과 그대로 유지:

PR20-D 산출 (2026-05-19, 홈 친구 최근 활동 1줄 배너 — UX#4 K-factor 다리):

PR20-C 산출 (2026-05-19, sender 컨텍스트 deep link 영속 + K-factor 다리 — UX#3):

PR20-C 산출 (2026-05-19, sender 컨텍스트 deep link 영속 + K-factor 다리 — UX#3):

PR20-B 산출 (2026-05-19, 인용구 텍스트 검색 — UX#2):

PR20-B 산출 (2026-05-19, 인용구 텍스트 검색 — UX#2):

PR20-A 산출 (2026-05-19, 저장→공유 액션 모델 통일 — UX#1):

매니저 모드 UX 종합 (2026-05-19, 모바일 UX 전문가 3명 병렬 점검 — Day 0 onboarding / IA·네비게이션 / D1-D30 retention):

PR20-A 산출 (2026-05-19, 저장→공유 액션 모델 통일 — UX#1):

PR18-D 산출 (2026-05-19, 책 상세 “이 책을 담은 친구 N명” 행 + 시트):

PR18-D 산출 (2026-05-19, 책 상세 “이 책을 담은 친구 N명” 행 + 시트):

PR18-C 산출 (2026-05-19, /u/:userId 친구 프로필 풀스크린):

PR18-C 산출 (2026-05-19, /u/:userId 친구 프로필 풀스크린):

상태: 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.dartboundary.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 _openEditorcontext.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 + enqueueref.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.setTemplatefontStep:0 명시 + _EditorselectTemplate/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.dartgetTemporaryDirectory + 임시 .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.dartLinkedHashMap LRU(maxCacheSize 100, 타임아웃 3s) + getPaletteWithFallback(coverUrl?, templateId) + PaletteGeneratorFactory 주입. palette_providers.dartpaletteServiceProvider(앱 1 인스턴스) + extractedPaletteProvider family(키 = Dart record). screen _PreviewBox/_MiniCardConsumerWidget 전환, .value ?? fallbackFor + AnimatedSwitcher 200ms cross-fade. PR9 산출: state/quote_card_data_provider.dartquoteByIdProvider + 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.dartrenderCardPng({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.dartshareCardImage({XFile file, String? subject}) = SharePlus.instance.share(ShareParams(files:[file])) 단일 wrapper, CardShareException 메시지 래핑. presentation/widgets/share_sheet.dartshowCardShareSheet(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 재사용). QuoteCardDatabookId 필드 추가. 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 _SentNoticeonResetEmail 콜백 + [이메일이 다른가요? 다시 입력] 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_booksstarted_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_booksstarted_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건. _FakeRepoextends Mock implements BookRepository(mockito 패턴 — SupabaseClientGoTrue periodic timer가 widget dispose 후에도 남는 이슈 회피). 17-C(8efd829) library_screen 2→3 세그먼트 + ?tab=calendar 분기. pubspec.yamltable_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.darttable_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 push20260517130000_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 필드 추가. QuoteInputisPrivate 추가. ⑤ QuoteRepositoryKeyService + 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 분기). ⑥ QuoteOutboxKeyService + 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 검증). flushsealed _OutboxEntry로 dispatch — _PlainEntrycreateQuote, _PrivateEntryinsertPrivatePayload. legacy ‘kind’ 없는 entries는 평문으로 해석(PR3 이전 호환). pending()은 평문만 노출(테스트/디버그), pendingCount()는 전체 — pendingOutboxCountProviderpendingCount()로 갱신. ⑦ 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.dartkeyServiceProvider + 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_readis_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.storysideBySide → topBottom으로 분기 + coverPanelSize 380→480 + paddingTop 240→144. 사유 = 9:16 캔버스에서 좌측 380px 표지 패널이 세로 스트립처럼 압축돼 표지 홍보 효과 약화(레이아웃 전문가). _VariantcoverWidth/coverHeight 토큰화 — _CoverPanelisHorizontal ? 300 : 240 매직 넘버 제거, 비율별 표지 치수 한 곳에서 관리. story-topBottom 표지 320×448(기존 sideBySide 300×420보다 살짝 크게). 검증: flutter analyze 3 파일 모두 clean. flutter test 206개 중 warm × 9:16 골든 1건만 깨짐(예상 회귀) → --update-goldenstest/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.dartshowPrivateShareWarningDialog(context) 헬퍼 — 잠금 인용구 카드 공유 직전 1회 노출. “본문은 잠겨 있지만 카드 이미지에는 인용구가 평문으로 박혀요” 경고 + [취소]/[그래도 공유] · barrierDismissible: false로 외부 영역 탭 닫힘 차단. ② QuoteCardDataisPrivate 필드 + isLockedAndUnreadable getter(isPrivate && quoteText.isEmpty) 추가 — quote_card_data_providerquote.isPrivate 전달. ③ card_editor_screen_onShareTap 진입부 잠금 분기 → 경고 모달 → [취소]면 공유 흐름 중단(_isSharing 토글 전). isLockedAndUnreadable이면 _Editor 대신 _LockedView 표시 + bottomNavigationBar(공유 버튼) 숨김. ④ quick_share_screen_share에 동일 경고 분기 · _bootstrap에서 isLockedAndUnreadable이면 _autoSheetTriggered=true 설정해 자동 시트 차단 + _buildBody에서 _LockedView early return. ⑤ quote_list_cardquote.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_testshowPrivateShareWarningDialog 그룹 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.dartChangePasswordDialog 추가 — 현재/새/확인 3필드. submit = openEnvelope(현재)rewrap(K, 새)updateWrapcacheMasterKey. K는 그대로라 인용구 재암호화 0. SecretBoxAuthenticationError → “현재 비밀번호가 달라요” · 새 비밀번호 6자 runes + 일치 검증(FirstLockDialog 패턴 일관). ③ _LockedViewonUnlock 옵션 콜백 — 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_testChangePasswordDialog 그룹 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=nullisLockedAndUnreadable=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 라벨·아이콘 회귀 가드 + 종이 백업 권장 카드 노출. _BodyLockPasswordBody public + _LockSnapshotLockSnapshot 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 push20260518120000_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_repositorysearchByDisplayName 추가 — 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결과 카피(“"\"을 찾지 못했어요") + 결과 ListTile(CircleAvatar 이니셜 + display_name + 인라인 [팔로우]/[팔로잉] 토글). **친구 프로필 진입(`/u/:userId`)은 PR18-C에서 본격** — 본 화면은 팔로우 토글까지만. ⑦ `lib/features/home/presentation/widgets/friend_search_cta.dart` 신규 — RecallCard 톤(accent50 InkWell)으로 "친구를 찾아 서재를 구경해 보세요 →". `home_screen`이 1차 분기(`feed.value not empty`)로 노출 + CTA 자체 2차 분기(`myFollowingCount == 0`)로 노출. **인용구 0개일 땐 빈 상태 CTA가 우선**(qa-tester). ⑧ `router.dart` — `/me/friend-search` GoRoute 추가(셸 `/me` 하위). **PostHog 선행 이벤트 3종**(DECISIONS P2)은 Stage 5 PostHog 작업과 묶기 — V1.0 출시 전 가족 5명 가입 단계에 임계 ≥40% 발생하면 재검토. **B/C 닉네임 게이트**는 PR18-C 첫 작업(닉네임 미설정 풀스크린 봉쇄). **테스트**: `me_screen_test`의 "친구 찾기 V1엔 숨김" 검증 갱신(`findsNothing` → `findsOneWidget`). 신규 위젯 단위 테스트는 SupabaseClient mock 패턴 새로 갖춰야 해서 PR18-C/D 묶음. **합산**: `flutter analyze` clean. `flutter test` 228/228 통과(기존 갱신 1건). release APK 67.2MB·52.8s 통과(release-only 함정 회피). **다음 세션 = PR18-C** — `/u/:userId` 친구 프로필 화면 + `_NicknameGateView` 풀스크린 게이트 + `FollowState` 분기 카피 + 본인 진입 라우터 `_redirect` 가드 + `listFollowing`/`listFollowers` repository 확장.

✅ PR5 남긴 출시 블로커 — 처리 완료 (2026-05-16):

문서 지도 (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로 폰 지정).

후속 작업 백로그 (Stage 2 마무리 전후 — 우선순위 낮음)


Stage 0a — Validation (2–3주, 진행 중)

코드 한 줄 쓰기 전에 시장 검증. 신호 미달 시 컨셉 피벗 또는 폐기 가능해야 함.

Gate: 5명 중 3명 이상이 비슷한 행동을 이미 하고 있고, 2명 이상이 베타 자발적 요청

Stage 0b — UX & Design (1–2주, 부분 완료)

Gate: 카드 5개를 인스타에 올렸을 때 본인이 부끄럽지 않은 수준

Stage 1 — 기반 (3–4주, 완료 — 세션 로그: sessions/2026-05-10-stage1.md)

화면 세부 설계 (Stage 0b 연장 — 2026-05-12 완료)

Stage 2 — 인용구 입력 (2–3주) — 진행 중

구현 순서: quotes 테이블 마이그레이션 → quote.dart(@freezed)/quote_repository(listMyQuotes cursor 시그니처)/quote_providers/createQuoteController/quote_outboxquote_input_screen 재작성 → home_screen 재작성(“내 인용 피드”) → quote_list_view(서재 탭 세그먼트) → me_screen 보강 → book_detail_screen 보강.

Stage 3 — 카드 (3–4주, 가장 공들일 단계) — PR7~10 완료, PR11~12 대기

설계: screens/card-editor.md + screens/card-share.md. 텍스트 위치 앵커(상/중/하)는 V1.5(V1은 폰트 크기 ±·정렬만). 표지 없는 책에서 T4 = 비활성화. DECISIONS 2026-05-12.

Stage 4 — 인용구 E2EE + 베타 (3–4주, 출시 한 달 연기 합의 2026-05-17)

V1.0 출시 전 마지막 인프라 단계 — “데이터 주권” 차별화 메시지를 기술적으로 진실하게 만든다(운영자도 못 봄). 선택적 E2EE(잠금 토글), 메타데이터는 평문 유지. 설계 근거 = DECISIONS 2026-05-17.

베타 + dogfooding

V1.5로 미룸 (변경 없음)

Stage 5 — 출시 (1–2주)