bookquote

화면 설계 — 카드 에디터 /quote/:id/card

그룹 1 · Stage 3 (가장 공들일 화면 — V1 차별화 ①의 무대). 입력 근거: competitor-screen-analysis-2026-05-11.md §5.2, QA-2 / Dart-2 가상 팀 산출, docs/design/templates/01~05, docs/design/color-extraction.md, docs/design/mockups/all-templates.html.


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


2. 레이아웃 와이어프레임 (3단 고정)

┌─────────────────────────────────────────┐
│ ←       ▢ 1:1  ▢ 4:5  ▣ 9:16      ⤺  공유│  ① 상단 바
│                                         │     좌: 뒤로  중: 비율 세그먼트(기본 9:16)
├─────────────────────────────────────────┤     우: 언두(스택 비면 비활성) · 공유(항상 1탭)
│         ┌───────────────────┐           │
│         │                   │           │  ② 가운데 — 라이브 프리뷰 (화면 ~55-60%)
│         │  "가장 깊은 밤에   │           │     선택한 비율로 렌더. 인용구 텍스트 박스를
│         │   가장 빛나는      │           │     "한 번 탭" → 인라인 핸들(이동/크기) — 더블탭 숨김 금지
│         │   별이 보인다."    │           │     아래 패널 조작이 즉시 반영(딜레이 체감 0)
│         │                   │           │     워터마크 = 캡처 트리 안 Positioned(켜져 있으면 보임)
│         │  미드나잇 라이브러리 │           │
│         │  ─ 책귀에서 만들었어요 ─│       │
│         └───────────────────┘           │
│  ● ● ● ● ●   ↑ 이 책 표지에서 추출       │  표지 추출 5스와치 줄 = "색 변경 UI"의 전부
│                                         │     탭 → 현재 템플릿의 해당 슬롯(배경/구분선/제목색)에 적용
│ ┌──┐┌──┐┌──┐┌══┐┌──┐                    │  ③ 하단 — 5종 템플릿 가로 한 줄
│ │미││웜││모││표││타│  ← 각 칸 = 지금 그   │     각 썸네일 = 와이어프레임 아님, "지금 그 사용자의
│ │니││  ││노││지││이│    사용자 인용구·    │     인용구·표지로 실제 렌더된 미니 카드".
│ │멀││  ││  ││발││포│    표지로 렌더된 미니 │     선택된 칸에 copper 언더라인
│ └──┘└──┘└──┘└══┘└──┘                    │
│  텍스트 │ 색 │ 위치 상·중·하 │ 워터마크 ● │ 다른 느낌 ↻ │  보조 행 — 이게 옵션의 끝(더 안 둠)
└─────────────────────────────────────────┘

3. 상태

상태 트리거 처리 표시 심각도
로딩: quote/book 로드 진입 입력 화면에서 막 만든 quote면 캐시 hit(빠름); deep link·목록 진입은 fetch. 카드 영역 스켈레톤(템플릿 프레임만 + 텍스트 자리 shimmer), 컨트롤 바 disabled Inline 낮음
로딩: 표지 이미지 다운로드 CachedNetworkImageProvider fetch 표지 자리에 베이지 placeholder + 미세 progress. 텍스트는 fallback 팔레트로 먼저 렌더 → 표지 도착 시 swap(cross-fade) Inline (영역) 중간
로딩: 팔레트 추출 대기 표지 로드 완료 → PaletteGenerator.fromImageProvider(100×100 다운스케일, 타임아웃 3s) T2/T4 선택 상태면 fallbackPalettes[templateId]로 즉시 렌더 → 추출 완료 시 색만 cross-fade 교체. 추출 중 “색” 스와치 영역에 로딩 점 Inline (영역) 중간
로딩: PNG 생성 “공유”/저장 탭 → RepaintBoundary.toImage → ByteData → 파일 “카드 만드는 중…” 모달-lite 또는 버튼 spinner. 이 동안 다른 입력 차단(중복 capture 방지) Modal-lite 중간
로딩: 공유 시트 등장 share_plus.shareXFiles 호출 → OS 시트 뜨기까지 버튼 spinner 유지. OS 시트 뜨면 자동 해제. 500ms+ 그냥 둠(OS 책임) 낮음
빈: 카드 만들 인용 없음 잘못된/삭제된 quoteId 직접 진입 (PGRST116 no rows / BIZ_QUOTE_NOT_OWNED) “이 인용구를 찾을 수 없어요” empty + [내 인용 보기] CTA Empty 중간
빈: 표지 이미지 없는 책 (cover_url == null) ISBN 직접 등록 도서 등 T1/T3/T5 정상(고정 배경). T2/T4는 fallbackPalettes['coverExtract'](#3D2817 계열)로 렌더 + 작은 안내 “이 책은 표지가 없어 기본 색으로 만들었어요”. T4(표지발췌)의 blur 표지 배경 → 단색 그라데이션으로 (또는 T4 비활성 — §7 미결) Inline 안내 중간
에러: 표지 URL 깨짐(404/CDN down) → 추출 불가 Storage/External 경계 getPaletteWithFallback → fallback 팔레트. 사용자에게 에러 표시 안 함(카드는 멀쩡히 나옴). Sentry breadcrumb만 (무표시) 낮음
에러: PNG 생성 실패 (OOM / 큰 표지 / Skia) StorageError Toast “카드 만들기에 실패했어요. 다시 시도해주세요” + 디자인 옵션 전부 유지. 재시도 시 pixelRatio 1차 폴백(1080→720), 2차도 실패 시 “기기 메모리가 부족할 수 있어요. 다른 앱을 닫고 다시” Toast (작업 보존) 높음 (공유 차단 = 핵심 가치 차단)
에러: 폰트 로드 실패 (NotoSerifKR asset 누락) StorageError 폰트는 앱 번들 asset 동봉이 1차 방어(런타임 다운로드 X — 이미 pubspec에 번들됨). 그래도 실패 시 시스템 세리프 + Sentry. PNG 생성 직전 폰트 로드 완료 보장(PaintingBinding.instance.fontLoader/endOfFrame await) (방어 위주) 높음 (첫 캡처 □□□ 방지)
에러: quote/book fetch 실패 (네트워크) NetworkError “카드 정보를 불러오지 못했어요” + [다시 시도] (retryable) Inline → 재시도 중간
에러: quote가 다른 기기에서 삭제됨, 그 후 cards 저장 시도 BusinessError (23503 FK) “이 인용구가 삭제되어 카드를 저장할 수 없어요. 이미지는 저장돼요” — PNG는 로컬에 있으니 공유/저장은 됨, cards 테이블 기록만 skip Toast 중간
에러: cards 테이블 INSERT(히스토리) 실패 StorageError, 비차단 조용히 무시(공유는 이미 완료, 히스토리는 nice-to-have). 재시도 안 함 (무표시) 낮음
오프라인 connectivity_plus 표지가 cached_network_image 캐시에 있으면 정상 렌더. 캐시 없으면 표지 없는 책처럼 fallback 팔레트. PNG 생성·저장은 로컬이라 오프라인에서도 됨. 공유 시트도 로컬(전송은 카톡/인스타 앱 책임) 배너(상단) 중간
권한 거부: 사진 라이브러리 쓰기 “이미지 저장” 시도 (card-share.md 참조) [저장] 버튼만 비활성, 카카오톡/인스타 공유 버튼은 끝까지 활성 + [설정 열기]. iOS add-only(NSPhotoLibraryAddUsageDescription) / Android 11+ scoped storage(권한 불필요) / Android 9↓ WRITE_EXTERNAL_STORAGE 분기. 전체 시트를 막지 않는다 Toast + 부분 비활성 높음

4. 인터랙션 명세


5. 디자인 토큰 매핑

영역 토큰
화면 배경 AppColors.secondary200 (paper) — 또는 카드를 강조하려 약간 어둡게 secondary300. (다크 톤은 안 씀 — 카드 자체 색이 주인공)
상단 바 투명 / ← primary500 / 비율 세그먼트: 선택 primary900 배경·secondary50 텍스트, 미선택 primary400 텍스트·primary200 border / ⤺ 언두 primary500(비활성 primary300) / 공유 = accent500 배경·secondary50 텍스트 ui w600 12, AppRadius.sm
라이브 프리뷰 컨테이너 AppRadius.md + AppShadow.card(카드를 종이 위에 올린 느낌)
카드 내부 templates/01~05.md의 명세 + ExtractedPalette/fallbackPalettes (이미 tokens.dart에 정의됨). 인용구 폰트 AppFonts.quote(NotoSerifKR), 책표시 AppFonts.ui(Pretendard) 또는 템플릿별 지정. 크기 = getQuoteFontSize(charCount)/AppTextStyles.quoteForLength
표지 5스와치 22×22, AppRadius.xs, border 1.5 rgba(0,0,0,.08). 추출값(dominant/secondary/vibrant/darkVibrant/muted) 그대로
템플릿 썸네일 ~52×88, AppRadius.xs. 선택 시 border 2 accent500 또는 하단 언더라인 accent500
보조 행 버튼 텍스트 버튼 ui xs(10.5) primary500, 활성 시 accent600. “다른 느낌 ↻” accent500
워터마크 AppWatermark.minimal/branded (tokens.dartWatermarkConfig) — 우하단/하단 중앙, opacity ~0.3, 텍스트 영역과 비겹침 좌표(templates/*.md에 박음)
Toast / Empty / 에러 공통 (AppTheme.snackBarTheme 등)

새 토큰: moodColors 맵(quote-input.md와 공유 — 카드에 무드 칩을 표시할 경우). 카드 비율 픽셀은 AppCardSize(1080×1920 / 1080×1080 / 1080×1350) 이미 있음. CardRatio enum 이미 있음.


6. 재사용 컴포넌트 / 신규

재사용 (코드에 있음)

신규


7. 엣지 케이스 / 접근성 / 미결

교차 관심사 (공통 8원칙): ① 오프라인=1급(표지 캐시·로컬 PNG) ② 데이터 유실 금지(편집 상태 영속화 — 인용구 본문은 DB) ③ PII 로그 금지(인용구 텍스트 미전송) ④ 막다른 골목 금지(저장 권한 거부해도 공유는 됨, 카톡 미설치해도 다른 공유 가능) ⑤ 해당 없음(이 화면엔 책 검색 시트 없음) ⑥ 에러 표시 일관성 ⑦ 인증 가드 ⑧ “미리보기 = export”(같은 위젯 트리, 스케일만 다름 — 스냅샷 테스트로 회귀 감지).

화면 고유 엣지

엣지 심각도 처리
인용구가 너무 길어 9:16에 안 들어감 (auto-fit 최소 폰트 도달 후에도 넘침, 예 1000자) 높음 ① 비율 자동 추천 “이 문장은 4:5/1:1이 더 잘 어울려요” ② 그래도 넘치면 명시적 경고 “카드에 다 안 들어가요” + 사용자가 알고 공유/혹은 텍스트 짧게. 잘린 채 조용히 export 금지
1단어/초단문 인용구 → 카드 휑함 낮음 T5(타이포)·T3(모노) 추천 + 폰트 자동 확대(최대 cap) + 중앙 정렬 강조
흑백·단색 표지 (채도<10) 중간 color-extraction.md §6 자동 폴백(lightenToBackground→secondary400, toMidTone→primary600). “이 표지는 색이 적어 기본 톤으로” 미세 안내(선택)
표지 색 vs 텍스트 대비 부족 (흰 글씨가 밝은 배경에 안 보임) 높음 ensureContrast(minRatio: 4.5) 자동으로 primary900/secondary200 중 대비 높은 쪽 교체. T4는 그라데이션 overlay 80% 추가 보장. 이 로직 깨지면 가독성 0 → 스냅샷 + 대비 테스트 필수
비율 전환 시 텍스트 위치 깨짐 중간 텍스트 위치를 상대좌표(0~1)로 저장 → 새 비율에 재투영 + clamp. “위치를 조정했어요” Toast
편집 중 앱 백그라운드 → 메모리 회수 중간 편집 상태 디버운스 영속화 → 재진입 “이어서 만들기”. 인용구 본문은 DB라 안전
같은 카드 여러 번 저장/공유 낮음 허용(의도적일 수 있음). cards엔 매번 새 row. 직전 5초 내 동일 design 해시 재요청은 dedupe
카드 PNG에 워터마크가 텍스트 가림 중간 워터마크는 항상 고정 안전 영역(templates/*.md에 텍스트 영역과 비겹침 좌표 박음). auto-fit 텍스트 영역 계산 시 워터마크 영역 제외
quote 본문에 이모지/특수문자/RTL 섞임 낮음 시스템 이모지 폰트 fallback 체인. PNG 캡처 전 렌더 검증
잘못된 quoteId (/quote/abc/card) 낮음 라우터 가드 또는 화면에서 PGRST116 → “찾을 수 없어요” empty. 무한 루프 금지(리다이렉트 1회)

접근성

결정 완료 (DECISIONS 2026-05-12)

  1. 텍스트 위치 앵커(상/중/하): V1엔 안 넣음. V1.5. V1 미세 조정 = 폰트 크기 ±·정렬(템플릿 허용 범위)만. 이유: templates/*.md가 이미 고정 좌표 모델(quoteArea y=192 등)이라 앵커는 5종 명세 재작성 + 디자인팀 재합의 필요 → 차별화와 시간 경쟁. 단 card_editor_controller의 텍스트 위치는 지금부터 상대좌표(0~1)로 직렬화 — V1.5에 앵커 3지점 스냅 붙일 때 마이그레이션 0.
  2. 표지 없는 책에서 T4(표지발췌): 비활성화. 썸네일 회색 + “표지가 필요해요” 오버레이(templates/04.mdshowTemplateDisabledOverlay), 나머지 4종 정상 + (가능하면) “표지 추가하기” 인라인 액션으로 ISBN 재검색 유도. 이유: T4의 정체성이 “이 색이 이 책 표지에서 나왔다”는 바이럴 순간 — 표지 없는데 단색 degrade하면 약속이 거짓 + T1/T3와 구분 안 됨.

변경 이력