Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Projects/Feature/SettingFeature/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
- **완결 알림/휴재 복귀 알림 목록(`NovelNotificationListView`, #188)** — `NotificationSettingView`의 두 row에서 push. **화면 구조가 완전히 같아**(사용자 확정) 별도 View 두 개를 만들지 않고 `NovelNotificationType`(`.completion`/`.hiatusReturn`)만 다르게 주입해 같은 View/VM을 재사용한다 — `SettingFeatureFactory`에 `makeCompletionNotificationListView`/`makeHiatusReturnNotificationListView` 두 진입점은 있지만 내부에서 같은 `NovelNotificationListView`(제목만 다름)를 만든다.
- **툴바 우측은 "수정"↔"삭제" 전환 버튼 하나**(`state.isEditing`, 사용자 확정) — 기본은 "수정"(gray300, 항상 탭 가능)이고, 누르면 "삭제"로 바뀌며 그때부터 행 선택이 가능해진다(`NovelNotificationRow`는 행 전체가 탭 영역이지만 `isEditing`이 아니면 `toggleSelection`이 VM에서 no-op). **체크 버튼 자체가 `isEditing`이 아니면 렌더되지 않는다**(숨김이 아니라 조건부 렌더 — "수정"을 눌러야 비로소 나타남). "삭제"는 하나 이상 선택해야 활성(primary100)이고, 선택 없이는 비활성(gray200) — "수정"의 gray300과는 다른 톤이라 섞어 쓰지 말 것. 편집 모드를 취소할 방법은 따로 없다(뒤로가기로 화면을 나가면 초기화). 삭제 성공 시 `isEditing`도 함께 꺼져 다음 진입은 항상 "수정"부터 다시 시작한다.
- **`NovelNotificationRow`는 `Button`이 아니라 `onTapGesture`**(사용자 확정) — `.disabled`를 얹은 `Button`은 비편집 상태에서 행 전체가 옅게 흐려 보이는 부작용이 있었다(no-op이어야 할 상태가 "비활성화된 것처럼" 보임). 체크 아이콘은 `WSSComponent`의 `WSSSelectionCheckIcon`(#188 — 여러 화면에 흩어져 있던 크로스페이드+스케일 스프링 구현을 공용 컴포넌트로 승격)을 그대로 쓴다 — 새 화면에서 같은 아이콘 쌍이 필요하면 직접 구현하지 말고 이 컴포넌트를 재사용할 것.
- 삭제는 `WSSAlertType.deleteNovelNotificationSubscriptions(summary:)`(신규, `WSSComponent`) 확인 알럿을 거친다. `summary`는 **목록에 보이는 순서 기준 첫 선택 항목 제목**("{제목} 외 N작품", View의 `Presentation` 확장이 조합 — 알럿 컴포넌트는 문구를 모른다).
- 삭제는 `WSSAlertType.deleteNovelNotificationSubscriptions(summary:)`(신규, `WSSComponent`) 확인 알럿을 거친다. `summary`는 **목록에 보이는 순서 기준 첫 선택 항목 제목**으로 View의 `Presentation` 확장이 조합한다(알럿 컴포넌트는 문구를 모른다) — 단일 선택은 접두어 없이 제목만("{제목}"), **다중 선택은 "Re : {제목} 외 N작품"**(사용자 확정, #269 — 단일/다중 문구 규칙이 대칭이 아니다).
- 페이지네이션은 서버가 명시적으로 내려주는 `nextSubscriptionID` 커서를 쓴다(`NotificationDomain`의 `Notification`처럼 마지막 항목 id로 유추하지 않음).
- ⚠️ **현재 로드된 페이지를 통째로 선택 삭제하면 목록은 비지만 다음 페이지가 남아있을 수 있다** — `state.subscriptions.isEmpty`만 보고 빈 상태(`WSSEmptyView`)로 판정하면 서버에 남은 구독이 화면에서 사라진 것처럼 보인다. VM이 삭제 직후 `subscriptions.isEmpty && hasNextPage`면 자동으로 다음 페이지를 이어 로드하고(`loadMore()`), View는 그 과도기를 빈 상태 대신 `LoadingView()`로 가린다. 삭제 로직을 만질 땐 이 자동 이어받기를 빠뜨리지 말 것.
- 목록이 비면 `WSSEmptyView(type: .novelNotification)`의 "작품 둘러보기" CTA가 뜨는데, 검색 화면은 다른 Feature 모듈이라 이 화면이 직접 못 연다 — `NovelNotificationListRoute.browseNovels`(#253 리뷰에서 Route로 편입 — 처음엔 "빈 상태 CTA"라 클로저로 남겼다가, App이 실제로 push하는 화면 전환 의도인데 기본값 no-op까지 있어 `MyLibraryRoute.search`와 판정이 어긋난다는 지적으로 정정)를 올리면 실제 이동은 App이 정한다(#201부터 `MypageRootView`/`LibraryRootView` 둘 다 일반 검색 화면으로 push해 배선 완료).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ private extension NovelNotificationListView {
)
}

/// "{첫 선택 작품 제목} 외 N작품" — 목록에 보이는 순서 기준으로 첫 선택 항목을 고른다.
/// "Re : {첫 선택 작품 제목} 외 N작품" — 목록에 보이는 순서 기준으로 첫 선택 항목을 고른다.
var deleteSummaryText: String {
let selected = viewModel.state.subscriptions.filter { viewModel.state.selectedNovelIDs.contains($0.novelID) }
guard let first = selected.first else { return "" }
return selected.count == 1 ? first.novelTitle : "\(first.novelTitle) 외 \(selected.count - 1)작품"
return selected.count == 1 ? first.novelTitle : "Re : \(first.novelTitle) 외 \(selected.count - 1)작품"
}

var toastBinding: Binding<Bool> {
Expand Down
12 changes: 6 additions & 6 deletions Projects/UI/WSSComponent/Sources/Alert/WSSAlertStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,14 @@ public extension WSSAlertType {

case .deleteNovelNotificationSubscriptions(let summary):
WSSAlertContent(
iconImage: nil,
title: "해당 작품 알림을 삭제할까요?",
titleFont: .title1,
titleBottomPadding: 10,
iconImage: WSSImage.icModalWarning.swiftUIImage,
title: "작품 알림을 삭제할까요?",
titleFont: .title2,
titleBottomPadding: 4,
description: WSSAlertDescription(
title: summary,
font: .body2,
titleColor: WSSColor.wssGray300.swiftUIColor,
font: .label1,
titleColor: WSSColor.wssGray200.swiftUIColor,
bottomPadding: 24
),
buttons: [
Expand Down
Loading