Skip to content

[Fix] #255 - 컬렉션·피드·유저페이지·설정·검색 QA 수정 - #259

Merged
Guryss merged 24 commits into
developfrom
Fix/#255
Sep 10, 2026
Merged

Guryss merged 24 commits into
developfrom
Fix/#255

Conversation

@Guryss

@Guryss Guryss commented Sep 9, 2026

Copy link
Copy Markdown
Member

💡 Issue


💭 Summary

2026-09-08 QA 라운드에서 나온 컬렉션·피드·유저페이지·설정·검색 5개 영역의 버그를 수정했어요.

실제 서버 로그·에러 코드를 근거로 원인을 확인했고, 특히 유저페이지의 비공개 프로필 상단 정보 노출 문제는 세 차례 재현·수정을 거쳐 해결했어요.


🔑 Key Changes

컬렉션

  • 컬렉션 상세 더보기 버튼 터치 영역을 44×44로 넓히고, 드롭다운이 스크롤 상태와 무관하게 항상 최상단에 보이도록 z-order를 정리했어요.
  • 컬렉션 설명 입력 박스가 입력할 때마다 계속 늘어나던 것을 고정 높이 + 내부 스크롤로 바꾸고, 캐럿이 항상 보이도록 자동 스크롤을 추가했어요.
  • 작품 검색 결과가 없을 때 문의 링크를 연결하고, 검색 결과 행의 탭 영역을 배지로 제한해 실수로 토글되는 걸 막았어요.
  • 선택 개수 초과 토스트 문구("N개까지 선택이 가능해요")를 확정하고 3개 화면(작품 평가·키워드·컬렉션)에 동일하게 반영했어요.
  • 미리보기 표지의 불필요한 그림자를 제거했어요.

피드

  • 피드 상세 드롭다운 문구를 "수정하기"/"삭제하기"로 통일했어요.
  • 피드 작성 중 토스트·검증 상태가 어긋나던 버그를 고쳤어요.
  • 내 피드가 없을 때 "글 쓰러 가기" 버튼이 비활성 상태로 보이던 걸 고쳤어요.
  • 사진 추가 버튼 좌측 패딩을 정리하고, 작품 연결 검색 결과가 없을 때 문의 링크를 연결했어요.
  • 이미 신고한 피드/댓글에 다시 신고를 시도하면(서버 REPORT-002/REPORT-004) 일반 실패 토스트 대신 "이미 신고한 피드예요"/"이미 신고한 댓글이에요"로 대상별로 안내하도록 통일했어요(FeedFeature·NovelDetailFeature·UserPageFeature 공통).

유저페이지

  • 타 유저 프로필의 "활동" 탭 피드 셀 탭(→피드 상세)·연결 작품 배너 탭(→작품 상세)이 QA 전까지 아무 동작이 없던 걸 배선했어요. "전체보기" 화면도 동일하게 고쳤어요.
  • 비공개 프로필이어도 상단 프로필(닉네임·소개·이미지)은 항상 보여야 하는데, 장르/취향·서재 통계·컬렉션 미리보기 중 하나라도 비공개로 실패하면 프로필까지 같이 가려지던 문제를 고쳤어요. 로드를 4갈래로 완전히 독립시켜 하나의 실패가 다른 걸 가리지 않게 했어요.

설정

  • 서버가 성별 응답 포맷을 "MALE"/"FEMALE"에서 "M"/"F"로 바꾸면서 성별/나이 변경 화면이 로컬 캐시를 잘못 읽던 에러를 고쳤어요. 새 포맷을 읽고 새 포맷으로 쓰도록 하되, 기존 캐시(구 포맷)도 계속 읽히도록 하위 호환을 유지했어요.

검색

  • 최근 검색어 저장(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 자체는 죽은 코드가 아니라 의도적으로 남겨뒀어요.
  • SocialData의 REPORT-002/REPORT-004 매핑에 리뷰 라운드에서 나온 Nit을 반영해 유닛 테스트를 추가했어요.

※ Reference

  • REPORT-002/REPORT-004, 성별 포맷 변경 등은 api-spec(OpenAPI) 문서에 없는 항목이라 실제 서버 응답으로 확인했어요.

🤖 Generated with Claude Code

https://claude.ai/code/session_014QhNWwcuQqwRE3BFyYj7pu

@Guryss Guryss self-assigned this Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

테스트 커버리지 리포트

Module Coverage
AuthData 55.3%
AuthDomain 98.2%
BaseDomain 70.3%
CollectionDomain 98.3%
CollectionFeature 0.0%
CommentData 0.0%
CommentDomain 94.9%
FeedData 62.8%
FeedDomain 98.6%
HomeFeature 0.0%
LibraryFeature 0.0%
NotificationData 33.9%
NotificationDomain 100.0%
NotificationFeature 0.0%
NovelData 15.4%
NovelDetailFeature 0.0%
NovelDomain 92.9%
NovelReviewData 77.4%
NovelReviewDomain 96.8%
NovelReviewFeature 5.1%
ProfileData 46.1%
ProfileDomain 97.1%
RecommendationData 54.7%
RecommendationDomain 91.7%
SearchData 5.1%
SearchDomain 100.0%
SettingData 31.1%
SettingDomain 99.0%
SocialData 53.4%
SocialDomain 87.5%
SplashData 77.1%
SplashDomain 95.5%
SplashFeature 32.9%

Generated by CI on 2026-09-10 09:48 KST

Guryss and others added 24 commits September 10, 2026 08:50
검색어에 해당하는 작품이 없을 때 뜨는 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
활동 탭의 피드 셀 탭·연결 작품 배너 탭이 아무 동작도 하지 않고 있었다(연결 작품 배너는 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
@Guryss
Guryss merged commit dd24d72 into develop Sep 10, 2026
38 checks passed
@Guryss
Guryss deleted the Fix/#255 branch September 10, 2026 03:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] 컬렉션·피드·유저페이지·설정·검색 QA 수정

1 participant