Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 첫 페이지 실패 시 헤더(타이틀·등록 버튼)를 남기고 그 아래만 NetworkErrorView로 대체 - 그리드/리스트 토글: 흰 원 하나를 항상 그려두고 offset만 바꿔 좌↔우 슬라이드(스프링) - 조건부 matchedGeometryEffect는 크로스페이드로 보여 이동이 안 드러나던 문제 수정 - 모듈 CLAUDE.md: 에러 3분화 서술 갱신 + 토글 슬라이드 함정 기록 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mage) - WSSAsyncImage: 디코딩된 UIImage를 인메모리 캐시(WSSImageCache, 화면 공유)에 두고 init에서 동기 조회 → 캐시 히트 시 첫 프레임부터 실제 이미지(placeholder 프레임 없음) - WSSNovelCoverImage: scaledToFill + WSS 빈 표지 폴백을 묶은 표지 편의 래퍼 - LibraryGridCell/LibraryListCell: AsyncImage/LibraryCoverImage → WSSNovelCoverImage (서재 전용 LibraryCoverImage 제거, WSSComponent로 승격) - 그리드↔리스트 토글·스크롤 재활용 시 빈 표지 번쩍임 제거 - WSSComponent/LibraryFeature CLAUDE.md: 캐싱 트랩·사용 규칙 기록 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- presentationCornerRadius 제거: presentationBackground(Color)와 함께 쓰면 배경이 시트 둥근 모서리에 클립 안 돼 양 옆·하단이 화면 밖으로 삐져나오던 문제 해결 - presentationDragIndicator(.hidden): 디자인상 상단 그래버 제거 - 선택 체크를 HStack→글씨 overlay로: 선택 시 글씨가 오른쪽으로 밀리지 않게 - 패딩을 VStack 전체에 한 번(ReadingPeriodSheet 패턴), sheetHeight는 콘텐츠에 딱 맞춤 - CLAUDE.md: 위 함정 3가지 기록 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…컨트롤러로 전환) NovelDetailFeature·LibraryFeature에 각각 복제돼 있던 SwipeBackEnabler를 WSSComponent의 `.enableSwipeBack()` 하나로 합친다. #166에서 한쪽만 고쳐져 갈라진 상태였고, 그 탓에 작품 상세를 다녀오면 서재 쪽 반납 가드까지 무력화돼 루트에서 내비게이션이 얼어붙을 수 있었다. 합치면서 delegate 소유 방식을 바꿨다. 기존에는 제스처 delegate를 Coordinator가 가로챘다가 화면을 떠날 때 반납했는데, delegate가 약한 참조라 반납 타이밍을 정확히 맞춰야 했고(늦게 도착한 async 블록이 반납을 되돌리는 경합까지 있었다) 복제본 중 하나가 반납을 빠뜨리면 다른 화면의 반납까지 무너졌다. delegate를 UINavigationController 자신에게 맡기면 그 수명이 스택과 같아져 반납·경합 가드·복제본 동기화가 모두 불필요해진다. 대신 UIKit 기본 delegate가 보던 조건을 직접 챙긴다 — 루트 여부, 전환 진행 중 여부, 그리고 뒤로가기 버튼 숨김 여부. 마지막 조건은 NovelReviewView가 "닫기 전 확인 알럿"을 그 성질에 기대고 있어 필요하다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
내 서재 경로에만 있던 isLiveKeywordCacheReady 가드를 타유저 서재에도 건다. 캐시가 비어 있어도 UseCase가 try? + ?? []로 폴백해 에러 없이 키워드 칩만 통째로 비므로, 실서버로 토글한 직후 바로 진입하면 화면 버그로 오진하기 쉽다. 이 플래그가 "동기화를 시도했다"까지만 뜻한다는 한계(syncKeywords가 실패를 안으로 삼킨다)도 함께 주석으로 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
복제본이 사라졌으니 두 Feature 문서는 WSSComponent를 정본으로 가리키게 줄이고, 가드 셋의 근거와 실측 결과를 정본에 모은다. 런타임으로 확인한 것(코드만 봐선 모르는 것): - `.toolbar(.hidden, for: .navigationBar)` 화면은 hidesBackButton이 false다 → 새 가드가 enableSwipeBack의 본래 목적을 막지 않는다 - `.navigationBarBackButtonHidden(true)` 화면은 true다 → 가드가 실제로 발화한다 - SwiftUI ToolbarItem(.cancellationAction)은 leftBarButtonItems로 잡히지 않는다 → UIKit 기본 delegate의 그 조건까지 흉내 낼 필요는 없었다 구 WSSiOS의 shouldReceive touch 규칙을 왜 가져오지 않았는지, 그리고 커스텀 헤더를 navigationBarBackButtonHidden으로 만들면 왜 스와이프백이 조용히 죽는지도 함께 남긴다. UI 레이어 문서에는 "순수 표현 컴포넌트" 규칙의 예외로 시스템 제스처 복구를 추가했다 — 어디로 가는지는 여전히 Feature/App의 몫이라는 선을 명시한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
장르·연재상태·키워드 탭은 WSSComponent 칩이라 애니메이션이 없어 밀림 증상이 없다고 추정만 해뒀는데, 세 탭 첫 선택을 직접 확인했다. 원인이 액션 시점 트랜잭션이라 코드만으론 단정할 수 없는 종류라 근거를 남긴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
develop이 그 사이 바꾼 두 가지에 맞춤: - NovelPublicationStatus가 NovelDomain → BaseDomain으로 이관 → import 교체 - KeywordGroup(name:image:keywords:) → KeywordGroup(category:keywords:) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…와 대칭) 화면이 둘인데 한쪽만 makeView라 호출부에서 어느 서재인지 읽히지 않았다. FeedFeature 방식(전부 명시적 이름)에 맞춰 대칭을 맞추고, 판단 기준을 Feature 레이어 문서에 규칙으로 남긴다(대등한 화면 vs 메인+하위 상세). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이 화면만 인증 만료를 loadFailed로 덮어 NetworkErrorView를 세우고 있었다. "push라 onAppear 재발화 복구가 없으니 재시도라도 남겨두자"는 의도였는데, 세션이 죽은 상태에선 재시도가 같은 authenticationRequired로 되돌아와 탈출구가 되지 못하고 원인도 네트워크 오류로 잘못 표시된다. catch 순서를 뒤집어 실패 플래그보다 먼저 걸러 return한다 (내 서재·NovelDetail 정본과 동일). 인증 만료 뒤 화면을 치우는 건 콜백을 받은 App 배선의 책임이다. Demo authExpired 실측: 진입 1회 + 정렬 변경 1회로 콜백 2회 발화, UserLibrary 실패 로그 0건(실패 뷰 경로 미진입). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
예외를 정당화하는 근거가 모듈 문서에 박혀 있어 그대로 두면 같은 판단이 재발한다. 화면 성격과 무관한 앱 전체 규칙이므로 Feature CLAUDE.md에 계약으로 세운다 — 실패 플래그보다 먼저 거를 것, 전면 실패 뷰로 덮지 말 것, push 화면이라는 이유로 예외를 만들지 말 것, 화면을 치우는 건 App 책임이며 콜백은 idempotent할 것. LibraryFeature 문서는 "한때 예외를 뒀다 되돌렸다"는 이력만 남기고 정본을 가리키게 정리했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Issue
closed #166
💭 Summary
서재 화면(LibraryFeature)을 새로 구현했습니다. 내 서재와 타유저 서재 두 화면이고, 이 화면을 붙이면서 필요해진 조회 V2 계약과 UI 공용 컴포넌트 정리를 함께 담았습니다.
🔑 Key Changes
LibraryFeature (신규)
onAuthenticationRequired콜백.onAppear로 복구되지만, 타유저 서재는 push라 그 경로가 없어 그대로 두면 "보관함이 비어있어요"로 굳고 재시도 버튼도 없습니다. 타유저 서재는 인증 만료도 실패 뷰로 덮습니다.ScrollPosition(18+)을 쓸 수 없어 두 스크롤 뷰를 동시에 살려두고 숨기는 방식으로 해결했습니다.requiresAuthentication신호는 View가 소비 후 되돌립니다. 서재는 탭 콘텐츠라 VM이 앱 세션 내내 살아서, 신호가 굳으면 2회차 만료가 조용히 삼켜집니다(push 후 dismiss되는 NovelDetail·NovelReview와 다른 지점).Domain (NovelDomain)
CursorPaginated추가 — 서재는 페이지 번호가 아니라 서버 발급 커서(nextCursor)를 그대로 왕복합니다.LibrarySortType추가(6종: 등록 최신·오래된순, 작품명, 읽은 날짜, 별점 높은·낮은순). 공용SortType(2종)과 별개이며, 타유저 서재도 같은 6종을 씁니다.LibraryRatingFilter추가 — 범위 + "별점 없음" 2가지 형태로, 검색의NovelRatingThreshold(이상 4단계)와 다른 타입입니다. 전체 범위(0.0~5.0)는setRatingRange가 nil로 정규화해 "필터 없음"의 표현을 하나로 유지합니다.MyLibraryFilter확장 — 시트 필터 6종 + 관심 토글 + 정렬.clearAll()(시트 "초기화")은 시트 필터 6종만 리셋하고 관심·정렬은 유지합니다(시트 소속이 아님).LoadMyLibraryKeywordsUseCase신규 — 필터 시트 키워드 탭 데이터(내가 서재 작품에 등록한 키워드).LoadMyLibraryUseCase·LoadUserLibraryUseCase가(filter:cursor:)+ 키워드 캐시를 받도록 바뀌었습니다.Data (NovelData)
/users/{id}/novels/v2) 연동 —UserLibraryV2Query, 응답 DTO 2종 추가.?genres=로 직렬화돼 서버가[""]필터로 오해).isInterest는 true일 때만 전송합니다(false를 보내면 "비관심만" 필터가 됨).genres는 영문 라벨,keywords는 한글 이름으로 검색 API의keywordIds와 다릅니다.Keyword로 복원합니다./users/{id}/novels)는 타유저 서재를 V2로 옮기면서 호출자가 사라졌습니다. V1은lastUserNovelId: 0하드코딩이라 애초에 첫 페이지 고정·필터 무시였습니다.DefaultNetworkLogger).UI (WSSComponent·DesignSystem)
WSSAsyncImage·WSSNovelCoverImage추가:AsyncImage는 뷰 정체성이 바뀔 때마다.emptyphase부터 다시 시작해 캐시 히트여도 placeholder가 한 프레임 번쩍입니다(목록 셀 재활용·모드 토글에서 매번 도짐). 디코딩된UIImage를 인메모리 캐시에 두고 렌더 경로에서 동기 조회하도록 만들어 첫 프레임부터 실제 표지가 나옵니다. NovelDetail 대형 표지가 자체 prefetch로 풀던 문제를 컴포넌트로 일반화한 것이라, 그쪽 복제 코드는 제거했습니다.enableSwipeBack추가: 커스텀 헤더를 쓰려고 네비바를 숨기면(.toolbar(.hidden, for: .navigationBar)) iOS가 스와이프 뒤로가기도 함께 끕니다. 이를 되살리는 코드가NovelDetailFeature·LibraryFeature에 각각 복제돼 있다가 한쪽만 고쳐지며 갈라져 사고가 나서, WSSComponent로 통합했습니다.UIGestureRecognizer.delegate가 약한 참조라 그 객체가 해제되면 nil이 되고 UIKit이 스스로 되돌리지 않습니다. 그러면shouldBegin기본값(YES)이 적용돼 루트에서도 pop 전환이 시작되고 내비게이션이 얼어붙습니다(앱 재시작 외 복구 불가). 지금은 네비게이션 컨트롤러 자신이 delegate를 맡습니다 — 스택이 사는 내내 살아 있어 반납·경합 가드·복제본 동기화가 통째로 필요 없어집니다.NovelReviewView가 "작성 중 나가면 확인 알럿"을 그 성질에 기대고 있어 빠뜨리면 초안이 확인 없이 사라집니다. 두 modifier가 서로 다른 플래그를 건드린다는 점(.toolbar(.hidden)은hidesBackButton = false,.navigationBarBackButtonHidden(true)는true)은 시뮬레이터 로그로 확인했습니다.stroke→strokeBorder전면 전환:stroke는 선을 shape 경로 중앙에 그려lineWidth 1이면 0.5pt가 프레임 밖으로 나갑니다. 컴포넌트를ScrollView안에 넣는 순간 바깥 절반이 클립돼 테두리가 잘려 보입니다(서재 필터 칩·필터 시트 키워드 칩에서 발생). 공용 컴포넌트는 어디에 놓일지 모르니 기본값을strokeBorder로 잡았습니다.icAlarm·icBookRegister·icReset·icHeartFilledStroke.Tuist·문서
library모듈 등록.new-feature스킬에 동작 계약 게이트를 추가했습니다(design-gap-checklist). 정적 Figma로는 안 잡히는 것(스크롤 고정 영역·로딩/빈/실패 분화·탭 결과 등)을 구현 전에 질문으로 확정하고, 모듈 CLAUDE.md의 "화면 동작 계약" 절에 정본으로 남기는 흐름입니다. 이번 타유저 서재에서 실제로 사용했습니다.📱 Simulation
🧑🧒🧒 To Reviewer
확인 부탁드릴 것
enableSwipeBack은UINavigationController에 제스처 준수를 붙이는 방식이라 import만으로 앱 전역에 영향이 갑니다(다른 컴포넌트는 쓸 때만 영향). 정의는 WSSComponent 한 곳에만 두고 UI 레이어에 둔 근거를Projects/UI/CLAUDE.md에 적었는데, 이 판단에 이견 있으시면 말씀해 주세요. 대안으로 associated object 방식이 있으나 지금 구조를 바꿀 만큼 급하진 않다고 봤습니다.MyLibraryFilter정책 테스트와 조회 UseCase 테스트가 이번에 추가됐습니다. 테스트 그린 확인 부탁드립니다.App 조립 시 주의
fetchKeywords()는 네트워크를 타지 않고 로컬 캐시만 읽습니다. 앱 시작 시syncKeywords()를 먼저 호출하지 않으면 서재·작품 상세의 키워드가 오류 없이 빈 채로 뜹니다. 화면에 에러가 안 나타나 원인 찾기 어려운 종류라NovelDomain/CLAUDE.md에 적어뒀습니다.Projects/App/에 아직 UseCase 조립 코드가 없어 Demo 앱이 그 역할을 합니다(다른 Feature 모듈과 동일한 상황).후속으로 넘기는 것
NovelData에 구 V1 조회 코드(getUserLibraryNovels·UserLibraryQuery·UserLibraryNovelsResponse·libraryNovels(from:))가 호출자 없이public으로 남아 있습니다. 주워 쓰면 첫 페이지 고정·필터 무시를 그대로 물려받아 문서에 경고만 달아뒀고, 정리는 별도로 하겠습니다.WSSFilterButton(h33)과 다릅니다. 디자인이 화면별로 다르게 잡힌 결과라 컴포넌트로 합치지 않았습니다.※ Reference
구 WSSiOS(UIKit)를 기준으로 삼은 것들입니다.
LibraryFilterView. 시트 높이가 고정인데 탭마다 콘텐츠 높이가 크게 달라, 탭 영역이 남은 공간을 차지하고 넘치면 그 안에서 스크롤하는 구조를 그대로 따랐습니다.WSSRangeSlider. 트랙을 핸들 반지름만큼 안쪽으로 잡아 0.0/5.0에서 핸들이 잘리지 않게 합니다.UIViewController+.swift(swipeBackGesture()+gestureRecognizerShouldBegin). "반납하지 않고 각 뷰컨트롤러가 delegate를 맡는다"는 아이디어의 출처입니다.