bookquote

화면 설계 — 책 검색 시트 (모달) (이미 구현 — 역정리 + 개선 권고)

그룹 3. 입력 근거: lib/features/book/presentation/book_search_sheet.dart, lib/features/book/state/book_search_controller.dart, lib/features/book/data/book_repository.dart, supabase/functions/aladin-search/(코드 기준), competitor-screen-analysis §5.8. 모달 시트 — 서재 FAB·인용구 입력 화면에서 호출, Future<Book?> 반환.

1. 목적 / 진입·이탈

2. 와이어프레임

┌─────────────────────────────────────────┐
│              ────                        │  _DragHandle (36×4, primary200)
│  ┌─────────────────────────────────────┐ │
│  │ 🔍 책 제목, 저자, ISBN               │ │  TextField autofocus, enabled: !_saving
│  └─────────────────────────────────────┘ │  400ms 디바운스 → bookSearchQueryProvider
│  내 서재 카탈로그                          │  cached 섹션 (findCachedByQuery, ilike, limit 5)
│  ┌──┐ 미드나잇 라이브러리 · 매트 헤이그   │  _CachedRow
│  └──┘                                    │
│  알라딘 검색 결과                          │  fresh 섹션 (searchBooks Edge Function,
│  ┌──┐ 미드나잇 라이브러리 (개정판) · …    │  같은 isbn13은 캐시 우선)
│  └──┘ …                                  │  _FreshRow
└─────────────────────────────────────────┘
[ query 길이 < 2 / 빈 ]  현행: _EmptyState("찾는 책이 없어요") ← 검색 전인데 뜨는 흠
                          (개선) → cached 카탈로그 + "책 제목·저자·ISBN으로 검색해보세요" 안내
[ query 있고 결과 0건 ]  _EmptyState("찾는 책이 없어요. 제목 일부만 다시 시도하거나, 책 뒤표지 ISBN을 붙여넣어 보세요.")
                          (개선) + [ISBN으로 등록] [직접 입력해서 등록] 버튼 2개
[ 에러 ]                  _ErrorView: code=='RATE_LIMIT' → "오늘 책 검색이 일시적으로 제한됐어요…" / 그 외 "검색에 실패했어요. 네트워크 상태를 확인해주세요." ← 재시도 버튼 없음
[ _saving ]               반투명 0x66000000 오버레이 + 스피너

3. 상태 (코드 기준)

| 상태 | 처리 | 심각도 | |—|—|—| | 입력 | _controller + _debounce Timer(400ms) → ref.read(bookSearchQueryProvider.notifier).update(value) | — | | 검색 | bookSearchProvider(FutureProvider.autoDispose<BookSearchResult>): query.trim().length < 2BookSearchResult.empty(). 아니면 findCachedByQuery(title/author ilike, limit 5)와 searchBooks(Edge Function) 동시 호출 → 같은 isbn13은 캐시 우선, remote는 캐시에 없는 것만 fresh. _safeCached: BookRepositoryException 시 빈 리스트. _safeRemote: code == 'NOT_FOUND' → 빈 응답 흡수, 그 외 rethrow | — | | 로딩 | result.when(loading: CircularProgressIndicator())화면 전체 덮음(캐시가 즉시 있어도 안 보임) | 낮음 (개선) | | 빈: query < 2 / empty | _EmptyState(“찾는 책이 없어요”) — 검색 전인데 뜸(혼란) | 중간 (개선) | | 빈: query 있고 0건 | _EmptyState + “ISBN 붙여넣어 보세요” 안내 — 버튼 없음(말만, 막다른 골목) | 높음 (개선) | | 에러 | _ErrorView: error is BookRepositoryException && code == 'RATE_LIMIT' → “오늘 책 검색이 일시적으로…”, 그 외 “검색에 실패했어요. 네트워크 상태를 확인해주세요.” — [다시 시도] 없음. (Edge Function이 던지는 코드: RATE_LIMIT/UPSTREAM/PARSE/NOT_FOUND/INVALID_INPUT_shared/aladin.ts; book_repository가 추가로 UPSTREAM(FunctionException)/PARSE(shape) 전파. RATE_LIMIT 문자열이 실제로 도달하는지 검증 필요) | 중간 (개선) | | 선택 (_onPick) | _saving 가드 → setState(_saving=true)input.cached != null이면 그대로 / input.fresh != null이면 repo.upsertBook(dto)(upsert_book RPC). on BookRepositoryException_saving=false + SnackBar(“책 저장 실패: ${e.message}” — raw). 성공 시 SnackBar(“"$title"을(를) 서재에 추가했어요” 2s — 카피 부정확, 실제론 카탈로그 upsert) → Navigator.pop(book) | 중간 (개선) | | 오프라인 | connectivity_plus 미연동 — searchBooksUPSTREAM으로 rethrow → bookSearchProvider 전체 error → cached 책도 안 보임 | 중간 (개선) |

4. 인터랙션

5. 토큰 매핑

6. 재사용 / 신규

7. 엣지 / 접근성 + 수정·보강 항목

교차 관심사: ④ 막다른 골목 금지 = ISBN/직접 등록 출구 + 알라딘 다운 시 [다시 시도] (현행 위반 — 개선 핵심) · ⑤ 시트 왕복 시 호출자 입력 보존(시트는 모달 → 호출자 state 안 건드림 — 회귀 테스트) · ② 데이터 = 시트 닫혀도 upsert 완료까지 책임(또는 _saving 중 닫기 차단) · ③ PII = 검색어 raw 미전송. 양호(유지): 캐시 우선 + 알라딘 fresh 동시 호출 · isbn13 dedupe · 400ms 디바운스 · _saving 가드·오버레이 · sheetCtx.mounted/context.mounted 가드. 수정·보강 권고 (현행 ≠ 권고):

변경 이력