bookquote

화면 설계 — 받은 카드 → 책 담기 (deep link 진입) /book/:id?from=share

그룹 1 · Stage 4 — 바이럴 K-factor의 핵심 경로. 입력 근거: competitor-screen-analysis §5.4, QA-2 / Dart-2, flows.md Flow C, lib/app/deep_link_handler.dart. 북적북적의 “우연한 인스타 캡처 바이럴”을 deep link 메커닉으로 의도 설계한다.


1. 목적 / 진입·이탈 / 라우트


2. 와이어프레임

┌─────────────────────────────────────────┐
│ ←  책              지영님이 단톡방에서 공유 │  AppBar — from=share/kakao면 보낸 사람 컨텍스트(V1.5)
├─────────────────────────────────────────┤
│  ┌────┐  미드나잇 라이브러리              │  표지(BookCover) + 제목/저자/출판사
│  │표지│  매트 헤이그 · 인플루엔셜          │  ISBN, 설명(점진적 공개 — 길면 "더 보기")
│  │    │  ISBN 9791191056556              │
│  └────┘                                  │
│  ┌─────────────────────────────────────┐ │  (quoteId 있으면) 받은 인용구 카드
│  │ "가장 깊은 밤에 가장 빛나는 별이      │ │  — 인용구 텍스트 + 페이지 + "지영님의 인용구"
│  │  보인다."     p.132 · 지영님의 인용구 │ │
│  └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐  │
│ │          + 내 서재에 담기           │  │  Primary CTA — accent500. 이미 담겼으면
│ └─────────────────────────────────────┘  │  "이미 서재에 있어요 ✓" (비활성)
│   비로그인이면: 책 정보 먼저 → "담기" 누르면 │
│   로그인 → 원래 화면 복귀해 담기            │  (안내 — 미로그인 시만)
└─────────────────────────────────────────┘

기존 /book/:id read-only 화면(book_detail_screen.dart)에 ① ?from= 컨텍스트 헤더 ② “내 서재에 담기” CTA(deep link 진입 시 1급, 일반 진입 시에도 노출 가능) ③ (quoteId 있을 때) 받은 인용구 카드 — 를 더한다. 책 상세 보강은 book-detail.md(그룹 2)와 공유.


3. 상태

상태 트리거 처리 표시 심각도
로딩: 책 fetch deep link 진입 bookByIdProvider(id) — 표지·메타 로딩 스피너. 표지는 BookCover placeholder fallback Inline 낮음
로딩: 인용구 fetch (quoteId 있을 때) 인용구 카드 영역만 미니 스피너 Inline (영역) 낮음
로딩: “담기” 처리 CTA 탭 addToLibrary <300ms. 버튼 inline 스피너. 낙관적으로 “담김” 표시 후 실패 시 롤백 Inline 중간
해당 없음 (책 ID로 진입)
에러: 그 책/인용이 삭제됨 (PGRST116) BusinessError “이 책은 더 이상 볼 수 없어요” 화면 + [홈으로] / [책 검색]. (flows.md Flow C 5.4) Empty 중간
에러: 인용구가 비공개/차단된 사용자 RLS 필터 책 상세는 보여주되 그 인용구는 표시 안 함. “내 서재 담기”는 가능 (인용구 영역만 숨김) 중간
에러: “담기” 중 네트워크 끊김 NetworkError “연결을 확인해주세요” + [다시 시도]. 낙관적 표시 롤백 Toast → 재시도 중간
에러: 이미 서재에 있는 책 (23505 unique_violation on user_books) BusinessError “이미 서재에 있는 책이에요” Toast(에러 아닌 정보성) + 그 책 상세 유지. 중복 INSERT 안 함 Toast (정보성) 중간
에러: deep link URI 변조/형식 오류 (://book/ id 없음) ValidationError 무시하고 홈으로. 크래시 금지 (조용히) 낮음
에러: deep link 무한 루프 (잘못된 redirect / /book/:id가 다시 deep link 트리거) deep link는 앱당 1회 consume 후 클리어. 라우터 redirect 최대 1홉. 이미 처리한 URI를 세션 단위 기억 (방어) 중간
미로그인 진입 redirect 안 함 (게스트 허용) 책 상세 read-only로 먼저 보여줌. “담기” 탭 → /auth/login?from=/book/:id → 로그인 → 복귀해 담기 자동 실행(또는 재노출). deep link payload를 로그인 동안 보존 (정상 흐름) 높음 (현재 미구현 — §6)
콜드스타트 + 미로그인 + deep link getInitialLink /splash → 로그인 게이트 → 로그인 후 보존된 deep link 소비 → 책 상세. 가입 흐름(flows.md Flow A 3.4 profiles trigger 지연)이면 “프로필 생성 중” 짧은 대기 후 책 상세 (정상 흐름) 높음
앱 미설치 → 설치 후 deferred deep link 커스텀 스킴(io.github.tgparkk.bookquote://)은 미설치 시 OS가 “열 수 없음” — fallback 안 됨. 진짜 fallback(웹 뷰어)은 Universal Link/App Link(https://) + 도메인 + apple-app-site-association/assetlinks.json 필요 = 인프라 작업 → V1.5. V1은 “앱 있는 사람끼리만 deep link 동작” 한계 수용 + 단톡방 텍스트에 “책귀 앱에서 보기” 안내 (V1 한계) 중간
오프라인 진입 connectivity_plus 책 fetch 실패 → “연결을 확인해주세요” + [다시 시도]. “담기”는 온라인 필요 배너 + 재시도 중간

4. 인터랙션


5. 토큰

영역 토큰
화면/AppBar secondary200 배경, 투명 AppBar. 보낸 사람 컨텍스트 ui xxs primary400
표지 BookCover 64×94 (또는 더 크게)
책 메타 제목 ui w600 16~18 primary900 / 저자·출판사 ui 12 primary500 / ISBN ui xxs primary400 / 설명 ui 13 primary600, 4줄 후 “더 보기” accent600
받은 인용구 카드 copper-100(accent100 #FAEBD6) 배경 + accent300 border + AppRadius.md / 인용구 AppFonts.quote 13 primary800 / “지영님의 인용구” ui xxs primary400
Primary CTA accent500 배경 / secondary50 텍스트 ui w600 14 / AppRadius.md / AppShadow.floating / 이미 담김 = secondary500 배경·primary400 텍스트 + ✓
안내 (미로그인) ui xxs primary400, 중앙
“더 이상 볼 수 없어요” Empty 상태 — primary400 텍스트 + [홈으로]/[책 검색] 버튼

6. 재사용 / 신규

재사용: book_detail_screen.dart(read-only 표시 — 확장), bookByIdProvider(book_providers.dart), BookCover, book_repository.addToLibrary/getById, router.dart/book/:id 라우트(게스트 허용 — 그대로), app_links(deep_link_handler에서 이미 사용).

신규 / 변경


7. 엣지 / 접근성

교차 관심사: ④ 막다른 골목 금지 = 미로그인이어도 책 정보 먼저 보여주고 담기→로그인→복귀; 미설치는 V1 한계(웹 뷰어 V1.5)지만 안내 카피로 완충. ② 데이터 유실 = deep link payload를 로그인 동안 보존. ③ PII = 보낸 사람 이름·인용구를 로그에 안 남김. ⑥ 에러 표시 일관성(삭제=Empty, 중복=정보성 Toast). ⑦ /book/:id는 게스트 허용(deep link용) — DECISIONS와 정합. ⑧ 해당 없음.

엣지 심각도 처리
deep link인데 그 책이 books 테이블에 없음(보낸 사람만 가지고 있던 임시 데이터?) 낮음 books는 공유 시 upsert되어 있어야 정상. 없으면 “더 이상 볼 수 없어요”
받는 사람이 그 책을 이미 가지고 있음 중간 23505 → “이미 서재에 있어요” 정보성 Toast
받는 사람이 보낸 사람을 차단함 낮음 책 정보만, 보낸 사람 컨텍스트·인용구 숨김
deep link로 가입 → profiles trigger 지연 낮음 “프로필 생성 중” 짧은 대기 후 책 상세
동일 deep link 여러 번 탭 낮음 이미 담겼으면 idempotent, Toast 폭탄 방지
from=share인데 quoteId 없음 (텍스트 링크만 공유, 카드 메타 없음) 낮음 받은 인용구 카드 영역 생략, 책 정보 + 담기만
웹에서 deep link (브라우저로 https://... 열림 — V1.5) V1.5 웹 뷰어가 같은 책 상세를 보여주고 “앱에서 담기”/”앱 설치” 안내

접근성: “내 서재에 담기” ≥48dp, 라벨 명확. 표지에 '$title 표지' 또는 표지 없으면 placeholder에 제목 텍스트. 받은 인용구 카드에 '$senderName이 보낸 인용구: $text, $page페이지' semantics. “이미 서재에 있어요” 상태도 스크린리더에 명시. 보낸 사람 컨텍스트는 색만으로 구분 안 함(텍스트).


변경 이력