이 프로젝트의 주요 결정사항을 날짜 역순으로 기록한다. 한 결정당 한 섹션, 핵심만. 새 결정은 맨 위에 추가한다. 결정을 뒤집을 때는 기존 항목을 수정하지 말고 새 항목으로 추가하면서 어떤 항목을 대체하는지 명시한다.
quote_like·review_like·follow 3종. 매니저 모드 4팀(Dart·UI/UX·기획·QA) 병렬 협의 산출.recent_public_book_reviews)라 좋아요가 product로 자연스럽고, 인용구는 “사적 수집” 멘탈모델이라 작성 위축 리스크가 있어 본인 콘텐츠엔 좋아요 버튼 미노출 + 카운트만으로 방어.20260609111229_likes.sql — 2026-06-09 원격 적용 완료):
quote_likes / review_likes 2개 분리(FK·RLS 단순).(user_id, book_id) 복합키라 FK 타깃 없음 → book_reviews.id uuid unique surrogate 추가(1책당 1개 불변식은 기존 PK가 계속 강제).(target_id, liker_id) — 더블탭·오프라인 재전송 무해.liker_id = auth.uid() and exists(select 1 from <대상> where id = ... and user_id <> liker_id) — 하위 exists가 quotes/book_reviews RLS를 상속해 차단·잠금·비공개·비공개프로필·self-like를 한 subquery로 게이트(별도 차단 필터 추가 금지 = 드리프트 0).rls_likes.test.sql).quote_like_counts/review_like_counts(SECURITY INVOKER, RLS 자연 게이트) RPC가 (id, n, liked_by_me)만 반환 — liker_id 미반환. SELECT 정책은 본인 like + 가시 대상의 like 행만 허용(INVOKER 집계용). 대안 B안(SECURITY DEFINER 카운트 + own-only SELECT로 liker_id를 DB단까지 완전 차단)은 가시성 술어를 RPC 안에 복제해야 해 드리프트 리스크 → 코드베이스 전체의 INVOKER + 자연 게이트 일관성 우선해 A안 채택.notifications(recipient_id, actor_id, type, quote_id?, review_id?, read_at). 적재는 클라 금지 → DB 트리거(SECURITY DEFINER)가 quote_likes/review_likes/follows insert 시 수행(recipient = 대상 소유자). unlike(취소)는 안 읽은 알림 삭제(스팸 방지). 묶음 표시(“외 N명”)는 저장이 아니라 read-time RPC 집계. SELECT는 수신자 본인만, update는 read_at 처리만. 차단 상대 알림은 read-time RPC가 profiles RLS로 자연 제외.firebase_messaging 추가 + device_tokens + Edge Function push-notification이 Database Webhook→FCM HTTP v1, 서비스계정은 Edge secret). Firebase 프로젝트는 Crashlytics로 이미 존재 → Cloud Messaging만 활성화. opt-in 마스터 토글 + 타입별 on/off(profiles.push_*) + POST_NOTIFICATIONS(Android 13+)는 런치가 아닌 맥락에서 요청(Play UX·메모리 계정 리스크 정신). iOS·APNs는 더 뒤로.like_count 컬럼+트리거로 카운트 RPC 대체(인터페이스 유지 시 클라 무변경).avatar_url에 OAuth picture/avatar_url을 저장하던 것을 중단하고, 기존 행의 avatar_url을 전부 null로 백필. 마이그레이션 20260529130000_drop_oauth_avatar.sql 한 장. Dart 변경 0(모든 아바타 위젯이 이미 avatar_url 비면 닉네임 이니셜 동그라미로 폴백).random_display_name/_for_all + 패턴 백필)이 OAuth name을 신뢰하지 않고 display_name을 무작위로 강제했지만 avatar_url은 손대지 않았다. 구글은 사진 미설정 사용자에게 실명 첫 글자가 박힌 기본 아바타 이미지를 주므로, 무작위 닉네임으로 막은 실명이 친구 프로필·팔로워 시트·책 상세·활동 피드·친구 검색·내 팔로잉·독서 순위·차단 목록·책 리뷰 등 거의 모든 화면의 원형 아바타로 그대로 노출됐다(사용자 보고 2026-05-29). 닉네임 무작위화와 정확히 같은 privacy 사고 클래스인데 빠져 있던 구멍. V1.0엔 아바타 업로드 기능이 없어 avatar_url은 항상 필터 안 된 구글 사진뿐 → 휴리스틱 없이 전부 비워도 안전(직접 올린 실사진도 사라지지만 그런 경로 자체가 없음).avatar_url을 다시 사용자 통제 하에 채운다.display_name ilike 검색의 silent killer(critic + qa-tester): 본명 회피를 유도하면 닉네임이 의미없는 문자열로 수렴 → 검색 무력화 → 친구 발견 funnel이 deep link 단일 경로로 축소 → 단방향 follow의 “모르는 사람 발견” 가치 0 ② PR18-B 닉네임 prerequisite가 강제 게이트 아님(planner + qa-tester): PR18-C가 PR18-B 없이 머지되면 본명 노출 사고가 곧 신호 ③ 친구 진입점 전무 + 선행 신호 부재(planner + qa-tester): 현재 me_screen “친구 찾기”는 V1엔 숨김. PR18-B에서 활성화하지만 그동안 진입점 0. 재검토 트리거가 모두 사후 신호. 출시 전 가족 5명 단계에서 잡을 선행 지표 부재.profiles SELECT RLS를 using(true) → using(is_library_public = true OR id = auth.uid())로 좁힘. 비공개 프로필이 검색·/u/:userId에 0 row로 응답. 본명 노출 원천 차단.follow_repository.searchByDisplayName 쿼리에 클라이언트 단 .eq('is_library_public', true) 명시 필터 추가 — defense in depth(DB+클라 이중 방어)./u/:userId 또는 “내 프로필 공개” 토글 접근 시 풀스크린 “닉네임 먼저 설정해주세요” gate. PR18-C 본 화면 진입 봉쇄.display_name이 email local-part 의심 패턴(.·_·@ 앞 형식)이면 공개 토글 활성화 직전 강제 다이얼로그 + 추천 닉네임 제안. 다이얼로그 OK 연타 회피 위해 닉네임 입력 확인 후에만 활성화.profiles.public_handle text unique 컬럼을 PR18-A 마이그레이션에 미리 박기 — V1.0엔 미사용, V1.0.1에 “@핸들 검색” 경로로 활성화. 스키마 마이그레이션 비용 0 추가, 데이터 backfill만으로 hotfix 가능. critic의 닉네임-검색 충돌 funnel 회피 두 번째 다리. unique constraint는 처음부터 박아 향후 핸들 점거 사고 0.<display_name>로 검색됨”. 사용자가 자기 노출 상태를 언제든 인지.FollowState enum 분기 카피(팔로우 전 vs 팔로잉 다른 문구) — designer 손. 인지 부조화(“내 팔로우가 먹혔나?”) 회피. 본문 카피가 상태 설명하므로 SnackBar는 단순 확인만.friend_search_zero_result_exit(검색 0건 후 종료), library_public_toggle_unchanged(Me 진입 후 토글 미변경 종료), book_detail_friend_count_zero(“친구 N명” 0 노출 빈도). 가족 5명 가입 단계에서 임계 ≥40%면 출시 전 재검토 — 사후 클레임이 아닌 선행 신호._redirect 단계로 끌어올림(현 명세는 initState) — 1프레임 흰 화면 깜박임 회피 + RLS 회귀 가드.follow_repository 코어 — 기존(follows + profiles.is_library_public + RLS 3종) + profiles.public_handle text unique 컬럼 추가 + profiles SELECT RLS 좁힘(is_library_public = true OR id = auth.uid()).searchByDisplayName 클라 필터 + 홈 빈 상태 CTA 친구 링크(조건부) + PostHog 선행 이벤트 3종 등재./u/:userId 풀스크린 gate. PR18-B 머지 후, PR18-C 진입 전 별도 sub-PR./u/:userId 친구 프로필 화면 — 기존 + FollowState enum 분기 카피 + 본인 진입 라우터 _redirect 가드.friend_search_zero_result_exit ≥ 40% → PR18-A에 박아둔 public_handle 검색 활성화를 V1.0.1 → V1.0으로 격상.library_public_toggle_unchanged ≥ 60% → 토글 UX 재설계(설명 보강 또는 기본값 변경 검토).book_detail_friend_count_zero ≥ 80% → 친구 N명 표시 UX 재검토(다른 retention 후크 탐색).profiles.is_library_public bool default false(per-quote is_public X) ③ 잠금 인용구(quotes.is_private = true, PR16)는 RLS에서 hard exclude — 친구 화면에 절대 노출 0% 보장 ④ 친구 발견 = display_name 검색 + 카드 공유 deep link → 보낸 사람 서재 두 경로만, 카톡 매칭 X ⑤ 화면 신설 1개(/u/:userId 친구 프로필 read-only) + 기존 화면 갱신 2건(Me “친구 찾기” 활성화 + 책 상세 “이 책을 담은 친구 N명” 1줄). BottomNav 슬롯 추가 X, 홈 피드에 친구 인용구 섞기 X — “내 인용 피드” 정체성 사수.?from=share)는 이미 인프라(PR10·deep_link_handler)가 있어 sender_user_id만 payload에 추가하면 “보낸 사람 서재 보기” 동선 자연. 풀-소셜은 정체성 흔들 위험(2026-05-12 보류 이유) → 단방향 + 부속 진입점 3개로 무게 최소화. PR16(E2EE)의 is_private 정책이 친구 탐험의 “공개 vs 비공개” 정책과 자연 호응 — 둘이 같은 한 달 패키지에 들어가는 게 결합 비용 낮음(친구 노출 게이트를 RLS에 한 번만 박음).is_public) — UI 부담 1번(Me에서 토글), PR16 잠금 토글과 혼동 0(잠금 = “나만 보기”·공개 = “공개 프로필이 ON일 때 친구가 보기”). per-quote는 V1.5 검토 슬롯.follows(follower_id, followee_id, created_at, PK(follower_id, followee_id)) 1테이블. follows_followee_idx 역방향. 차단(blocks)·뮤트는 V1.5.using 조건 is_private = false. 정책 단위로 강제 = 클라이언트 버그가 있어도 DB가 막음. PR18-E에 RLS 침투 테스트로 회귀 가드.(auth.uid() in (select follower_id from follows where followee_id = quotes.user_id) and exists profile where id = quotes.user_id and is_library_public = true and quotes.is_private = false). SELECT 정책 OR로 추가(기존 본인 정책 유지).cards 테이블에 shared_at 외 별도 컬럼 추가 X. 공유 시 deep link URL에 &sender=<user_id> 직접 인코딩(share_service.dart). 받는 쪽 deep_link_handler가 query 파싱 → /book/:id?from=share&sender=<uid> → book_detail의 sender 컨텍스트 배너에 [이 사람 서재 보기] 버튼.display_name이 가입 시 이메일 local-part 자동 채워짐(현 handle_new_user_oauth). 본명 노출 위험. PR18 prerequisite = Me에 “공개 닉네임 편집” UI 필수(기본값=현재 display_name 표시 + “공개될 이름이에요” 안내). is_library_public=true 토글하기 전 닉네임 확인 다이얼로그 강제.is_public bool 토글 → 거부. 입력 화면에 토글 1개 더, PR16 잠금 토글과 혼동, “공개 1개 / 비공개 99개”의 흔한 패턴이 사실 프로필 단위 토글로 충분.follow_requests 테이블 + 거절/대기 UI까지 따라옴. V2 슬롯.friend-explore로 RLS 우회 + 서버 단위 권한 체크 → 거부. RLS가 정통이고 단위 테스트 가능. Edge는 service_role 필요한 경우만.20260518xxxxxx_follows_and_public_profile.sql — follows 테이블(PK (follower_id, followee_id), 둘 다 cascade) + profiles.is_library_public bool not null default false 컬럼 + follows_followee_idx (followee_id) + 새 RLS 정책 quotes_friends_read/user_books_friends_read(둘 다 is_library_public=true + quotes.is_private=false 게이트) + profiles RLS 변경(공개 프로필만 누구나 read, 비공개는 본인만 — 현행 using(true) 좁힘).pubspec.yaml 변경 없음(순수 Flutter + supabase_flutter).lib/features/follow/ 신규 모듈 — domain/follow.dart + data/follow_repository.dart(searchByDisplayName ilike + follow/unfollow/isFollowing/listFollowing/listFollowers/followersCountForBook) + state/follow_providers.dart.lib/features/profile/friend_profile_screen.dart 신규 (/u/:userId) — 공개 책 리스트 + 공개 인용구 무한스크롤(잠금 자동 제외) + [팔로우/언팔로우] 버튼. 비공개 프로필 시 “잠긴 서재” 빈상태.me_screen “친구 찾기” 활성화(현행 숨김 → ListTile 1줄) + 신규 섹션 “내 프로필 공개”(토글 + 닉네임 편집 다이얼로그).book_detail_screen 헤더 메타 다음 “이 책을 담은 친구 N명” 1줄(N≥1일 때만 렌더, 0이면 숨김 — 빈 상태 회피) + 탭 시 시트로 친구 미니리스트.share_service.dart deep link URL에 &sender=<user_id> 추가 + deep_link_handler 파싱.book_detail_screen의 deep link 진입 배너에 sender가 팔로우 중이면 [이 사람 서재 ▸], 아니면 [팔로우 + 서재 보기] 1탭.router.dart에 GoRoute(path: '/u/:userId') 추가. 로그인 가드(deep link도 로그인 필수 — 친구 컨텍스트라).delete-account Edge Function 흐름 변경 없음(auth.users cascade가 follows도 자동 정리 — 양방향 FK 둘 다 cascade).quotes의 is_private 컬럼(PR16 추가 → PR18 RLS에서 게이트로 사용). PR16-A 마이그레이션이 PR18-A에 선행. PR18은 PR16-B(quotes 읽기 측 wiring) 닫힌 다음.is_private=true 카운트도 제외하는지 PR18-E 침투 테스트 필수.is_library_public=true 게이트 통과한 사용자만 집계.follows + profiles.is_library_public + RLS 정책 3종) + follow.dart 도메인 + follow_repository 코어(follow/unfollow/isFollowing만) · 18-B follow_repository 검색·카운트(searchByDisplayName ilike + followersCountForBook + listFollowing/listFollowers) + Me 신규 섹션 “내 프로필 공개” 토글 + “공개 닉네임” 편집 다이얼로그(prerequisite — is_library_public=true 가기 전 강제 확인) · 18-C /u/:userId 친구 프로필 화면(공개 책 + 공개 인용구, 잠금 hard exclude, [팔로우/언팔로우] 버튼) + 라우터 추가 · 18-D 책 상세 “이 책을 담은 친구 N명” 1줄 + 친구 미니리스트 시트 + share_service.dart deep link sender 추가 + deep_link_handler sender 파싱 + book_detail sender 배너에 [이 사람 서재 ▸] 1탭 · 18-E 골든(친구 프로필 비공개·공개 두 상태) + RLS 침투 테스트(잠금 quote 0 row · 비공개 프로필 0 row · 팔로우 안 한 사용자 0 row) + release APK 검증. 각 PR 끝 flutter analyze + flutter test + release APK 빌드 sanity(feedback_release_only_traps 강제).user_books에 started_at date/finished_at date 컬럼 추가(CHECK finished_at >= started_at, 둘 다 null 허용 — 시작만 입력하고 아직 안 다 읽은 케이스가 핵심) ② 책 상세에 별점 행 아래 “읽기 시작 / 다 읽음” 1탭 입력 영역(오늘/어제/직접선택 칩, 입력 후 칩 형태 표시 + 재탭=지우기) ③ 서재 탭을 [책]·[인용구]·[캘린더] 3 세그먼트로 확장 — 캘린더 셀 마커는 시작(accent200 outline)·완독(accent500 채움) 두 색 분리, 셀 탭=그 날 책 리스트. 동시에 글로벌 검증된 마찰 감소 UX 3건 채택 — (a) 날짜 기본값=오늘, DatePicker 숨김(Letterboxd·StoryGraph — 왓챠피디아 평가일=감상일 실패 지점 직격) (b) 별점 재탭=해제 패턴을 캘린더 칩에도 일관 적용(이미 StarRating에 구현됨) (c) 표지 long-press → 액션시트(V1.0.1 후속 PR로 분리).started_at 없으면 둘 다 today로 set + Toast “함께 시작일도 오늘로 저장했어요”(StoryGraph 자동 기입 패턴, 과거에 읽었던 책 등록 마찰 0).table_calendar ^3.x(Flutter ecosystem 표준, 자체 구현 대비 -2~3일).reading_status 컬럼(reading/finished/wishlist, 기본값 reading) — 시작만 입력=reading 유지, 완독 입력=finished 자동 갱신. UI에 한 번도 안 쓰이고 있던 컬럼을 캘린더와 묶어 활성화.quotes.created_at 활용, 즉시 가능) → 거부. 우리 본진을 또 다른 단면으로 보여줄 뿐 차별화 없음.20260518xxxxxx_user_books_reading_dates.sql — started_at/finished_at date 컬럼 + CHECK + partial index 2개 ((user_id, finished_at desc) where finished_at is not null·동 started_at).pubspec.yaml: table_calendar ^3.x 추가. release APK 검증 필수(feedback_release_only_traps 강제).book_repository에 setReadingDate({bookId, kind: started|finished, date?}) — setMyRating의 upsert + auto-add-to-library 패턴 재사용. date=null이면 그 컬럼만 unset.library_screen 세그먼트 2→3개. library.md의 V1.5 보강 권고였던 [인용구] 세그먼트도 PR17과 묶어 V1.0으로 끌어올림(서재 화면 두 번 건드림 회피).book_detail_screen — 별점 행 아래 신규 위젯 _ReadingDatesRow.user_books 컬럼 추가만이라 스키마 충돌 0. 책 상세 헤더가 별점+읽기 날짜+E2EE 잠금 토글로 합류 — 디자인 검토 1회 필요.book_repository.setReadingDate · 17-B 책 상세 _ReadingDatesRow · 17-C 서재 3 세그먼트화 + calendar_segment.dart(table_calendar) + 이전 V1.5 보강 [인용구] 세그먼트 합본 · 17-D 골든 + release APK 검증. 각 PR 끝 release 빌드 검증.text + manual_book_text만 암호화, 메타데이터(moods/page/book_id/created_at)는 평문 유지(무드 GIN 인덱스·my_quote_mood_counts RPC·홈 피드 그대로). 마스터키 K(32B 랜덤)는 flutter_secure_storage에 캐시. 다기기는 envelope 암호화 — 사용자가 설정한 “잠금 비밀번호”로 PBKDF2-HMAC-SHA512(600k iters) wrap_key 파생 → K_wrapped만 새 테이블 user_crypto_envelopes에 서버 저장. 비상 백업으로 K 자체를 QR/base64 종이 인쇄. 기존 평문 인용구는 그대로(is_private=false).service_role·Supabase Studio·pg_dump로 운영자(나)가 평문 접근 가능. PR15-A의 “데이터 주권” 차별화 메시지를 기술적으로 진실하게 만들어야 거짓말이 안 됨. 출시 후 E2EE 추가하면 “그 사이 운영자가 봤다”는 사실이 안 지워짐. → 출시는 한 달 미루더라도 V1.0에 포함하기로 합의.cryptography 패키지(순수 Dart, native 의존 0).INTERNET·debugNeedsPaint 사건 패턴) 회피 위해 거부.crypto_version smallint 컬럼으로 V2 알고리즘 회수 슬롯 확보.user_crypto_envelopes 테이블 RLS 본인만, lazy 생성(첫 잠금 시도 시점). “잠금 비밀번호”는 Supabase 매직링크 로그인과 명확히 분리된 독립 항목 — UI에 “이 비밀번호는 서버가 모릅니다” 명시.ilike 검색·서버 통계 전부 죽음, V1.5 텍스트 검색 백로그와 충돌.20260517_quotes_e2ee.sql(text_encrypted bytea·manual_book_text_encrypted bytea·crypto_version smallint·is_private boolean default false 추가, text·manual_book_text NOT NULL 해제, CHECK 재정의, quotes_user_private_idx (user_id) where is_private = true partial index) + 20260518_user_crypto_envelopes.sql(envelope 테이블 + RLS 본인만 select/insert/update).cryptography ^2.7.0, qr_flutter(백업 QR), mobile_scanner(가져오기 스캔). 카메라 권한 + ProGuard rules 점검.android:allowBackup="false" 또는 dataExtractionRules로 flutter_secure_storage 경로 백업 제외 — 안 하면 Google Drive로 키 새서 E2EE 무력화.delete-account Edge Function 흐름에 클라이언트 KeyService.deleteAll() 추가(envelope row는 auth.users cascade로 자동).cards.design jsonb에 quote text 사본이 들어가는지 PR16-B에서 검증(들어가면 그 경로도 암호화 대상).print 호출 grep 필요).flutter_secure_storage·mobile_scanner iOS 동작 점검).cryptography 패키지가 Argon2 지원 추가 시 → KDF 마이그레이션 검토(crypto_version 슬롯 활용).feedback_release_only_traps 패턴 강제. Stage 4에 편성, 출시 한 달 후로 일정 합의(2026-05-17).user_books.rating (정수 1~5, nullable)user_books에 컬럼 추가(rating smallint check between 1 and 5, nullable=미평가). 별도 book_ratings 테이블 X. 별점을 매기면 그 책이 자동으로 내 서재에 들어옴(setMyRating = upsert {user_id, book_id, rating} onConflict — addToLibrary는 rating 미포함 upsert라 기존 별점을 안 건드림). 별점 지우기 = rating=null update(서재에는 그대로). 마이그레이션 20260512130000_user_books_rating.sql remote 적용 완료.numeric(2,1)로 확장(데이터 마이그레이션 쉬움).book_detail_screen 헤더에 StarRating 행(로그인 시만 — /book/:id는 게스트 허용이라 비로그인은 별점 행 숨김). 탭=설정, 현재 별점 별 재탭=지우기. repo.setMyRating → ref.invalidate(myRatingProvider(bookId)) + myLibraryProvider. book-detail.md 설계 문서에 반영.매니저 모드(UI/UX·기획·Dart·QA) 협의 후 competitor-screen-analysis-2026-05-11.md §7 미해결 5건 중 4건 + 신규 2건 결정. 근거 상세는 docs/sessions/2026-05-12-screen-design-b.md.
docs/design/templates/01~05.md가 이미 고정 좌표 모델(quoteArea y=192 등)이라 앵커를 넣으면 5종 명세를 “정렬 기반”으로 재작성 + 디자인팀 재합의 필요 → V1 차별화(①②④)와 시간 경쟁. Tezza/Unfold의 자유 배치가 미관 깨고 신규 사용자 헤매게 한 사례(competitor §2.5)와도 어긋남. 단 card_editor_controller의 텍스트 위치는 지금부터 상대좌표(0~1)로 직렬화 — V1.5에 앵커 3지점 스냅 붙일 때 마이그레이션 0. (card-editor.md §7 미결 1 → 해소)cover_url == null)에서 T4(표지발췌): 비활성화. 썸네일 회색 + “표지가 필요해요” 오버레이(templates/04.md의 showTemplateDisabledOverlay), 나머지 4종(T1/T2/T3/T5)은 정상 제공 + (가능하면) “표지 추가하기” 인라인 액션으로 ISBN 재검색 유도(막다른 골목 금지). 이유: T4의 정체성이 “이 색이 이 책 표지에서 나왔다”는 바이럴 순간 — 표지 없는데 단색 그라데이션 degrade하면 그 약속이 거짓이 되고 T1/T3와 시각 구분도 안 됨. (card-editor.md §7 미결 2 → 해소)StatefulShellRoute state). (quote-list.md §1 결정 대기 → 해소: (b)안 채택)/의 “받은 카드 함”: V1엔 안 넣음. V1 홈 = 순수 “내 인용 피드”. V1.5에 received_cards/received_books 테이블 1개 + deep link 핸들러 INSERT로 추가. 이유: V1 deep link 수신 흐름은 deep-link-receive.md 명세상 “책 상세 + user_books 담기”이지 “카드를 내 계정에 복제”가 아님 — “받은 카드”의 영속 저장소가 V1에 없다. follow 타임라인은 V1.5에 같은 피드에 합쳐 진화. flows.md/client-architecture.md의 timelineProvider(follow 의존)·quotes INSERT 시 publish to followers Realtime은 코드에 0 — “제거”가 아니라 그 문서들의 해당 절을 “V1.5”로 마킹하는 게 작업. home_screen.dart 재작성 시 Realtime 구독 코드 금지(Realtime은 V2 — DECISIONS 2026-05-10).quote_repository.listMyQuotes cursor 시그니처 확정. 홈·인용 목록·책 상세 셋이 다 호출하므로 한 번만 정의: listMyQuotes({String? bookId, Set<QuoteMood>? moods, ({DateTime createdAt, String id})? after, int limit = 15}) — cursor-after(created_at + id), offset 금지(DECISIONS 2026-05-10). 누적 상태는 Notifier<AsyncValue<List<Quote>>> + _isLoadingMore 가드 패턴(createQuoteController와 결). bookSearchPagedProvider는 README 주석에만 있고 코드에 없으므로 “참고 구현”으로 못 씀 — cursor-pagination은 그룹 1에서 처음 짜는 패턴.shared_preferences(또는 hive)에 JSON 리스트(아웃박스)로 들고 있다가, 앱 포그라운드 복귀 / 연결 회복 시 best-effort flush(실패 시 그대로 두고 다음 기회 재시도). 책은 온라인으로 골라뒀으면 book_id, 아니면 manual_book_text(텍스트) 저장 → 온라인 시 재매칭 제안. 홈/서재에 “동기화 대기 N개” 뱃지. flows.md Flow F의 완전 동기화 엔진(connectivity 상시 감지 + 충돌 해결 + 실시간 publish + 책 재매칭 UI)은 V1.5drift/sqflite 불필요, 스키마 마이그레이션 0, 단일 기기라 last-write-wins로 충분). 완전 엔진은 별도 서브시스템(M~L)이라 차별화와 시간 경쟁. 게다가 내장 OCR을 뺀 지금 오프라인 캡처 흐름 자체가 이미 반쪽(책 검색은 어차피 온라인 필요)quotes.book_id nullable(on delete set null) + manual_book_text text 필드를 V1에 넣어두면 V1.5에 큐 붙일 때 마이그레이션 안 함google_mlkit_text_recognition) 등 앱 내장 OCR은 V1에 넣지 않는다. 사용자가 책 사진에서 텍스트를 따올 때는 OS 기본 기능(iOS Live Text, Android Google Lens/구글렌즈, 갤럭시 빅스비 비전 등)으로 복사 → 책귀에 붙여넣기. 인용구 입력 화면은 클립보드에 새 텍스트가 있으면 “붙여넣기” 배너를 띄움google_mlkit_* + image_picker + 카메라/사진 권한) + 한국어 모델 번들(앱 용량 증가) + 웹 미지원(kIsWeb 가드 필요) + 세로쓰기·곡면 책 정확도 한계 + 결과 후처리 코드 → M~L 작업이 통째로 붙는데, 차별화(표지 팔레트·deep link 공유·무드 태그)와 시간 경쟁 ② iOS Live Text·구글렌즈가 이미 OS 레벨에서 한국어 OCR을 잘 함 — 굳이 우리가 다시 만들 이유 없음 ③ flows.md Flow B 4.3이 원래 이 방식(“폰 기능 + 클립보드”)이었음 — 이 결정으로 그 명세가 다시 유효해짐source 컬럼은 manual / clipboard 2종(ocr 제거 또는 V1.5 대비 유지). pubspec.yaml에 OCR/카메라 패키지 추가 안 함. competitor-screen-analysis-2026-05-11.md §7 결정 #1 = 해소AuthController.signInWithKakao, Supabase Dashboard의 Kakao provider 키, handle_new_user OAuth 호환 트리거)는 그대로 유지 — V1.5 활성화 시 LoginScreen의 OutlinedButton에 _signInKakao 다시 연결만 하면 됨account_email scope를 클라이언트 인자와 무관하게 하드코딩으로 요청한다 (Supabase issue #36878). 그런데 카카오 개인 앱은 비즈니스 인증 없이는 account_email scope를 동의항목에 등록할 수 없어 KOE205 (“설정하지 않은 동의 항목”) 에러로 실패. 매직링크는 정상 동작kakao_flutter_sdk_user + signInWithIdToken로 GoTrue OAuth 우회 → 30~60분 + 네이티브 설정. V1.5에 평가StatefulShellRoute + auth gate) + 비용 정책StatefulShellRoute.indexedStack 4 슬롯 BottomNav (홈/서재/[+]/내정보), [+]는 sentinel이라 /quote/new 풀스크린 push. 카드 편집기·인용구 입력은 parentNavigatorKey: rootKey로 셸 외부. /book/:id만 게스트 미리보기 허용. /splash initialLocation으로 cold-start 세션 hydrate 경합 회피redirect + GoRouterRefreshStream(supabase.auth.onAuthStateChange) 패턴. ref.read만으로는 로그아웃 후 화면이 안 바뀌는 함정 (QA 자문가가 P0로 지목)/me 안 진입점으로 흡수GoRoute 트리 — 거부 (탭 전환 시 스크롤·검색 입력 상태 손실)cached_network_image: Stage 1 (이미지 렌더 시작 시점)cursor-after (created_at + id), 페이지 사이즈 15. offset 금지riverpod_lint / custom_lint 보류riverpod_lint·custom_lint 제외하고 셋업 완료flutter_riverpod 3.3.x가 요구하는 riverpod 3.2.1과 riverpod_lint 최신 stable이 요구하는 riverpod 3.1.0 사이 버전 충돌. Riverpod 생태계에서 린터가 메인 패키지보다 항상 한 박자 늦는 패턴flutter_riverpod을 3.1.x로 다운그레이드 → 거부 (최신 기능 손실), (b) 보류 → 채택riverpod_lint이 riverpod ^3.3 지원하면 dev_deps에 추가. 분기 1회 확인palette_generator 그대로 사용palette_generator 0.3.3+7을 그대로 사용docs/design/color-extraction.md가 이 패키지 API 기반. 셋업 단계에서 알고리즘 재설계 비용 회피. 동작에는 문제 없음palette_generator_master (커뮤니티 포크) — 단일 유지보수자 신뢰도 낮음, (b) ColorScheme.fromImageProvider (Flutter 내장) — Vibrant/Muted 버킷 모델 다름, 알고리즘 일부 재설계 필요C:\GIT\bookquotebookquote (소문자 단일 단어, Dart 식별자 규칙)io.github.tgparkk.bookquote
tgparkk 기반 reverse domain. 개인 도메인 미보유 시 권장되는 안전한 패턴 (com.sttgp.* 같이 미소유 도메인 기반은 충돌 위험)supabase_flutter 커뮤니티 SDK + Supabase (Postgres + Auth + Storage + Realtime)#1C1917 (Ink) × #FAFAF8 (Paper) × #B87333 (Copper)lib/core/theme/tokens.dart (TS·MD 명세는 docs/design/)docs/design/design-system.md, docs/design/tokens.mdbookquote 사용oh-my-claudecode:designer): 카드 템플릿, 디자인 시스템, 비주얼. 이 세션 산출물은 docs/design/로 통합 완료 (2026-05-10)다음 항목은 아직 결정 전. 결정되면 위로 옮긴다.