feat: 댓글/대댓글 CRUD + 큐레이팅 화면 + 채팅방 오디오 오류 배너 #4#36
Conversation
- BattleAPI / BattleService.postVote(battleId:, body:) 추가 - BattleInterface.submitPostVote + Default/Repository 구현 (PreVoteResult/PreVoteRequest 재사용) - PreVoteFeature.VoteMode (.pre / .post) 신설 - State.voteMode + State.primaryButtonTitle 으로 CTA 텍스트 분기 - primaryButtonTapped 가 voteMode 별로 submitPreVote / submitPostVote 디스패치 - AsyncAction.submitPostVote + InnerAction.postVoteResponse + CancelID.submitPostVote - 성공/실패 모두 delegate.voteSubmitted 로 동일하게 알림 - ChatRoomFeature.DelegateAction.requestFinalVote(battleId) 추가 - customAlert.confirmTapped 시 audioPlayer.pause + delegate.requestFinalVote 발행 - ChatCoordinator: chatRoom.requestFinalVote 수신 시 .preVote(.init(battleId:, voteMode: .post)) push - 모든 switch 분기에 explicit return 적용
- 일부 battle 응답에서 options[].label 이 누락되어 BattleOptionDTO 디코딩 실패하던 문제 수정 - BattleOptionDTO.label 을 Optional 로 변경 - 매퍼에서 nil 일 때 빈 문자열 fallback (Entity 의 label 은 표시용으로 직접 사용되지 않으므로 안전)
- 일부 시나리오 응답에서 philosophers[].label 이 누락되어 ScenarioPhilosopherDTO 디코딩 실패하던 문제 수정 - ScenarioPhilosopherDTO.label 을 Optional 로 변경 - 매퍼에서 인덱스 기반 폴백(A/B/C/D…) 적용해 ChatRoomFeature 의 label 매칭이 계속 유효하도록 보장
- ChatRoomView 의 layout 상수(bubbleMaxWidth / avatarSize / avatarImageWidth / avatarImageHeight) 를
private enum Metric 으로 묶음. 호출처 모두 Self → Metric 으로 정리
- ChatRoomFeature.View 에 onDisappear 추가
- audioObserver(.audioObserver) cancel + audioPlayer.pause + isPlaying = false
- swipe back / programmatic dismiss 등 backButton 외 경로로 화면을 나가도 음원이 멈춤
- ChatRoomView 에 .onDisappear { send(.onDisappear) } 연결
- Domain/Entity/Home/Comment.swift 신설
- Comment / CommentAuthor / CommentSortType / CommentTab 모델
- Chat 모듈에 Comment Feature/View 디렉터리 추가 (mock 단계)
- ChatCoordinator.ChatScreen.comment(CommentFeature) 케이스 추가
- preVote.voteSubmitted 시 voteMode 분기:
- .pre → chatRoom push
- .post → comment push (최종 투표 후 댓글 화면 진입)
- comment.delegate.dismiss 시 backAction
- ChatCoordinatorView 에 comment 케이스 렌더
- PreVoteFeature.DelegateAction.voteSubmitted 시그니처에 voteMode 추가
- ChatRoomFeature
- hasFinishedListening 을 UserDefaults 로 영속화해 재진입 시 시킹 즉시 허용
- CustomConfirmationPopupView 정비 (디자인 토큰 / 패딩 정렬)
- Entity/Home/BattleVoteStats + BattleVoteStatsOption 신설 - Data Model: BattleVoteStatsDataDTO + BattleVoteStatsOptionDTO + 매퍼 (updatedAt 은 ISO8601 fractional seconds 파서) - BattleAPI / BattleService.voteStats(battleId:) (GET, no body) - BattleInterface.fetchVoteStats + Default/Repository 구현 - CommentFeature - AsyncAction.fetchVoteStats + InnerAction.voteStatsResponse(Result) - View.onAppear 에서 fetch 트리거 (isLoadingStats 가드) - 성공 시 응답 옵션 0/1 을 VoteSummary.optionA/B 에 매핑, ratio·label·stance 반영 - 실패 시 fallback (기존 voteSummary) 유지 + 로그 - CommentView.onAppear 에서 send(.onAppear) 호출
- Entity/Error/BattleError 신설 (Attendance_iOS 의 AttendanceError 패턴)
- networkError / decodingError / noData / unauthorized / backendError /
serverError / unknown + LocalizedError 텍스트 + BattleError.from(_:) 컨버터
- BattleRepositoryImpl: 모든 throw AuthError.backendError → throw BattleError.backendError
- PreVoteFeature / ChatRoomFeature / CommentFeature
- Result<*, AuthError> → Result<*, BattleError>
- mapError(AuthError.from) → mapError(BattleError.from)
- BattleVoteStatsOptionDTO
- title 을 Optional 로 (응답에서 누락되는 케이스 디코딩 실패 해결)
- imageUrl 필드 추가
- BattleVoteStatsOption Entity 에 imageUrl 추가
- 매퍼: ratio 가 1 보다 크면(서버가 퍼센트로 내려준 경우) 100 으로 나눠 비율로 정규화
- Entity/Home/BattlePerspective + Page + User + Option + BattlePerspectiveSort 신설 - Data Model: BattlePerspectivePageDataDTO / 매퍼 (ISO8601 fractional 파서, null fallback) - BattleAPI / BattleService.perspectives(battleId:, cursor:, size:, optionLabel:, sort:) (GET + query) - BattleInterface.fetchPerspectives + Default/Repository 구현 - CommentFeature - State.isLoadingComments / nextCursor / hasNext / comments - AsyncAction.fetchPerspectives(reset:) + InnerAction.perspectivesResponse(Result, reset:) - onAppear 에서 voteStats + perspectives 병렬 호출 - filterTapped / sortTapped 시 reset 페치 (서버에 optionLabel / sort 쿼리) - 응답 BattlePerspective 를 CommentItem 으로 매핑 (deterministic UUID, relativeTime) - CommentFilter.queryLabel / CommentSort.perspectiveSort computed 로 enum → 쿼리값 매핑
- PieckeDomain.comment 추가 (api/v1/comments/) - Entity: CommentLikeResult / CommentError 신설 - Data: CommentLikeDataDTO + 매퍼 - CommentAPI / CommentService.like(POST) · unlike(DELETE) 추가 (헤더 only) - DomainInterface: CommentInterface (likeComment / unlikeComment) + DependencyKey + Default 구현 - Repository: CommentRepositoryImpl (MoyaProvider<CommentService>.authorized) - App DI: CommentRepositoryImpl as CommentInterface 등록 - CommentFeature - State.CommentItem.perspectiveId 필드 추가, BattlePerspective 매핑 시 ID 보존 - View.likeTapped 시 optimistic update + AsyncAction.toggleLike 디스패치 (현재 isLiked 기준 POST/DELETE 분기) - InnerAction.likeResponse 로 서버 응답(likeCount/isLiked) 반영, 실패 시 로그 - @dependency(\.commentRepository) 주입
- PieckeDomain.perspective 추가 (api/v1/perspectives/)
- Entity
- PerspectiveComment / PerspectiveCommentPage / PerspectiveCommentUser
- PerspectiveCommentMutationResult (create/update 응답)
- CommentReplyItem 에 commentId 필드 + PerspectiveComment 매핑 init 추가
- Data Model: Perspective DTO/매퍼 (cursor 페이지 + create/update 응답 + 부모 perspective detail 재사용)
- PerspectiveAPI / PerspectiveService
- detail (GET) / listLabeledComments (GET + cursor·size) / createComment (POST) /
updateComment (PUT) / deleteComment (DELETE)
- PerspectiveCommentBody { content }
- DomainInterface: PerspectiveInterface + Default + DependencyKey
- Repository: PerspectiveRepositoryImpl (MoyaProvider<PerspectiveService>.authorized) + EmptyDTO
- App DI: PerspectiveRepositoryImpl as PerspectiveInterface 등록
- CommentReplyFeature
- State: perspectiveId / parentComment / replies / nextCursor / hasNext / editingCommentId / 로딩 플래그
- AsyncAction: fetchParent / fetchReplies(reset:) / createReply / updateReply / deleteReply /
toggleParentLike / toggleReplyLike
- InnerAction 으로 응답 단일화, 좋아요는 /comments/{id}/likes 재사용
- onAppear 시 부모 + 답글 페이지 병렬 페치, sendTapped 시 createReply/updateReply 분기
…렬 토큰 정리 #4 - CommentFeature.State.perspectiveId(Int?) / isSubmitting 추가 - isSendEnabled 가 perspectiveId 존재 + 비제출 중일 때만 활성 - AsyncAction.createComment(perspectiveId:, content:) + InnerAction.createCommentResponse 추가 - sendTapped 시 perspectiveRepository.createComment(POST /perspectives/{pid}/comments) 호출 - 성공 시 fetchPerspectives(reset:true) 로 리스트 재로딩 - @dependency(\.perspectiveRepository) 주입 - BattleService.createPerspective + CreatePerspectiveRequest 신설 (battle 단위 댓글 작성용도 함께 준비 — 백엔드 endpoint 확정 시 활용) - BattleInterface.createPerspective + Default/Repository 구현 (BattlePerspective 반환) - CommentView 정렬 버튼 색상을 디자인 토큰(.primary500 / .primary50) 으로 정리, hardcoded hex 제거 - CommentView 필터 탭 라벨을 voteSummary.optionA/B.title 로 동적 매핑 (.pen `전체/<옵션 title>/<옵션 title>` 매칭) - 필터 탭 하단 underline 추가 (선택 primary500 2.5pt, 비선택 neutral200 1pt)
- CommentItem.authorImageURL / optionLabel 필드 추가
- BattlePerspective → CommentItem 매핑 시 user.characterImageUrl + option.title 전달
- CommentView 댓글 카드
- 아바타: 첫 글자 텍스트 → KFImage(authorImageURL) + Circle stroke beige600
(URL 없으면 기존 fallback 사용)
- 옵션 라벨 뱃지: "A 변기는 변기다" → "변기는 변기다" 만 (.pen Z3egi 매칭)
- 시간 폰트 10pt → 12pt Medium (.pen `nI8bv` 매칭)
- Kingfisher import 추가 (Chat 모듈)
- CustomConfirmationPopupView.reportContent .pen byHAR 매칭
- 헤더 "신고사유" 16pt SemiBold .primary800 (#653226)
- 사유 5개 라디오 grid (2-column)
- 좌: 영리목적/홍보성 · 음란성/선정성 · 기타
- 우: 같은 내용 반복 게시 · 욕설/인신공격
- Ellipse 20x20 stroke .gray200 (#b0afae) 3pt + 선택 시 inner Circle .primary500
- 버튼 horizontal: 신고하기 (.secondary50 fill / .primary800 text) /
뒤로가기 (.primary500 fill / .secondary50 text)
- frame 343 cornerRadius 6 .beige500 (#f5f1ea) + border .primary500 1.5pt
- ReportReason enum (commercial/repeat_/explicit/insult/other) 추가
- CommentView.sortButton 간격 0 → 8pt (.pen sort frame gap 매칭)
- byHAR 디자인 매칭: 전체 frame width 343 / height 236 - padding top 20 + 헤더 + gap 16 + 사유 grid (height 116) + gap 16 + 버튼 - 사유 grid: 내부 padding vertical 12 + .frame(height: 116) 명시 - reportContent 를 @ViewBuilder 서브뷰로 분리 - reportHeader / reasonGrid / reasonColumn / reasonRow / reasonRadio - reportButtons / reportSubmitButton / reportCancelButton
…틀 타이틀 동적 #4 - AGENTS.md ViewBuilder 규칙(95~196) 준수 - 모든 sub-view 를 @ViewBuilder private func 으로 분리 - navigationBar/summarySection/changeBadge/voteSide/voteProgress/avatarLabel - filterSection/filterTabs/sortRow/filterButton/sortButton - commentList/commentCard/commentBody/commentHeader/commentAuthorBlock - reportButton/optionBadge/commentActions/moreButton/replyCountButton - likeButton/actionLabel/avatar - inputBar/inputTextBox/sendButton - CommentFeature: fetchBattle 추가 - AsyncAction.fetchBattle / InnerAction.battleResponse / CancelID.fetchBattle - onAppear merge 에 fetchBattle 추가 - 응답 성공 시 state.title = detail.battleInfo.title - State.init default title "" + voteSummary .mock (서버 응답 도착하면 덮어씀)
- CommentItem 을 Entity/Sources/Home/Comment.swift 로 이동 - BattlePerspective→CommentItem 매핑 init / deterministicUUID / relativeTimeString 포함 - 사용처 없는 mocks 정적 배열 제거 - VoteSummary / VoteOptionSummary / CommentFilter / CommentSort 신규 파일 Entity/Sources/Home/VoteSummary.swift 로 이동 - CommentReplyFeature: replies 기본값 mock fallback → 빈 배열 - CommentReplyView Preview 제거 (mockdata 의존 끊기) - AGENTS.md ViewBuilder 규칙 강화: 모든 sub-view 는 @ViewBuilder + private func 단일 뷰여도 var 형태 금지 — 호출부 일관성 확보
- finalVoteContent HStack spacing 10 → 0 두 버튼 (다시 들어볼래요 / 최종투표하기) 이 붙어있도록 변경
- inline default + 외부 주입 인자만 init 파라미터로 노출 - CommentFeature.State.init(battleId:) — title/voteSummary/comments 등은 inline default - PreVoteFeature.State.init(battleId:, voteMode:) — battle 파라미터 제거 (internal state) - CommentReplyFeature.State.init(perspectiveId:, parentComment:) — replies 인자 제거 - PreVoteView Preview: .init() 만 호출 (.mock 의존 제거) - AGENTS.md: 파라미터 ≥ 2 시 멀티 라인 규칙 추가
…re UseCase 패턴 도입 #4 API - GET /api/v1/battles/{battleId}/perspectives/me — 본인 perspective 조회 - DELETE /api/v1/perspectives/{perspectiveId} — perspective 삭제 - BattleAPI/PerspectiveAPI / Service / RepositoryImpl 모두 연결 - PerspectivesQueryRequest 별도 파일로 분리 (Encodable) UseCase 도입 (Attendance_iOS 패턴) - BattleUseCaseImpl / HomeUseCaseImpl / CommentUseCaseImpl / PerspectiveUseCaseImpl 기존 Interface 그대로 채택 + @dependency 로 Repository 주입 - 각 Impl 에 DependencyKey 채택 (live / test / preview 모두 명시) - DependencyValues 키: battleUseCase / homeUseCase / commentUseCase / perspectiveUseCase - 5개 Feature 의 @dependency 를 Repository → UseCase 로 전부 교체 PreVoteFeature - voteMode == .pre 일 때만 fetchMyPerspective 호출 - 응답이 있으면 customAlert = .alreadyWatched() 표시 - confirm: deleteMyPerspective(perspectiveId:) 호출 → state.myPerspective = nil - cancel: dismiss - ifLet(\.$customAlert, action: \.scope.customAlert) + CustomConfirmAlert DesignSystem - CustomAlertStyle.alreadyWatched 추가 - CustomAlertState.alreadyWatched() 팩토리 (.pen nah54 매칭) - CustomConfirmationPopupView.alreadyWatchedContent 신규 헤더 / 본문 / 취소·다시 버튼 (.pen 색상 토큰 매칭) PreVoteView - .customAlert($store.scope(state:\.customAlert, action:\.scope.customAlert)) modifier 추가 규칙 / 포맷 - AGENTS.md: 🧩 UseCase 강제 / 📦 Feature 모델 Entity 이동 / 📏 함수 시그니처 멀티라인 규칙 추가 - .swiftformat 에 --disable redundantReturn 추가 — switch case 의 return 자동 제거 방지
AGENTS.md 의 `📏 함수 시그니처` 규칙(파라미터 ≥ 2 → 멀티 라인) 적용. 자동 일괄 변환이라 30+ 파일 단일 커밋 (커밋 단위 규칙 예외).
- 진입 시 perspectiveId 가 nil 이라 sendTapped 가 항상 막혀 댓글 작성 불가능했음 - CommentFeature.onAppear 의 merge 에 fetchMyPerspective 추가 - AsyncAction.fetchMyPerspective / InnerAction.myPerspectiveResponse / CancelID.fetchMyPerspective 신설 - 응답 성공 시 state.perspectiveId = perspective?.perspectiveId - 본인이 작성한 perspective 가 없으면 nil 유지 → 댓글 작성 비활성 (의도된 동작)
- Entity/Error/PerspectiveError.swift 신규 - LocalizedError + Equatable + .from(_:) 컨버터 - PerspectiveRepositoryImpl - fetchPerspective / deletePerspective 의 throw 를 PerspectiveError 로 교체 - 댓글 CRUD (fetchLabeled / create / update / deleteComment) 는 CommentError 유지 - PreVoteFeature.deleteMyPerspectiveResponse / mapError 를 PerspectiveError 로 - CommentReplyFeature.parentResponse / fetchParent mapError 를 PerspectiveError 로
Decode the createPerspective response as the lightweight creation payload returned by the server instead of requiring a full BattlePerspective object. Rejected: Make BattlePerspectiveDTO fields optional | that would weaken list/detail decoding and hide contract mismatches. #4
After the create endpoint confirms success, fetch my perspective to preserve the BattleInterface createPerspective return contract without decoding the create response as a full resource. Rejected: Change createPerspective to return Void | callers and interface semantics expect the created perspective value. #4
Add perspective mutation and like endpoints through Service, Repository, DomainInterface, and UseCase while moving battle perspective request/query shapes into explicit service models. Rejected: Keep default perspective repository inline in PerspectiveInterface | the expanded interface made the file noisy and harder to maintain. #4
Connect comment and reply screens to perspective creation, edit, delete, filtering, liking, menu sheets, skeleton states, and reply navigation using the expanded domain surface. Rejected: Continue using comment like endpoints for parent perspective cards | it updates the wrong backend resource. #4
Move share sheet models out of PreVoteFeature and build richer share payloads from battle text, tags, options, URL, and a rendered card snapshot. Rejected: Keep ShareItem nested in PreVoteFeature | it violates the reducer-only feature file rule and bloats the reducer surface. #4
Let users deselect or change the selected quiz choice instead of locking the first tapped answer. Rejected: Keep buttons disabled after first tap | it prevents correcting accidental taps before any commit action. #4
Add project guidance that keeps TCA feature files limited to reducer components and moves presentation helper models into module Model files. Rejected: Leave model extraction as an implicit convention | future reducers could reintroduce nested presentation types. #4
Show a compact 나 badge next to the author name for comments and replies backed by isMine state. Rejected: Replace the author name with 나 | preserving the nickname keeps the row identity clear while adding ownership context. #4
The pre-vote hero image needs to fade into the beige content area so the text-image boundary does not read as a hard middle line. Rejected: Move content lower below the image | it changes the composition and can leave the boundary visible. #4
PreVote needs a fixed presentation without user scrolling, and its screen-specific dimensions should live outside the view body for easier adjustment. Rejected: Keep constants as private static values in PreVoteView | it keeps the view noisy and ignores the requested file split. #4
Comment and reply avatars should crop consistently inside circular frames so remote images do not appear offset or underfilled. Rejected: Patch only CommentView | CommentReplyView renders the same avatar pattern and would keep the inconsistency. #4
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ad33c9588
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if perspective != nil { | ||
| state.customAlert = .alreadyWatched() |
There was a problem hiding this comment.
Do not delete an authored perspective to restart voting
When a returning user has previously written a perspective, fetchMyPerspective returns that authored comment and this branch presents the “watch again” alert; confirming it dispatches deleteMyPerspective, which calls DELETE /perspectives/{id}. Reopening a battle and choosing to watch again therefore permanently deletes the user's published perspective, even though a perspective is comment content rather than voting-progress state. Use an actual participation/vote-status signal instead of BattlePerspective and never delete user content as part of restarting playback.
Useful? React with 👍 / 👎.
| // 최종투표는 1회만 가능 — 이미 투표한 경우 서버가 500. | ||
| // 재투표가 불가하므로 결과(댓글) 화면으로 이동한다. | ||
| Log.error("[PreVoteFeature] submitPostVote failed: \(error.localizedDescription)") | ||
| return .send(.delegate(.alreadyFinalVoted(battleId: state.battleId))) |
There was a problem hiding this comment.
Preserve post-vote failures instead of assuming a prior vote
Every post-vote failure—including offline requests, authentication failures, timeouts, and unrelated server errors—is treated as if the user already completed the final vote and navigates to comments. In those scenarios the selected vote was never recorded, but the UI proceeds as though it succeeded and offers no retry. Only the specific backend response indicating an existing final vote should dispatch alreadyFinalVoted; other errors should remain on this screen.
Useful? React with 👍 / 👎.
| try await repository.fetchPerspectives( | ||
| battleId: battleId, | ||
| cursor: cursor, | ||
| size: 20, | ||
| optionId: optionId, | ||
| sort: sort |
There was a problem hiding this comment.
Add a path to fetch subsequent comment pages
The perspectives request is capped at 20 items, but every repo call site dispatches fetchPerspectives(reset: true) and there is no scroll/load-more action that dispatches reset: false. Although nextCursor and hasNext are saved, battles with more than 20 perspectives permanently hide every later comment from users. Trigger a non-reset fetch when the final visible item appears or otherwise expose pagination.
Useful? React with 👍 / 👎.
| try? await repository.deletePerspective(perspectiveId: perspectiveId) | ||
| await send(.delegate(.dismiss)) |
There was a problem hiding this comment.
Dismiss the reply screen only after deletion succeeds
When deleting the parent perspective fails due to connectivity, authorization, or a backend error, try? suppresses the failure and the delegate dismissal still runs. The screen closes as if deletion succeeded even though the perspective remains on the server, misleading the user and preventing any retry or error handling. Wrap the operation in a Result response and dismiss only on success.
Useful? React with 👍 / 👎.
|
{ |
There was a problem hiding this comment.
전반적으로 잘 구성된 PR입니다. 댓글/대댓글 CRUD, 큐레이팅 화면, 채팅방 오디오 오류 배너 기능이 잘 구현되었으며, Clean Architecture, TCA, SwiftUI, Swift 코드 품질 가이드라인을 잘 준수하고 있습니다. 특히 새로운 도메인 및 DTO 매핑, 에러 처리, UI 컴포넌트 분리 등 전반적인 아키텍처 관점에서 모범적인 패턴을 보여줍니다.
주요 개선점은 다음과 같습니다:
ChatCoordinator에서CommentReplyFeature로 진입 시perspectiveId가nil일 경우0으로 강제 할당하는 부분에 대한 명확한 예외 처리가 필요합니다.
| state.routes.push( | ||
| .commentReply( | ||
| .init( | ||
| perspectiveId: comment.perspectiveId ?? 0, |
There was a problem hiding this comment.
🟠 [P2] Major
comment.perspectiveId가 nil일 경우 0으로 대체하고 있습니다. CommentReplyFeature는 perspectiveId가 Int 타입이고, 대댓글 목록을 가져오는 핵심 식별자이므로 0이 유효한 ID가 아닐 경우 문제가 발생할 수 있습니다.
comment.perspectiveId가 nil이 될 수 없는 상황이라면 guard let으로 미리 바인딩하는 것이 명확하며, nil이 될 수 있다면 해당 경우에 대한 명확한 에러 처리 (예: 경고 표시 후 뒤로가기) 또는 CommentReplyFeature 진입 불가를 고려해야 합니다.
| perspectiveId: comment.perspectiveId ?? 0, | |
| guard let perspectiveId = comment.perspectiveId else { | |
| // perspectiveId가 없는 CommentItem은 대댓글 화면으로 진입할 수 없다고 가정하거나 | |
| // 사용자에게 알림 후 early return 처리 필요 | |
| Log.error("[ChatCoordinator] Attempted to open CommentReply for a CommentItem with nil perspectiveId") | |
| return .none | |
| } | |
| state.routes.push( | |
| .commentReply( | |
| .init( | |
| perspectiveId: perspectiveId, | |
| parentComment: comment | |
| ) | |
| ) | |
| ) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fdb84a9e9b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch { | ||
| Log.debug("[BattleRepositoryImpl] fetchMyPerspective failed (no participation): \(error.localizedDescription)") | ||
| return nil |
There was a problem hiding this comment.
Propagate non-absence errors when fetching my perspective
When this request fails because the device is offline, the token is expired, the server is unavailable, or decoding fails, the catch block converts the error into nil as though the user has no perspective. This lets the voting flow proceed with incorrect participation state, and after a successful createPerspective POST the follow-up failure is also reported as a failed creation even though the comment was already published. Only the specific response meaning “no perspective exists” should return nil; other failures should be thrown.
Useful? React with 👍 / 👎.
| state.isSubmitting = true | ||
| state.commentText = "" | ||
| if let editId = state.editingPerspectiveId { | ||
| state.editingPerspectiveId = nil | ||
| return .send(.async(.updatePerspective(perspectiveId: editId, content: text))) |
There was a problem hiding this comment.
Preserve the comment draft until submission succeeds
When creating a comment fails due to connectivity, authentication, or a server error, the text is cleared before the request and createCommentResponse(.failure) only logs the error, permanently discarding the user's draft. Editing has the same problem because the edit state and text are cleared before an update whose errors are suppressed. Keep the submitted text/edit context until success, or restore it on failure so the user can retry.
Useful? React with 👍 / 👎.
| let cursor = reset ? nil : state.nextCursor | ||
| return .run { [repository = perspectiveUseCase] send in | ||
| let result = await Result { | ||
| try await repository.fetchLabeledComments(perspectiveId: pid, cursor: cursor, size: 20) |
There was a problem hiding this comment.
Add a path to load subsequent reply pages
The reply request is explicitly capped at 20 items, but every call site dispatches fetchReplies(reset: true) and no view action ever requests reset: false. Although the reducer stores nextCursor and hasNext, perspectives with more than 20 replies permanently hide every later reply; trigger a non-reset fetch near the end of the visible list.
Useful? React with 👍 / 👎.
| case let .success(page): | ||
| state.battles = page.items |
There was a problem hiding this comment.
Retain recommendation pagination metadata
When the recommendations endpoint returns hasNext == true, this branch keeps only the first page's items and discards both nextCursor and hasNext; the service/interface also expose no cursor parameter or load-more action. As a result, the curation screen can never display recommendations beyond the server's first page. Store the pagination metadata and request subsequent pages as the user scrolls.
Useful? React with 👍 / 👎.
| let wasLiked = state.comments[index].isLiked | ||
| state.comments[index].isLiked.toggle() | ||
| state.comments[index].likeCount += state.comments[index].isLiked ? 1 : -1 |
There was a problem hiding this comment.
Roll back optimistic like changes after request failures
When liking or unliking a perspective fails because of connectivity, authorization, or a backend error, these optimistic mutations remain in state because likeResponse(.failure) only logs the error. The UI therefore shows a like state and count that were never recorded and stays incorrect until the list is reloaded. Restore the previous values on failure or defer the mutation until the request succeeds.
Useful? React with 👍 / 👎.
개요
댓글(관점) 기능 전반 + 큐레이팅 화면 + 채팅방 오디오 오류 처리.
picke.pen디자인 기준으로 구현했습니다.주요 변경
댓글 / 대댓글
MenuAction/commentRow) 통합, 터치 영역 확대큐레이팅 화면 (신규)
GET /battles/{id}/recommendations/interesting흥미 기반 추천 배틀 API 전 계층 연결>진입, 추천 배틀 카드 리스트 (pen 동일)채팅방 오디오 오류
AVPlayerItem.isPlayable) → 상단 floating 오류 배너 (FloatingErrorView, pen채팅방_오류)디자인 시스템
errorStrong토큰 추가, hex 직접 사용 → 토큰(neutral200/secondary200)으로 교체기타
Refs
Refs #4