Conversation
테스트 커버리지 리포트
Generated by CI on 2026-09-10 09:48 KST |
검색어에 해당하는 작품이 없을 때 뜨는 WSSEmptyView의 "작품 문의하러가기" 버튼 action이
빈 클로저({})라 눌러도 아무 반응이 없었음. AppURL.inquiryAddNovel을 열도록 연결.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
행 전체를 눌러도 추가/삭제가 토글돼 실수하기 쉽다는 QA 지적으로, 필 배지(+ 추가/× 삭제) 자체만 탭되도록 좁혔다. 표지·제목·작가 영역은 더 이상 탭을 받지 않는다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
컬렉션 작품 100개 초과 선택 시 뜨는 WSSToastType.selectionOverLimit 문구를 기획팀 확정
카피("100개까지 선택이 가능해요")로 교체. NovelReviewFeature(매력포인트 3개)·
KeywordFeature(키워드 20개)와 공유하는 범용 텍스트라 세 화면 모두 함께 반영됨.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
axis: .vertical TextField가 minHeight만 갖고 있어 엔터를 칠 때마다 박스 전체가 계속 늘어났고, 글자수 카운터도 bottomTrailing overlay라 텍스트가 박스 하단까지 차면 겹쳤음. 텍스트필드 영역을 고정 높이 ScrollView로 감싸고 카운터를 별도 줄로 분리했다. 추가로 ScrollViewReader를 붙여 입력할 때마다 캐럿(텍스트 끝) 쪽으로 자동 스크롤되게 했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
"..." 더보기 버튼과 그 드롭다운에 얽힌 UI 결함 두 가지를 함께 정리했다. - 더보기 버튼 터치 영역이 아이콘(18×18) 크기 그대로라 너무 작았음 → 뒤로가기 버튼과 같은 방식으로 44×44까지 넓히고, 아이콘 시각 위치는 트레일링 패딩 보정으로 유지. - 더보기 드롭다운이 스크롤로 스티키 정렬 바가 붙으면 그 흰 배경에 가려짐 → menuOverlay를 화면 루트 ZStack의 마지막 자식(최상위 z-order)으로 옮겨 항상 위에 그려지게 함. - 드롭다운 위치도 네비바와 120pt나 떨어져 있던 걸 버튼 바로 아래(44pt)로 붙였다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
작품 연결 검색에서 결과가 없을 때 뜨는 WSSEmptyView의 "작품 문의하러가기" 버튼 action이
빈 클로저({})라 눌러도 아무 반응이 없었음. AppURL.inquiryAddNovel을 열도록 연결.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
작품이 연결된 채로 검색바를 눌러 "이미 연결된 작품" 토스트가 뜬 상태에서 연결된 작품을 제거하면 "알 수 없는 오류" 토스트로 문구가 바뀌어 보이는 버그 수정. 원인: handle(_:)가 액션마다 validationError를 먼저 nil로 리셋하는데, showToast는 이 리셋과 별개로 관리돼 있었음 — 토스트가 떠 있는 동안 무관한 다른 액션(.removeConnectedNovel)이 들어오면 validationError만 nil이 되고 showToast는 true로 남아, View의 toastType이 nil 폴백 케이스(.networkDelay)로 떨어지며 엉뚱한 문구로 바뀌었다. presentValidationError가 validationError와 showToast를 항상 함께 재계산하도록 수정. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
피드 상세 화면의 내 글 드롭다운이 "수정"/"삭제"로 짧아 목록 셀("수정하기"/"삭제하기")과
문구가 갈려 있었음. 두 화면 모두 "수정하기"/"삭제하기"로 통일. "삭제하기" 텍스트 색도
목록 셀과 맞춰 검정으로 정리.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
활동 탭의 피드 셀 탭·연결 작품 배너 탭이 아무 동작도 하지 않고 있었다(연결 작품 배너는 TODO 주석만 있는 no-op, 피드 셀은 탭 제스처 자체가 없었음). 미리보기(UserPageView)·전체보기 (UserFeedListView) 화면 둘 다 동일하게 비어 있어 두 View에 onFeedTapped/onNovelTapped를 나란히 추가하고 Factory→Assembly→4탭 Root까지 배선했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
이미 신고한 피드/댓글을 다시 신고했을 때 화면마다 동작이 달랐다 — 피드 목록(SosoFeed)은 실패해도 View가 안 읽는 죽은 상태만 갱신해 토스트가 안 떴고, 피드 상세(FeedDetail)는 try?로 성공/실패를 구분하지 못해 실패해도 항상 "신고 완료" 알럿이 떴다. UserPage/NovelDetail은 이미 실패 시 토스트를 띄우고 있었지만 "알 수 없는 오류"로 뭉뚱그려졌다. 실제 서버 에러 코드(REPORT-002 피드/REPORT-004 댓글, dev 서버 실측으로 확인 — 처음 전달받은 FEED-008/COMMENT-003는 오기였다)를 RepositoryError.alreadyReported로 매핑하고, 5개 화면 (SosoFeed/FeedDetail/UserPage/UserFeedList/NovelDetail) 모두 "이미 신고한 피드예요"/"이미 신고한 댓글이에요"(요청대로 문구 분리) 전용 토스트로 통일했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
비공개 유저 프로필의 닉네임/소개/이미지가 서버가 실제로 성공 응답을 주는데도 화면에 안 보였다. loadUserPage가 프로필·장르·취향·서재통계·컬렉션미리보기 5개를 한 병렬 묶음으로 일괄 대입하던 구조라, 비공개 유저에게 실패하는 나머지 호출(장르/취향은 privateProfile로 명시 실패, 서재 통계· 컬렉션 미리보기는 서버가 그 코드를 구분 못 해 일반 에러로 실패 — 둘 다 실측으로 확인, 문서엔 서재 통계가 안전하다고 잘못 적혀 있었다) 중 하나만 실패해도 이미 성공한 프로필 응답까지 함께 버려졌다. 프로필(loadProfileSection)만 유일하게 실패를 전면 에러로 취급하도록 남기고, 서재 통계·컬렉션 미리보기·장르/취향은 각각 독립된 async let으로 완전히 격리해 실패를 조용히 흡수하게 했다. ProfileMapper도 화면이 안 쓰는 genrePreferences 필드 때문에 전체 매핑이 실패하지 않도록 관대하게 고쳤다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
서버가 GET /users/info의 gender 값을 "MALE"/"FEMALE"에서 계정정보 API와 같은 "M"/"F"로 통일했다(실측 확인). syncUserBasicInfo()가 그 값을 원문 그대로 userDefaults에 캐시하는데, 그 캐시를 읽는 localGender(from:)는 여전히 "MALE"/"FEMALE"만 받아들이고 있어 "성별/나이 변경" 화면 진입마다 Failed to convert 'F' to Gender 매핑 에러가 났다. localGender(from:)가 "M"/"F"와 "MALE"/"FEMALE"(이 전환 전 이미 캐시된 기존 설치용) 둘 다 받아들이도록 하고, localGenderRawValue(from:)는 새로 쓸 때부터 "M"/"F"를 쓰도록 맞췄다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
작품 검색 API(GET /novels)의 recordRecentSearch 파라미터가 클라이언트에 아예 없어, 일반 검색· 피드 작품 연결·컬렉션 작품 담기 세 화면 모두 로그인 상태면 서버가 똑같이 최근 검색어로 기록하고 있었다. 검색이 목적인 화면(일반 검색)과 검색이 다른 작업의 부수 수단인 화면(작품 연결/작품 담기)을 구분할 방법이 클라이언트에 없었던 것. SearchNovelUseCase.searchByText/SearchNovelRepository.searchNovelByText에 recordRecentSearch: Bool을 필수 파라미터로 추가했다(기본값 없음 — 새 호출부가 이 판단을 빠뜨리지 못하게). 이 값을 결정하는 곳은 Repository/Data가 아니라 호출부(Feature)다 — Repository는 세 화면이 공유하는 단일 메서드라 호출자를 구분할 방법이 없고, "이게 사용자의 진짜 검색 의도인가"는 그 화면을 그리는 Feature만 아는 제품 판단이라 그렇다. NormalSearchViewModel만 true, CreateFeedViewModel/CollectionSearchNovelViewModel은 false. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
작품 상세에서 작가 이름을 탭해 일반 검색 결과로 진입하면, 화면을 가만히 둬도 /novels 검색과 /novels/recent-searches가 계속 반복 호출되는 버그가 있었다. NormalSearchViewModel.init이 initialQuery로 곧바로 executeSearch(Task 스폰)를 부르고 있었는데, 이 init은 NormalSearchView의 State(initialValue:) 인자 표현식으로 쓰인다. 그 표현식은 "저장값은 최초 1회만 반영"과 무관하게 App 탭 Root의 .navigationDestination(for:) 클로저가 재평가될 때마다(그 Root의 아무 @State나 바뀌기만 해도) 매번 다시 실행되고, 그렇게 만들어졌다 버려지는 "고아" 인스턴스도 각자 자기 init을 완주하며 실제 네트워크 요청을 냈다. init은 검색어만 채우고, 실제 검색 실행은 실제로 마운트되는 View에서만 발화하는 onAppear로 옮겨 didRunInitialSearch(기존 didAutoFocus와 동일한 1회성 가드 패턴)로 딱 한 번만 하도록 고쳤다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
작가 이름 탭으로 진입한 검색 결과 화면이 필요 없는 API까지 부르고 있었다 — initialQuery로 바로 결과 화면이 뜨는데도 onAppear가 소소픽·인기 키워드·최근 검색어 조회를 항상 같이 실행했다(브라우즈 섹션 자체가 안 보이는데 그 데이터를 받아온 것). isSearchExecuted로 가드해 이 세 호출을 건너뛴다. 또한 작가 이름 탭으로 실행된 검색은 사용자가 직접 의도한 검색이 아니므로 최근 검색어로 남을 필요가 없다. 사용자가 직접 실행한 검색(executeSearch, 검색바 제출·최근 검색어 칩·자동완성 제안어 탭)과 분리해 executeInitialSearch 액션을 신설, recordRecentSearch: false로 검색한다. 실패 후 재시도는 새 검색이 아니라 방금 그 검색을 다시 시도하는 것이므로, 원래 검색이 기록 대상이었는지를 세션 동안 유지해 그대로 따르는 retrySearch 액션으로 분리했다 — 안 그러면 작가 이름 검색이 실패 후 재시도할 때 갑자기 기록 대상으로 바뀌는 모순이 생긴다. 성공 후 최근 검색어 재조회도 기록 대상 검색에만 하도록 좁혔다(기록 안 한 검색은 다시 불러와도 목록이 그대로라 낭비였다). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
loadPreferenceBundle이 일반 에러(privateProfile 이외)를 presentError로 넘기고 있었는데, presentError는 state.hasLoadError를 세워 UserPageView가 body 전체를 NetworkErrorView로 덮는다. 그러면 이 함수와 병렬로 독립 실행되는 loadProfileSection이 이미 성공시킨 프로필(닉네임·소개·이미지) 까지 함께 가려진다 — 이 함수를 프로필과 완전히 격리하려던 이번 QA 라운드의 목적 자체가 privateProfile 이외의 에러 경로로는 무의미해지는 상태였다(PR 리뷰에서 발견). 서재 통계·컬렉션 미리보기와 동일하게 실패를 조용히 흡수하도록 바꿨다 — 장르/취향 섹션만 비게 되고 상단 프로필은 영향받지 않는다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
recordRecentSearch 파라미터 추가 이후에도 문서 상단 몇 곳이 옛 2-파라미터 시그니처 (searchByText(text, page: 0))로 남아 있었다(PR 리뷰에서 발견). 실제 시그니처로 정정. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
localGenderRawValue(from:)가 "MALE"/"FEMALE"에서 "M"/"F"로 바뀌었는데
saveAccountInfo_success_updatesLocalStorage/loadLocalGenderAndBirth_missing_fallsBackToServerAndCaches
두 테스트가 옛 포맷을 기대한 채 남아 있었다(PR 리뷰에서 CI 실패로 발견). 기대값을 새 포맷으로
고치고, 로컬 캐시가 새 포맷("F")으로 저장된 경우를 읽는 테스트를 추가했다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
PR 리뷰에서 두 곳의 문서-코드 drift가 발견됐다. - CollectionFeature/CLAUDE.md가 여전히 icThreedots를 가리켰는데 실제로는 이번 브랜치에서 icThreedotsVertical로(NovelDetailFeature/UserPageFeature와 통일) 이미 바뀌어 있었다. - WSSPillBadge의 action nil 분기 독스트링이 예시로 들던 CollectionSearchNovelView는 #255 QA로 배지 단독 탭 방식으로 바뀌어 더 이상 그 분기를 쓰지 않는다(WSSComponent/CLAUDE.md엔 이미 기록됨). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
loadPreferenceBundle이 isSilentRefresh를 받고 있었지만 실제로는 fresh/silent 구분 없이 항상 동일하게 실패를 조용히 흡수해 함수 본문 어디에서도 값을 쓰지 않았다(PR 리뷰에서 발견) — loadProfileSection과 시그니처만 맞춰놓은 흔적이라 "silent와 fresh가 다르게 동작한다"는 오해를 줄 수 있었다. 파라미터를 제거했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
…테스트 추가 리뷰 라운드3 Nit 반영 — 4개 신고 메서드 모두 이미 신고 시 RepositoryError.alreadyReported로 변환되는지 커버리지 없이 코드만 있던 상태였다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu
…케이스 추가 RepositoryError에 alreadyReported 케이스가 추가되며 Demo의 switch가 비-exhaustive해져 CI(Test NotificationData)가 컴파일 실패했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ob5T5NaMoQWUqi9Rf1pW1
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
💭 Summary
2026-09-08 QA 라운드에서 나온 컬렉션·피드·유저페이지·설정·검색 5개 영역의 버그를 수정했어요.
실제 서버 로그·에러 코드를 근거로 원인을 확인했고, 특히 유저페이지의 비공개 프로필 상단 정보 노출 문제는 세 차례 재현·수정을 거쳐 해결했어요.
🔑 Key Changes
컬렉션
피드
REPORT-002/REPORT-004) 일반 실패 토스트 대신 "이미 신고한 피드예요"/"이미 신고한 댓글이에요"로 대상별로 안내하도록 통일했어요(FeedFeature·NovelDetailFeature·UserPageFeature공통).유저페이지
설정
검색
recordRecentSearch)이 일반 검색 화면이 아닌 다른 화면(피드 작품 연결, 컬렉션 작품 추가)에서도 함께 기록되던 걸, 실제 일반 검색 화면에서만 기록하도록 정리했어요. 이 판단은 Repository/Data가 아니라 화면을 아는 Feature 레이어에서 하도록 설계했어요./novels·/soso-picks·/keywords/popular·/novels/recent-searches가 반복 호출되던 심각한 버그를 고쳤어요. SwiftUI@State(initialValue:)에 side effect를 넣어뒀던 게 원인으로, 화면이 재구성될 때마다 버려지는 "고아" 인스턴스가 계속 API를 호출하고 있었어요. 이 진입 경로는 소소픽·키워드·최근 검색어 조회도 건너뛰고, 최근 검색어에도 남기지 않도록 함께 정리했어요.📱 Simulation
🧑🧒🧒 To Reviewer
UserPageFeature/CLAUDE.md의 "비공개 프로필" 절에 전체 경위를 남겨뒀어요) — 비슷한 병렬 로드를 다른 화면에 추가할 때 참고해 주세요.WSSPillBadge의action: nil(행 전체 탭) 분기는 이번에 유일한 콜사이트가 없어졌지만, API 자체는 죽은 코드가 아니라 의도적으로 남겨뒀어요.※ Reference
api-spec(OpenAPI) 문서에 없는 항목이라 실제 서버 응답으로 확인했어요.🤖 Generated with Claude Code
https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu