Repository navigation
클래스 API와 Android·Figma 화면 정합성 반영 - #79
Conversation
서버 중간 투표 계약과 클래스 생성 요청 키를 반영하고, 투표 복구·결과 매핑·오디오 분기 테스트를 추가한다.
배틀과 AI 주제의 요청 키가 서로 충돌하지 않도록 실제 생성 대상 ID를 해시에 반영한다.
클래스 결과 화면과 오류 처리, 홈 Best 댓글 및 빈 상태를 Android와 Figma 기준에 맞춘다. Figma 추출 플러그인과 검증 기록을 추가하고 관련 빌드 및 테스트 결과를 남긴다.
- 사전·최종투표: 카테고리·댓글 수 배지, 선택지 카드 색·테두리·간격, 최종투표 배경과 위치를 Figma 기준으로 맞춤 - TTS 청취: 말풍선 색·여백·글꼴, 화자 이름 색, 진행바 두께·손잡이 크기를 시안대로 조정 - 의견: 상단 배경, 탭·정렬 칩, 카드 스타일과 작성 시각(N분 전) 표시를 반영 - 결과: 인사이트·참여 현황·입장 분포 카드 수치와 주요 의견 배지·인용 표시를 반영 - 문구는 Android 기준으로 통일(생각이 동일해요, 댓글 작성율, 의견을 남겨보세요...) - 작성 시각·인용 표시 테스트 추가, 의견 변화 문구 기대값 갱신
- 실제 클래스 경로 화면을 Figma 프레임 크기·1x로 렌더링하는 캡처 테스트 추가(PICKE_CLASS_VISUAL_AUDIT=1에서만 실행) - 실기기 하단 safe area를 재현해 하단 버튼 위치를 대조 - Figma 추출 플러그인에 클래스 탭 프레임(12378:23897) 추가
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
- PickeDesignKit: Figma 줄 높이(글자 크기 대비 배수)를 맞추는 pretendardFont(_:lineHeightMultiple:) 추가 - 사전·최종투표: 상단 여백 비율과 제목·설명·선택지 줄 높이 조정, 홈 인디케이터 기기에서 마감 문구만 Figma처럼 하단 영역에 걸치게 함(SE 계열은 기존 여백 유지) - TTS: 화자 이름·말풍선·재생 시간 줄 높이, 재생바와 컨트롤 사이 간격 제거 - 의견: 요약 열 폭·간격과 막대 위치, 탭 높이 44, 카드 글자 줄 높이, 입력창 높이·테두리·배경을 시안대로 조정(보내기 버튼 탭 영역 44 유지) - 결과: 섹션 제목 14pt, 인사이트·참여 현황·입장 분포·주요 의견 줄 높이와 행 높이 조정 - 캡처 테스트: 창 하단 safe area가 이미 있으면 추가하지 않도록 수정(375×812 iPhone 13 mini에서 Figma와 같은 조건으로 캡처) - 검증: Class 테스트 238/238 통과, 13 mini·17 Pro 캡처에서 주요 요소가 Figma와 ±3pt 이내 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
전반적으로 Figma 디자인 및 Android 화면 정합성 반영, 중간 투표 기능 추가, 네트워크 로그 보안 강화, 오류 처리 개선, 홈 화면 빈 상태 처리 등 다양한 개선 사항이 잘 반영되었습니다. 특히 민감 정보 로깅 방지, 의견 삭제 시 사용자 피드백 제공, 클래스 리포트 빈 참여율 계산 로직 보완, 인터랙티브 배틀 흐름 제어, 그리고 홈 화면의 빈 콘텐츠 상태 시 사용자 안내 등 핵심 사용자 경험과 안정성에 영향을 미치는 중요한 개선점들을 발견했습니다. 전반적으로 코드 품질과 사용자 경험이 향상되었습니다.
| headers | ||
| .sorted { $0.key < $1.key } | ||
| .map { " \($0.key): \($0.value)" } | ||
| .map { key, value in |
There was a problem hiding this comment.
🟠 [P2] Major
이전 버전에서는 네트워크 요청 로그에 Authorization, Cookie 등 민감한 정보가 포함된 헤더 값이 마스킹 없이 기록되었습니다. 이는 보안 및 개인정보 보호에 중요한 위험을 초래할 수 있습니다.
변경된 코드는 특정 민감 헤더를 <redacted> 처리하여 로그 노출 위험을 줄였습니다. 이 변경으로 인해 이전 버전에서 존재하던 P2 보안 취약점이 해결되었습니다.
| return .run { [repository = perspectiveUseCase] send in | ||
| try? await repository.deletePerspective(perspectiveId: perspectiveId) | ||
| await send(.delegate(.dismiss)) | ||
| let result = await Result { |
There was a problem hiding this comment.
🟠 [P2] Major
이전에는 의견 삭제 시 API 호출 실패가 발생하면 사용자에게 아무런 피드백 없이 오류가 무시되었습니다. 이로 인해 사용자는 삭제 요청의 실패 여부를 알 수 없었으며, UI 상태가 실제와 불일치할 가능성이 있었습니다.
현재 변경은 삭제 작업의 결과를 명시적으로 처리하고, 실패 시 PerspectiveError로 매핑하여 사용자에게 오류 알림을 표시하도록 개선했습니다. 이는 핵심 사용자 경험 흐름의 일관성과 신뢰성을 높입니다.
| public var serverParticipationRate: String { | ||
| guard let rate = classResult?.stats.participationRate else { return "" } | ||
| return rate.formatted(.number.precision(.fractionLength(0...1))) + "%" | ||
| guard let stats = classResult?.stats else { return "" } |
There was a problem hiding this comment.
🟠 [P2] Major
이전 버전의 클래스 참여 현황 계산 로직(serverParticipationRate, serverCommentRate)은 활성 멤버 수가 0일 때 0% 또는 NaN%와 같은 잘못된 값을 표시하거나, 잠재적으로 런타임 오류(0으로 나누기)를 발생시킬 수 있었습니다. 이는 사용자에게 부정확한 정보를 제공합니다.
변경 코드는 활성 멤버가 없을 때 "아직 참여 없음"으로 표시하고, 계산에 0으로 나누기 오류가 발생하지 않도록 하여 데이터 표현의 정확성과 견고성을 향상했습니다.
| .cancellable(id: CancelID.midVote(state.instanceID), cancelInFlight: true) | ||
| case .playbackTapped: | ||
| guard state.isAudioStep, state.isAudioReady else { return .none } | ||
| guard state.isAudioStep, state.isAudioReady, |
There was a problem hiding this comment.
🟠 [P2] Major
이전 버전에서는 사용자가 중간 투표 선택이 필요한 시점에도 오디오 재생/정지, 탐색(seek), 다시 듣기 등의 컨트롤을 조작할 수 있었습니다. 이는 인터랙티브 배틀의 흐름을 방해하고, 필수적인 선택 지점을 건너뛸 수 있는 문제를 발생시켰습니다.
변경된 코드는 pendingCheckpointID가 존재할 때 오디오 컨트롤러를 비활성화하고, 탐색 범위도 현재 노드로 제한하여 사용자가 먼저 중간 투표를 완료하도록 강제함으로써 인터랙티브 스토리텔링의 의도를 보장합니다.
| ScrollView(showsIndicators: false) { | ||
| if store.shouldShowSkeleton { | ||
| HomeSkeletonView() | ||
| } else if !store.hasVisibleHomeContent { |
There was a problem hiding this comment.
🟠 [P2] Major
이전 버전의 홈 화면은 표시할 콘텐츠가 없을 때 빈 화면을 그대로 노출하여 사용자에게 혼란을 줄 수 있었습니다. 사용자는 앱에 문제가 발생했는지, 혹은 단순히 콘텐츠가 없는 것인지 명확히 알 수 없었습니다.
변경된 코드는 hasVisibleHomeContent 상태를 통해 홈 콘텐츠가 없을 때 전용 emptyHomeSection() 뷰를 표시하도록 개선했습니다. 이는 사용자 경험을 명확히 하고, 앱의 상태를 직관적으로 전달합니다.
- Android develop(b127754)과 같이 GET classes/{classId}/result/my-activity(cursor) 추가
- 받은 추천 수·댓글 수·내가 남긴 댓글(커서 페이지) 엔티티와 DTO 매핑 추가(댓글·의견 ID 없는 항목 제외)
- ClassResultInterface에 fetchMyActivity를 추가하고 저장소·유스케이스·Mock·테스트 대역 갱신
- 경로·메서드·커서 쿼리와 응답 매핑 계약 테스트 추가
Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- Android ReportPerspectiveUseCase와 같이 REPORT_409 응답을 PerspectiveError.alreadyReported로 변환 - 그 밖의 오류는 기존 네트워크 오류 그대로 전달(일반 배틀은 try? 호출이라 동작 변화 없음) - 중복 코드와 다른 오류 코드 저장소 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 리포트 내 투표 결과 활동 행을 Android와 같이 댓글·대댓글·받은 추천 3열로 변경(받은 추천은 내 활동 API, 모르면 —) - 내 활동 요청은 요청 ID로 늦은 응답을 거르고 재조회·뒤로·닫기·라우트 제거 시 취소 - 내가 남긴 의견: 공개된 내 의견은 남긴 댓글, 내 활동 댓글은 남긴 대댓글(원 의견 제목·내 이름의 답글)로 표시 - 의견과 대댓글을 따로 보관해 응답 순서와 무관하게 유지하고, 목록 끝에서 커서로 다음 대댓글을 이어 붙임(첫 페이지는 교체) - 내 활동·멤버 조회 실패는 의견을 유지한 채 "정보를 불러오지 못했어요" 토스트 - 리포트·내 의견 리듀서 테스트 추가 및 서버 의견 행 기대값 갱신 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- Android PerspectiveItemCard와 같이 남의 의견은 신고, 내 공개 의견은 삭제·수정, 거절된 의견은 수정만, 검수 중은 메뉴 없음 - 공개되지 않은 내 의견(검수중·거절됨)을 목록 위 상태 카드로 표시 - 수정은 입력창에 내용을 채워 PATCH, 삭제·신고는 확인 알림 후 실행하고 목록과 내 의견을 다시 조회 - 삭제 후 같은 화면에서 새 의견을 다시 쓸 수 있게 하고, 신고 결과(접수·중복·실패)는 토스트로 안내 - 의견 변경 요청은 상세 화면 이동에서는 유지하고 라우트 제거 시에만 취소 - 메뉴 종류·수정·취소·삭제·신고·상태 카드·화면 이탈 후 응답 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 완료 단계 진입마다 내 의견을 다시 받아 제출 직후에도 검수중·거절됨 카드와 거절 의견 수정이 보이게 함 - 수정·삭제·신고 요청이 진행 중이면 다른 메뉴 동작을 막아 같은 취소 ID로 앞 요청이 취소되지 않게 함 - 다음 대댓글 페이지 실패 시 더 불러오기 중단(무한 로딩·반복 토스트 방지) - 상태 카드가 있으면 빈 상태 문구를 숨기고 검수 중·거절 의견은 상세로 이동하지 않음 - 쓰이지 않는 메뉴 닫기 액션 제거, 수정 중 같은 의견을 다시 눌러도 입력 유지 - 메뉴 버튼은 공용 PickeActionPill과 기존 신고 아이콘 재사용 - 화면 이탈 뒤 실제 신고 effect 응답·변경 중 차단·재조회·페이지 실패 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
클래스 중간 투표 API 및 화면 정합성 반영, 의견 관리 기능 추가, 홈 화면 Best 댓글 섹션 도입 등 광범위한 기능 개선 및 UI/UX 정합성 개선이 이루어졌습니다. 전반적으로 코드 품질이 높고, 새로운 기능에 대한 테스트 커버리지도 충실하게 추가되었습니다. 일부 데이터 매핑 시 유효하지 않은 서버 응답을 필터링하는 정책이 적용되었으며, 이는 의도된 동작으로 보입니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Feature/Class/Sources/ClassMyOpinions/Reducer/ClassMyOpinionsFeature.swift:214 [미검증] 🟠 [P2] Major 기존
ClassReportOpinion의kind가.unspecified였던 의견들이 이제.comment로 매핑됩니다. 이는 Android의 "남긴 댓글" 섹션에서 검수 중/거절된 의견을 제외하고 공개된 의견만 보여주는 방식과 일치합니다. 기존의 명시되지 않은 의견 처리 방식이 변경되었으나, Android 정합성을 위한 의도적인 변경으로 보입니다. - Projects/Feature/Class/Sources/ClassBattleFlow/Reducer/ClassBattleFlowFeature.swift:502 [미검증] 🟠 [P2] Major
CommentReplyFeature에서deleteParent액션에.cancellable(id: CancelID.mutate, cancelInFlight: false)가 추가되었습니다.cancelInFlight: false는 다른mutate액션이 발생해도 삭제 요청이 취소되지 않도록 합니다. 이는 삭제 요청이 한 번 시작되면 완료되어야 하는 특성 때문에 의도된 것으로
| func toDomain() -> ScenarioInteractiveOption { | ||
| ScenarioInteractiveOption(label: label, nextNodeId: nextNodeId) | ||
| func toDomain() -> ScenarioInteractiveOption? { | ||
| guard let nextNodeId else { return nil } |
There was a problem hiding this comment.
🟠 [P2] Major
ScenarioInteractiveOptionDTO 매핑 시 nextNodeId가 nil인 경우 해당 선택지를 완전히 버리도록 변경되었습니다. 서버에서 nextNodeId 없이 유효한 인터랙티브 옵션이 전달될 가능성은 낮지만, 이러한 하드 필터링은 향후 서버 계약 변경이나 불완전한 데이터에 대해 민감하게 반응할 수 있습니다. 현재 PR의 interactiveOptionFallsBackToTitleAndSkipsMissingNextNode 테스트에서 이 동작을 명시적으로 검증하고 있으므로, 의도된 디자인으로 보입니다.
| extension ClassMyActivityDTO.Comment { | ||
| /// Android와 같이 댓글·의견 ID가 없는 항목은 버린다. | ||
| func toDomain() -> ClassMyActivityComment? { | ||
| guard let commentId, let perspectiveId else { return nil } |
There was a problem hiding this comment.
🟠 [P2] Major
ClassMyActivityDTO.Comment 매핑 시 commentId 또는 perspectiveId가 nil인 경우 해당 댓글을 버리도록 변경되었습니다. BattleScenarioDataDTO+와 유사하게, 핵심 ID가 없는 데이터를 처리하지 않는 것은 데이터 무결성을 유지하는 데 도움이 되지만, 서버가 부분적인 유효 데이터를 전송할 경우 사용자에게 보이지 않을 수 있습니다. 테스트를 통해 이 동작이 의도되었음이 확인됩니다.
- 서버가 참여 완료로 판정한 뒤에는 /perspectives/me가 비어도 입력창을 다시 열지 않음(중복 의견 방지) - 내 의견 조회 실패는 판정 전에만 재시도 문구를 띄우고, 판정 뒤에는 목록 오류 문구를 덮지 않음 - 내 의견 상세에서 돌아오면 판정을 비우고 /me 결과로 다시 판정(상세에서 삭제 후 재작성 가능), 플래그는 의견을 누를 때마다 대입 - 의견 필수·참여 완료, 판정 전후 조회 실패, 내 의견·남의 의견 상세 복귀, 변경 중 알림 확인 테스트 추가 - 내 의견 응답 테스트의 고정 UUID를 상태의 요청 ID로 대체 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 참여한 운영자의 개인 결과가 있으면 내 활동 API로 받은 추천 수를 함께 조회 - 활동 행을 Android 결과 화면과 같은 댓글·대댓글·받은 추천으로 표시(모르면 —) - 다시 불러오기·뒤로 가기·라우트 제거 시 내 활동 요청을 취소하고 값 초기화 - 받은 추천 성공·실패 표시 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 라우트에 내가 남긴 의견 화면이 없으면 코디네이터가 조회·대댓글·다음 페이지 요청을 취소(스와이프 닫기 포함) - 화면은 항상 최상단 하나뿐이라 상태 플래그 없이 라우트 존재 여부로 판단 - 테스트 대역의 내 활동 조회를 멈춰 둘 수 있게 바꾸고 스와이프 제거 시 취소 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- TestStore 밖에서 만들던 클래스 상세 상태를 withDependencies(uuid incrementing) 안에서 생성 - 10/25부터 실패하던 AI 생성 취소 테스트 3건이 실기기에서 통과 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
| extension ClassMyActivityDTO.Comment { | ||
| /// Android와 같이 댓글·의견 ID가 없는 항목은 버린다. | ||
| func toDomain() -> ClassMyActivityComment? { | ||
| guard let commentId, let perspectiveId else { return nil } |
There was a problem hiding this comment.
🟠 [P2] Major
ClassMyActivityDTO.Comment를 도메인 엔티티로 매핑하는 과정에서 commentId 또는 perspectiveId가 nil인 경우 해당 댓글을 nil로 반환하여 목록에서 제외합니다. PR 설명에 "Android와 같이 댓글·의견 ID가 없는 항목은 버린다"고 명시되어 있어 Android와의 정합성을 맞춘 것으로 보입니다. 그러나 서버가 유효한 댓글임에도 불구하고 이러한 ID를 nil로 반환하는 경우가 있다면, 사용자가 자신의 댓글을 확인하지 못하는 데이터 누락으로 이어질 수 있습니다. 서버 응답이 항상 commentId와 perspectiveId를 포함하도록 보장되거나, nil인 경우에도 사용자에게 필요한 정보를 표시할 수 있는 대체 처리가 필요한지 제품팀과 확인해 보는 것이 좋습니다.
- 스와이프 등으로 리포트 라우트가 제거될 때 투표 기록·인사이트 요청까지 취소(뒤로 가기와 같은 5종) - 테스트 대역의 인사이트 조회를 멈춰 둘 수 있게 바꾸고 라우트 제거 시 인사이트 취소 테스트 추가(수정 전 실패 확인) - 운영자 대시보드 라우트 제거 시 내 활동 요청 취소 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 개인 피드백을 받지 못하면(FAILED 등) 개수를 모르므로 '내가 남긴 의견 보기'로 표시(실기기에서 의견 1건인데 0개로 보이던 문제) - 뒤로 가기·다시 불러오기 시 내 활동 요청 취소, 받은 추천 초기화, 이전 응답 무시 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 목록과 내 의견 조회가 모두 실패한 뒤 다시 시도하면 오류 문구만 사라져 빈 목록처럼 보이던 문제 수정 - 목록이 비어 있고 진행 중인 목록 요청이 없을 때만 함께 다시 불러오고, 목록이 있으면 유지 - 목록 재조회·목록 유지 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
네트워크 로깅 민감 정보 가림, 클래스 생성 요청 키로 중복 방지, 중간 투표 및 의견 관리 기능 추가, 리포트 수치 계산 오류 수정, Figma 디자인 정합성을 위한 폰트 줄 높이 조정 등 전반적으로 견고한 구현과 테스트가 이루어졌습니다. 특히 네트워크 보안 및 데이터 일관성, 사용자 피드백 관련 개선이 돋보입니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Domain/ClassDomain/Sources/Repository/PerspectiveRepositoryImpl.swift:131 [미검증] 🟠 [P2] Major 의견 신고 시 서버 응답 코드
REPORT_409를PerspectiveError.alreadyReported로 명확하게 매핑하여 처리합니다. 이를 통해 이미 신고된 의견에 대해 사용자에게 정확한 오류 메시지를 제공하고, 중복 신고로 인한 불필요한 서버 요청 및 상태 혼란을 방지할 수 있습니다. - Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift:169 [미검증] 🟠 [P2] Major 클래스 리포트에서 입장 분포 비율(
serverDistributionOptionRatioTexts)을 계산할 때 부동 소수점 오차로 인해 총합이 100%가 되지 않을 수 있는 문제를 수정했습니다. 마지막 옵션의 비율을 남은 값으로 할당하여 모든 옵션의 비율 합계가 항상 100%가 되도록 보장함으로써, 사용자에게 표시되는 데이터의 정확성을 높였습니다. 이는 데이터 일관성과 사용자 신뢰도에 영향을 - Projects/UI/PickeDesignKit/Sources/Token/Typography/PretendardFont.swift:62 [미검증] 🟠 [P2] Major Figma 디자인 시스템의 줄 높이(lineHeight)를 iOS에서 정확히 반영하기 위해
lineSpacing과padding(.vertical)을 활용하는 새로운 폰트 모디파이어를 추가했습니다. 이는 UI/UX 일관성을 저해하던 시각적 불일치 문제를 해결하여, 앱의 전반적인 디자인 품질을 향상하는 중요한 개선 사항입니다.
| let method = urlRequest?.httpMethod ?? "?" | ||
| let url = urlRequest?.url?.absoluteString ?? "?" | ||
| // Query values can contain invite or ticket codes; keep only the route in logs. | ||
| let url = urlRequest?.url?.path ?? "?" |
There was a problem hiding this comment.
🟠 [P2] Major
URL 로깅 시 쿼리 파라미터를 제거하고 경로만 기록하도록 변경되었습니다. 이는 API 키, 초대 코드, 이용권 코드 등 민감한 정보가 로그에 노출되는 것을 방지하는 중요한 보안 개선 사항입니다. 이전 구현은 민감 정보를 노출할 수 있는 취약점이 있었습니다.
| .map { key, value in | ||
| let shouldRedact = ["authorization", "cookie", "set-cookie"] | ||
| .contains(key.lowercased()) || key.lowercased().contains("token") | ||
| return " \(key): \(shouldRedact ? "<redacted>" : value)" |
There was a problem hiding this comment.
🟠 [P2] Major
요청 및 응답 헤더에 민감 정보(인증, 쿠키, 토큰 등)가 포함된 경우 해당 값을 "<redacted>"로 대체하도록 변경되었습니다. 이는 인증 정보가 실수로 로그에 기록되어 보안 사고로 이어지는 것을 방지하는 중요한 개선 사항입니다.
- Android ClassTicketStatus.from과 같이 알 수 없는 이용권 상태값을 잘못된 응답 대신 만료로 매핑 - 상태값 매핑 테스트 갱신 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- notice(취소 왼쪽·확인 오른쪽, 취소 문구가 비면 확인 1개)와 noticeReversed(확인 왼쪽·취소 오른쪽) 스타일 추가 - 배경 탭이 취소라서 "안내만 닫기"가 항상 취소가 되도록 배치를 나눔 - Android와 같은 클래스 이용권 안내 7종 팩토리와 단일 버튼 행 헬퍼 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 새 클래스 만들기 전에 내 이용권을 확인: 사용 중이면 생성, 만료면 등록 안내(등록하기 → 마이 탭 코드 입력), 없으면 이용권 필요 안내, 조회 실패는 토스트 - 확인 중 다른 이동이나 늦은 응답이 화면을 쌓지 않도록 확인 취소와 만들기 루트 가드 추가 - 이용권 화면: 만료·없음이면 코드 입력부터, 진입 조회 실패는 토스트 후 닫기, 내가 쓰는 코드 재입력은 기존 개요로 이동(만료면 만료 안내) - 코드 확인·등록 오류를 입력창 아래 글자 대신 Android 안내창으로 표시(사용 중 이용권 409만 iOS에서 따로 안내) - 확인 단계 뒤로가기는 코드 입력으로, 코드는 제출 시 ASCII 영문·숫자·하이픈 대문자로 정규화 - 확인 단계·FAQ·개요 문구를 Android와 같게, 안내 중 탭바 숨김, 이용권 라우트 제거 시 진입 조회 취소 - 기존에 실패하던 이용권 App 테스트 3건을 현재 등록 흐름으로 수정하고 상태별·코디네이터 테스트 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
전반적으로 Figma 화면 정합성 반영, 중간 투표 및 내 활동 API 연동, 오류 처리 강화, 개인 정보 보호를 위한 네트워크 로그 수정 등 다양한 개선 사항이 포함된 PR입니다. 특히 TCA 테스트 강화 및 의존성 주입 개선이 돋보입니다. 몇 가지 주요 동작 흐름과 데이터 처리의 잠재적 버그 및 UX 개선 사항을 발견했습니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift:95 [미검증] 🟠 [P2] Major 경로: 클래스 참여 통계에서
activeMemberCount가 0일 때 비율 계산 원인:activeMemberCount가 0일 때participationRate및commentRate계산 시 0으로 나누는 문제가 발생할 수 있었고, 이는 런타임 오류나 부정확한 값을 초래할 수 있었습니다. 수정 방향:activeMemberCount가 0인 경우를 명
| return loadProgress(state: &state) | ||
| case let .failure(error): | ||
| state.errorMessage = error.message | ||
| if submittedStep == .postVote, error == .midVoteRequired { |
There was a problem hiding this comment.
🔴 [P1] Critical
경로: 사후 투표 시 중간 투표 누락 오류 발생
원인: midVoteRequired 오류 발생 시 needsMidVoteRecovery 플래그를 설정하고 오디오 재생을 재개하여 사용자가 중간 투표를 완료할 수 있도록 합니다. 이 로직이 없으면 사용자는 사후 투표를 제출할 수 없게 되어 진행이 막힙니다.
수정 방향: 중간 투표가 필수인 시나리오에서 사용자가 막히지 않도록 midVoteRequired 오류 처리 로직을 추가하여 핵심 사용자 흐름을 유지해야 합니다.
| return !classState.showsTabBar | ||
| case .myPage: | ||
| if case .ticket? = myPageState.routes.last?.screen { return false } | ||
| if case let .ticket(ticket)? = myPageState.routes.last?.screen { return ticket.customAlert != nil } |
There was a problem hiding this comment.
🟠 [P2] Major
경로: 마이 탭에서 이용권 화면에 진입 시 탭바 표시
원인: 이전 코드(return false)는 이용권 화면이 루트에 있으면 항상 탭바를 숨겼습니다. 변경된 코드(return ticket.customAlert != nil)는 이용권 화면에 customAlert이 있을 때만 탭바를 숨기므로, 일반적인 이용권 등록/개요 화면에서는 탭바가 계속 표시됩니다.
수정 방향: 이용권 화면에서 customAlert이 없는 경우에도 탭바가 숨겨져야 하는지, 혹은 현재 의도대로 customAlert이 있는 경우에만 숨기는 것이 맞는지 디자인/기획 의도를 재확인하여 탭바 표시 동작을 일관성 있게 맞춰야 합니다.
|
|
||
| extension ClassTicketDTO { | ||
| func toDomain() throws -> ClassTicket { | ||
| // Android `ClassTicketStatus.from`과 같이 모르는 상태값은 만료로 본다. |
There was a problem hiding this comment.
🟠 [P2] Major
경로: 서버에서 알 수 없는 이용권 상태 값이 내려오는 경우
원인: 이전에는 서버에서 알 수 없는 status 값이 오면 ClassTicketError.invalidResponse 오류를 발생시켜 전체 흐름이 중단되었습니다. 이제는 알 수 없는 상태를 .expired로 기본 처리하여 앱이 오류로 중단되지 않고 다음 단계로 진행되도록 변경되었습니다.
수정 방향: 알 수 없는 상태를 .expired로 처리하는 것이 비즈니스 로직 및 사용자 경험상 적절한지 재검토가 필요합니다. 의도된 동작이라면 변경은 합리적입니다. 만약 다른 처리가 필요하다면, 해당 케이스에 대한 명시적 오류 처리나 대체 상태 정의를 고려해야 합니다.
| PerspectiveService.reportPerspective(perspectiveId: perspectiveId), | ||
| as: PickeEmptyResponse.self | ||
| ) | ||
| do { |
There was a problem hiding this comment.
🟠 [P2] Major
경로: 이미 신고한 의견을 다시 신고하려고 할 때
원인: 이전에는 REPORT_409와 같은 특정 서버 오류 코드를 PerspectiveError.alreadyReported로 매핑하지 않고 일반적인 PickeNetworkError로 처리했습니다. 이로 인해 사용자에게 이미 신고했다는 정확한 피드백을 제공하지 못하고 일반적인 네트워크 오류 메시지를 보여줄 수 있었습니다.
수정 방향: 서버 오류 코드를 특정 PerspectiveError로 매핑하여 사용자가 현재 상황을 명확히 인지하고 적절한 다음 행동을 할 수 있도록 정확한 오류 메시지를 제공해야 합니다. 현재 변경은 이 문제를 해결합니다.
- Android ic_reply_corner 를 replyCorner imageset(SVG)으로 추가 - ImageAsset 에 replyCorner 케이스 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- ContentSection 에 bestComment("best_comment") 케이스 추가
- 크로스 플랫폼 분석 스펙 문서의 content_action section 목록에 best_comment 추가 (Android 와 동일)
Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- Best 배틀: 최대 3개만 노출, 순위 24 SemiBold(1위 갈색·나머지 연회색), X VS Y 배지, 제목 1줄, 행마다 구분선 - Best 댓글: 제목 13 SemiBold 1줄, ㄴ자 답글 아이콘, 둥근 선택지 배지를 본문 첫 줄 가운데 높이에 inline 으로 붙임 - SwiftUI 가 이미지 줄 위쪽을 이미지 높이로 잡는 만큼 본문을 위로 당겨 아이콘과 배지 위치를 맞춤 - 섹션 헤더 "더 보기" 제거(moveToExplore·homeMore 분석 이벤트 삭제), 헤더 18 SemiBold, 섹션 간격 Android 수치로 정리, 광고 항상 노출 - 첫 로딩 실패 시 스켈레톤 대신 빈 상태(.failed), 다시 들어오면 재요청 - 오늘의 Pické 잔재(퀴즈·투표 상태, QuizCardView·VoteCardView) 삭제 - 홈 루트에서 홈 탭 재선택 시 맨 위로 스크롤 후 다시 불러오기(로딩 중이면 스크롤만) - 테스트: Best 3개 제한, Best 댓글 탭 분석, 실패 후 재진입, 재선택(로딩 중 포함), MainTab 재선택 재조회 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
네트워크 로그에서 URL 쿼리 파라미터가 불필요하게 전부 삭제되어 디버깅에 방해가 될 수 있습니다. 클래스 리포트 화면에서 참여율 계산 로직이 0명일 때를 처리하지 못하여 잘못된 값을 표시하거나 오류를 유발할 수 있는 부분이 수정되었습니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift:95 [미검증] 🟠 [P2] Major 클래스 리포트 화면에서
activeMemberCount또는postVoteCount가 0일 경우, 참여율 및 비율 계산 시 0으로 나누기 오류가 발생하거나 "0%"와 같이 오해의 소지가 있는 값이 표시될 수 있었습니다. 이 변경으로 0명 참여 시 "아직 참여 없음"과 같이 적절한 문구를 표시하고, 진행률을 0으로 설정하여 UI 안정성과 사용자 경험이 개선되었습니다. 관련해서 `LINE
| let method = urlRequest?.httpMethod ?? "?" | ||
| let url = urlRequest?.url?.absoluteString ?? "?" | ||
| // Query values can contain invite or ticket codes; keep only the route in logs. | ||
| let url = urlRequest?.url?.path ?? "?" |
There was a problem hiding this comment.
🟠 [P2] Major
URL 로깅 시 absoluteString 대신 path를 사용하여 쿼리 파라미터 전체를 삭제했습니다. PR 설명에서 특정 초대/이용권 코드와 같은 민감 정보를 가리는 것이 목적이라고 하였으나, path는 민감 정보 여부와 관계없이 모든 쿼리 파라미터를 제거합니다. 이는 디버깅 시 유용한 비민감성 쿼리 파라미터 정보까지 유실시켜 문제 분석에 어려움을 줄 수 있습니다. 민감 정보만 선택적으로 가리거나 해싱하는 방식이 더 적절합니다.
- 본문에는 배지 폭 × 높이 1 투명 자리만 두고, 실제 배지는 firstTextBaseline overlay 로 대문자 높이 중앙에 맞춤 - 첫 줄 아래는 배지가 본문 줄 아래 몫보다 더 내려간 만큼(약 2pt)만 늘림(Android placeholder 와 같은 방식) - 이전 inline 이미지 방식은 줄 위·아래가 배지 높이만큼 늘어 행이 Figma보다 약 5pt 길었음 → 98.0pt(Figma 99.0) Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- onAppear 뒤 store.finish() 로 남은 fetch effect 를 기다린 다음 결과 상태를 확인(타이밍 의존 제거) Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
전반적으로 잘 구성된 PR입니다. 중간 투표, 이용권 관련 기능 확장 및 홈 화면 개편이 안정적으로 구현되었고, 테스트 커버리지도 훌륭합니다. Figma 화면 정합성을 위한 UI 개선 및 네트워크 로그의 민감 정보 가림 처리 등 UX와 보안 개선 노력도 돋보입니다. 몇 가지 주요 동작 흐름과 에러 처리 방식에 대한 피드백을 드립니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Feature/Class/Sources/ClassTicket/Reducer/ClassTicketFeature.swift:38 [미검증] 🟠 [P2] Major 이용권 코드를 대문자로 정규화하고 유효한 문자(영문, 숫자, 하이픈)만 남기도록
normalizedCode를 추가한 것은 입력값 처리의 견고성을 크게 향상시킵니다. 이를 통해 불필요한 공백이나 특수 문자로 인한 오류를 줄이고, 서버와의 데이터 일관성을 유지할 수 있습니다. (관련: LINE 48, 182, 213, Projects/Feature/Class/Tests/Sources/ClassT
| } | ||
| if let body = urlRequest?.httpBody, !body.isEmpty { | ||
| lines.append(" · Request Body\n\(indent(prettyBody(body)))") | ||
| lines.append(" · Request Body\n\(isSensitiveRoute ? " <redacted>" : indent(prettyBody(body)))") |
There was a problem hiding this comment.
🟠 [P2] Major
민감한 라우트의 요청/응답 본문 및 헤더(LINE 65, 125-128)를 로그에서 가리는 처리는 보안 강화를 위한 중요한 개선 사항입니다. isSensitiveRoute 판단 기준과 shouldRedact 로직이 적절하게 추가되어 의도된 대로 민감 정보가 로그에 남지 않도록 합니다.
| public extension ScenarioInteractiveOptionDTO { | ||
| func toDomain() -> ScenarioInteractiveOption { | ||
| ScenarioInteractiveOption(label: label, nextNodeId: nextNodeId) | ||
| func toDomain() -> ScenarioInteractiveOption? { |
There was a problem hiding this comment.
🟠 [P2] Major
ScenarioInteractiveOptionDTO의 toDomain() 매핑 로직이 nextNodeId가 nil일 경우 nil을 반환하도록 변경된 것은, 불안정한 API 응답에 대한 견고성을 높이는 좋은 접근입니다. compactMap을 사용하여 유효하지 않은 선택지를 필터링하는 방식 또한 DTO-Entity 간의 데이터 정합성을 유지하는 데 기여합니다. (관련: LINE 53)
| return loadProgress(state: &state) | ||
| case let .failure(error): | ||
| state.errorMessage = error.message | ||
| if submittedStep == .postVote, error == .midVoteRequired { |
There was a problem hiding this comment.
🟠 [P2] Major
사후 투표 제출 시 midVoteRequired 에러가 발생했을 때 중간 투표 복구 흐름(needsMidVoteRecovery = true, 오디오 재생 재시작 등)을 추가한 것은, 사용자 경험 측면에서 매우 중요합니다. 누락된 중간 투표를 완료하도록 유도하여 배틀 참여의 완전성을 보장합니다.
| case .requestKeyConflict: return "클래스 생성 요청이 충돌했어요. 입력 내용을 확인하고 다시 시도해 주세요." | ||
| case let .network(message): return message | ||
| case let .userMessage(message): return message | ||
| case .unknown: return "클래스 요청을 처리하지 못했습니다. 다시 시도해 주세요." |
There was a problem hiding this comment.
🟠 [P2] Major
.unknown 에러의 메시지를 사용자에게 친숙하고 추상적인 문구로 변경한 것은 좋은 사용자 경험 개선입니다. .userMessage(String) 케이스를 추가하여 개발자가 의도적으로 사용자에게 표시할 메시지를 직접 전달할 수 있도록 한 점도 효과적인 에러 처리 전략입니다. (관련: LINE 35, 57)
- Android ic_alarm(20×20, 원본색)·ic_clock(16×16)·ic_eye(13×9) 벡터를 SVG imageset 으로 추가, 시계·눈은 template - ImageAsset 에 alarm·clock·eye 케이스 추가 - appLogo 에 벡터 보존을 켜 빈 화면에서 160pt로 키워도 흐려지지 않게 함(약관 화면·Hifi 헤더에도 같이 적용, 원래 크기에서는 모양 변화 없음) Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- pretendardFont(_:lineSpacingMultiple:) 추가: 줄 사이만 (크기 × 배수 − 자연 줄높이) 벌리고 첫 줄 위·마지막 줄 아래는 늘리지 않음 - Android Compose 1.7 기본(includeFontPadding = false, LineHeightStyle.Default = Trim.Both)과 같은 높이: 한 줄은 자연 높이, n줄은 자연 높이 + (n−1) × 줄높이 - 기존 pretendardFont(_:lineHeightMultiple:)(Figma 기준)는 그대로 둠 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 홈 탭 재선택은 맨 위로만 이동하고 다시 불러오지 않음(Android ac84ca0 에서 재조회 삭제) - 당겨서 새로고침 추가: 받아 둔 화면 위에서 다시 조회하고 스켈레톤은 띄우지 않음, 빈 화면에서도 당길 수 있게 세로 튕김 허용 - 홈 조회 실패 시 "정보를 불러오지 못했어요" 토스트(첫 조회·새로고침 모두) - 홈 조회는 첫 진입 한 번 + 새로고침만, 실패 후 재진입해도 다시 부르지 않음(Android ViewModel init 과 같음) - 미읽음 조회 결과를 Result 로 받아 실패해도 종 로딩 상태를 내림(isUnreadBadgeLoading, shouldShowBellSkeleton) - HomeTests 새 동작 기준으로 수정·추가(12개), MainTab 재선택 테스트는 재조회 없음 단언으로 변경 - 감사 호스트·감사 테스트 홈 상태에 종 로딩 해제 반영 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 상단 바: 높이 48, 배경 backgroundBeige, 구분선 없음, 로고 63×40, Android ic_alarm 20pt + Material3 소형 배지 위치, 첫 로딩·미읽음 조회 전 24pt shimmer 원 - EDITOR PICK: 검정 배경, 위 배지·쪽 번호 줄 고정, 이미지 폭 ÷ 1.8 + 검정 30% 덮개, 선택지 16 SemiBold 흰색 그림자, 제목 1줄·요약 2줄 최소 높이·태그·조회수, 0.6초 FastOutSlowIn 자동 넘김 - 지금 뜨는 배틀 카드: 1.2 비율 썸네일(radius 4, 안쪽 테두리 4), 태그 배지(없으면 #이슈), 제목 1줄, 시계·눈 12pt 메타 → 266.3pt - 새로운 배틀 카드: 태그 배지 + 메타, 제목·요약 줄 간격 1.4, BattleOpinionBox·ProfileImage(40 원, 2/3 이미지)·28pt VS - 메타 라벨: SF Symbol 대신 Android ic_clock·ic_eye, 아이콘 크기·간격·색을 카드별로 받음 - 빈 화면: 로고를 borderBeigeDefault 색으로, 스크롤 영역 가운데 정렬 - 390pt 캡처 측정: 상단 바 48.0, EDITOR PICK 376.3(계산 376.0), 지금 뜨는 배틀 카드 266.3, 새로운 배틀 카드 155.0 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- Android Compose 1.7 기본(Trim.Both)은 한 줄 글자를 글꼴 자연 높이로 그림 → 1차의 Figma 기준 위아래 줄높이 여백을 걷어냄 - 섹션 헤더: 세로 패딩 12 고정(45.5pt), Best 배틀 제목·태그·메타: 자연 높이, 시계·눈은 Android ic_clock·ic_eye - Best 댓글: 제목 자연 높이, 본문은 줄 간격만 13 × 1.4, 배지 높이 12 Medium 자연 높이 + 4(18.3pt, 1차 20.8pt는 Figma 값) - 배지 가운데를 본문 글꼴 (ascent − descent) / 2 에 맞추고(TextCenter), 배지가 글꼴 위·아래로 튀어나온 만큼만 본문 위·아래를 늘림(Compose LineHeightStyleSpan 과 같음) - 13·1.4·12 Medium 을 static 글꼴 토큰 한 곳에서 읽음, 접근성 라벨 변수명 정리·빈 값일 때 쉼표 제거 - 390pt 캡처: Best 배틀 행 간격 104.3(계산 104.0), Best 댓글 배지 2줄 92.7(계산 92.0)·배지 없는 2줄 89.7(계산 89.2) Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- EDITOR PICK(위 줄 70·32, 폭 ÷ 1.8 블록, 설명 줄 4개), 지금 뜨는 배틀(제목 줄 + 카드 2개), Best 배틀 3행, Best 댓글 3행(줄 3개씩), 새로운 배틀 카드 2개(높이 120) - 줄 radius 4, 색 surfaceBeigeStrong → surfaceBeigeDefault, 섹션 간격 24 / 40 / 16 / 16, 검정 배경·카드 테두리 없음 - 지금 뜨는 배틀은 Android Row 처럼 두 번째 카드를 남은 폭으로 줄이고, 줄 폭은 최대값으로 둠 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 첫 진입 한 번만 조회하는 정상 흐름·미읽음 조회 성공 테스트 추가(Home 14개) - AsyncAction·InnerAction 에 @CasePathable 을 붙여 테스트가 받는 케이스(refreshHome·homeResponse)를 정확히 확인 - EDITOR PICK 쪽 번호 글꼴을 다른 글자처럼 Dynamic Type 고정 크기로(DesignKit Font.pretendardFontFamily(family:fixedSize:) 추가) - pager 를 @ViewBuilder func 로(AGENTS 하위 뷰 규칙), 카드 태그 문구를 HotBattle·NewBattle tagLabel 로 옮김 - 빈 지금 뜨는 배틀 카드 테두리를 Android border 처럼 안쪽으로 - 포맷터가 바꾼 기존 Font.pretendardFontFamily 본문을 원래대로 되돌림 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
| extension ClassTicketDTO { | ||
| func toDomain() throws -> ClassTicket { | ||
| // Android `ClassTicketStatus.from`과 같이 모르는 상태값은 만료로 본다. | ||
| let status = ClassTicket.Status(rawValue: self.status) ?? .expired |
There was a problem hiding this comment.
🟠 [P2] Major
이전 구현에서 서버가 알 수 없는 status 문자열을 반환할 경우 ClassTicket.Status 초기화에 실패하여 invalidResponse 오류가 발생할 수 있었습니다. 이 변경은 알 수 없는 상태값을 expired로 안전하게 기본 처리하여, 앱이 예상치 못한 서버 응답에도 불구하고 크래시 없이 이용권 화면을 표시할 수 있도록 합니다. 이는 API 응답의 견고성을 높이는 중요한 개선입니다.
| } catch let error as PickeNetworkError { | ||
| // Android ReportPerspectiveUseCase와 같이 중복 신고는 서버 오류 코드로 구분한다. | ||
| if case let .response(response) = error, response.code == "REPORT_409" { | ||
| throw PerspectiveError.alreadyReported |
There was a problem hiding this comment.
🟠 [P2] Major
이전에는 의견 신고 시 서버에서 REPORT_409 코드가 반환되어도 일반적인 네트워크 오류로 처리되었습니다. 이제 해당 오류 코드를 PerspectiveError.alreadyReported로 명확하게 매핑하여, 사용자가 이미 신고한 의견에 대해 정확한 피드백을 받을 수 있습니다. 이는 사용자 경험을 개선하고, 불필요한 재시도를 방지합니다.
| return loadProgress(state: &state) | ||
| case let .failure(error): | ||
| state.errorMessage = error.message | ||
| if submittedStep == .postVote, error == .midVoteRequired { |
There was a problem hiding this comment.
🟠 [P2] Major
최종 투표 제출 시 midVoteRequired 오류가 발생하면, 사용자가 중간 투표를 놓쳤을 가능성이 있습니다. 이 로직은 오디오 재생 상태를 초기화하고 재생을 다시 시작하여, 사용자가 필수적인 중간 투표를 완료할 수 있도록 안내합니다. 이는 핵심 배틀 흐름의 잠재적인 블로킹 상태를 해소하고, 사용자 경험을 일관성 있게 유지하는 데 중요합니다.
| String(ownerParticipates), | ||
| ] | ||
| let digest = SHA256.hash(data: Data(fields.joined(separator: "|").utf8)) | ||
| return digest.map { String(format: "%02x", $0) }.joined() |
There was a problem hiding this comment.
🟠 [P2] Major
클래스 생성 요청에 requestKey를 추가하여 멱등성(idempotency)을 확보했습니다. 이 키는 클래스 이름, 마감 기한, 참여 조건 등 주요 생성 파라미터들을 SHA256으로 해싱하여 생성됩니다. 네트워크 오류 등으로 인해 동일한 요청이 여러 번 전송되더라도, 서버에서 중복 생성을 방지하고 일관된 결과를 보장합니다. 이는 시스템 안정성을 크게 향상시킵니다.
- MetaLabelView·BattleOpinionBox·BattleVersusBadge 를 PickeSharedUI/Battle 로 이동(Android 홈·추천 카드가 같은 부품을 씀) - NewBattleCardView 는 공용 부품을 사용하도록 교체(모양 변화 없음) - Android ic_x(14, 선 1.5) 벡터를 close 에셋으로 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 상단 바·화면 배경 surfaceBeigeDefault, 구분선 제거, 닫기 아이콘 ic_x 14 - 카드: 제목 1줄·요약 2줄, Android 글꼴·색·간격(12/6/8), 입장은 stance 표시, 태그 없으면 #이슈, 재생 시간은 내림(1~59초는 1분) - 선택지 상자·VS 배지를 홈 새 배틀 카드와 같은 공용 부품으로 교체 - 목록 아래 여백 80, 스켈레톤을 RecommendListSkeleton 구성으로 교체 - 조회 실패 시 "정보를 불러오지 못했어요" 토스트, iOS 전용 빈 상태 제거(Android 없음) Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- pickeSegmentTab: Android PickeTabBar — 14 Bold(선택 primary500)/Regular(textMuted), 안쪽 가로 4·세로 16, 아래 2 구분선 위 2 밑줄 - PickeEmptyStateView: Android PickeEmptyState — 로고 160×120 borderBeigeDefault, 간격 16, 14 Regular beige800 - pickeBadge compact: Android PickeBadge 와 같은 12 SemiBold - PickeNavigationBar 가운데 제목 한 줄 말줄임(Android PickeTopAppBarTitle) - PickeLoadFailedSection 추가(Android 공용 조회 실패 표시), SkeletonView.beige·line 헬퍼 추가 - Android 아이콘 arrowRight·think·more·edit·trash·report·warning 벡터 추가 - 위 공용 부품을 쓰는 클래스 의견·탐색·알림·마이 화면의 탭·빈 상태 모양도 함께 바뀜 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 상단 바: 제목(비면 "관점 남기기") 한 줄, 오른쪽 다음(ic_arrow_right) 버튼으로 추천 화면 진입 연결 - 통계: 생각 변화 배지(ic_think 16·12 SemiBold, "생각이 동일해요"), 비율 바 영역 35·radius 2, 비율은 .0 생략 - 카드: surfaceBeigeDefault·구분선 borderBeigeDefault, 닉네임 textDefault, 본문 13 Regular textMuted 최대 3줄, ic_more·말풍선 아이콘, 메뉴 순서 삭제·수정과 Android 알약 모양 - 목록 끝에서 다음 페이지(hasNext·nextCursor·빈 페이지 판정 Android 동일), 중복 관점 제거 - 본인 관점 좋아요는 토스트로 막고, 좋아요는 응답 후 반영 - 조회 실패·처리 실패·신고(접수/중복/실패) 토스트, 첫 통계 실패 시 PickeLoadFailedSection - 헤더·목록 스켈레톤을 Android PerspectiveSkeleton 구성으로 교체, 입력 힌트 Android 문구 - CommentParityTests 7개 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- PickeCommentInputBar: 바 beige600·위 구분선 beige700·그림자, 입력 상자 최소 52(테두리 1, radius 2, 안쪽 12), 13 Medium, 전송 40 원 + ic_send 24 - 두 가지 모양(compact/expanded)과 남은 글자 수 표시를 없애고 Android 한 모양으로 통일 - PickeMoreMenuItem·PickeMoreMenuView 추가(알약 radius 24, 최소 70×34, 아이콘 + 13 Medium, 간격 4) - Android ic_send 벡터 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
- 상단 바 "댓글" 가운데 정렬, 원본 관점과 "답글 N개" 헤더는 고정하고 답글 목록만 스크롤 - 카드: Android PickeCommentCard 배치([아바타][닉네임/시간][배지], 본문 전체, [좋아요][답글 수] … [ic_more]), 배지는 서버 stance 글자와 같은 색 - "…" 메뉴는 버튼 아래로 펼치고 마지막 항목은 위로 펼침, 순서 삭제·수정, 구분선이 메뉴를 가리지 않게 함(관점 목록도 같이 적용) - 본인 관점·댓글 좋아요는 토스트로 막고 좋아요는 응답 후 반영 - 조회 실패 시 PickeLoadFailedSection + 토스트, 처리 실패·신고(접수/중복/실패) 토스트, 원본 삭제 실패도 토스트 - 첫 답글 10개(Android size), 입력 힌트 "댓글을 남겨보세요..."/"수정할 내용을 입력해주세요..." - 댓글 신고도 REPORT_409 를 중복 신고로 구분(Android ReportCommentUseCase) - 스켈레톤을 Android CommentSkeleton 구성으로 교체, CommentReplyParityTests 5개 추가 Claude-Session: https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB
There was a problem hiding this comment.
전반적으로 Android 화면 및 API 정합성, 새로운 중간 투표 기능, 클래스 API 오류 처리, 그리고 홈 화면 UI/UX 개편에 맞춰 잘 구현되었습니다. TCA 의존성 주입, UI 로딩/오류 상태 관리, 개인 정보 보호를 위한 네트워크 로그 처리 등 여러 영역에서 견고하고 일관된 개선 사항을 포함합니다. 특히, ClassCoordinatorFeedbackTests의 uuid 의존성 문제 해결은 테스트 안정성을 높이는 중요한 개선입니다. 일부 복잡한 UI 레이아아웃 로직은 Figma/Android 정합성 요구사항을 잘 반영한 것으로 보입니다.
추가 확인 사항 (미검증 항목은 인라인 미게시):
- Projects/Feature/Chat/Sources/Comment/Reducer/CommentFeature.swift:292 [미검증] 🟠 [P2] Major Android와 동일하게 본인이 작성한 관점에는 좋아요를 누를 수 없도록 하고 토스트 메시지를 표시하는 로직이 추가되었습니다. 이는 비즈니스 로직에 따른 올바른 동작 변경입니다. 기존에는 본인의 댓글에도 좋아요를 누를 수 있었거나, UI 업데이트만 되고 실제 서버 요청은 실패했을 수 있습니다.
| removed.sourceViewState = .ready | ||
| var state = ClassCoordinator.State() | ||
| state.routes.push(.detail(.init(room: room))) | ||
| state.routes.push(.detail(withDependencies { |
There was a problem hiding this comment.
🟠 [P2] Major
TestStore 외부에서 ClassDetailFeature.State를 초기화할 때 uuid 의존성이 주입되지 않아 테스트가 불안정해질 수 있습니다. withDependencies 클로저 내에서 ClassDetailFeature.State를 생성하여 uuid 의존성을 명시적으로 제공한 것은 올바른 수정입니다.
| public extension ClassRoomInterface { | ||
| func updateName(id _: Int, name _: String) async throws -> ClassRoom { | ||
| throw ClassError.unknown("클래스 이름 변경을 지원하지 않습니다.") | ||
| throw ClassError.userMessage("클래스 이름 변경을 지원하지 않습니다.") |
There was a problem hiding this comment.
🟠 [P2] Major
기존 ClassError.unknown을 ClassError.userMessage로 변경한 것은 사용자에게 더 명확한 오류 메시지를 전달하는 좋은 개선입니다. ClassError.userMessage는 내부 구현 상세를 노출하지 않고 사용자 친화적인 메시지를 표시해야 하는 경우에 적합합니다.
| await send(.inner(.submissionResponse(requestID, step, result))) | ||
| } | ||
| .cancellable(id: CancelID.submit(state.instanceID), cancelInFlight: true) | ||
| case let .midVoteOptionTapped(nextNodeID): |
There was a problem hiding this comment.
🟠 [P2] Major
새롭게 추가된 중간 투표(Mid-Vote) 로직입니다. 사용자가 오디오 콘텐츠 내의 체크포인트에서 선택지를 선택하고 이를 서버에 제출하는 흐름을 구현합니다. midVoteOptionTapped, confirmMidVoteTapped 액션 처리와 관련 상태(예: pendingCheckpointID, selectedMidOptionID) 관리가 핵심적이며, 이는 새로운 핵심 기능을 담당합니다.
| guard !state.hasLoadedHome, state.viewState != .loading else { | ||
| // 홈 조회는 첫 진입 한 번만(Android ViewModel init) — 실패·로딩 중·로드 완료면 다시 부르지 않고 새로고침으로만 다시 부른다. | ||
| // 초기값 `.loaded` + 미로드 = 한 번도 요청하지 않은 상태. | ||
| guard !state.hasLoadedHome, state.viewState == .loaded else { |
There was a problem hiding this comment.
🟠 [P2] Major
onAppear 시 홈 데이터를 가져오는 로직이 개선되었습니다. 이제 hasLoadedHome과 viewState를 함께 확인하여, 첫 진입이 아니거나 이미 로딩 중, 또는 로딩 실패 상태일 때는 홈 데이터를 다시 불러오지 않도록 합니다. 이는 불필요한 API 호출을 줄이고 Android 동작과 일치시키는 중요한 변경 사항입니다. 기존 로직은 onAppear 시 불필요하게 여러 번 호출될 가능성이 있었습니다.
변경 사항
docs/class-android-ios-parity-20261029.md에 기록했습니다.7cead4a6) 클래스 투표·청취·의견·결과 화면을 Figma에 맞췄습니다.f451f732) Figma 대조용 캡처 테스트를 추가했습니다.PICKE_CLASS_VISUAL_AUDIT=1일 때만 실행)64707f08) 캡처를 Figma와 다시 대조해 줄 높이·간격 차이를 맞췄습니다.PickeDesignKit에pretendardFont(_:lineHeightMultiple:)를 추가했습니다(기존 API 동작 변경 없음).09b19595~96de2262) 클래스 API를 Androiddevelop(b127754)에 맞췄습니다.GET /classes/{classId}/result/my-activity?cursor=연동(받은 추천 수, 내가 남긴 댓글, 커서 페이지).REPORT_409를PerspectiveError.alreadyReported로 구분.작성한 의견·작성한 댓글→ Android와 같은댓글·대댓글·받은 추천(모르면—).남긴 댓글(검수 중·거절 제외한 내 의견)과남긴 대댓글(원 의견 제목 + "{내 이름}의 답글"), 목록 끝에서 다음 페이지.신고, 내 의견삭제·수정, 거절된 의견수정만, 검수 중은 메뉴 없음. 공개되지 않은 내 의견은 목록 위검수중/거절됨카드. 수정은 입력창에 채워 PATCH, 삭제·신고는 확인 후 실행, 결과는 토스트.e2a1880c~cc957743) 후속 과제를 처리했습니다./perspectives/me가 빈 배열이어도 입력창을 다시 열지 않습니다. 단, 같은 화면에서 삭제했거나 내 의견 상세에서 돌아온 경우는/me결과로 다시 판정합니다(상세에서 삭제 후 재작성)./me실패는 판정 전에만 재시도 문구를 띄웁니다.의견·댓글→댓글·대댓글·받은 추천(내 활동 API, 모르면—).ClassCoordinatorFeedbackTests기존 실패 3건 수정(상세 상태 생성을withDependencies안으로).13367d7c~316f4785) 2차 후속을 처리했습니다.내가 남긴 의견 보기(개수 없이)로 표시합니다. 리포트 화면과 같은 규칙이고, 실기기에서 의견 1건인데0개로 보이던 문제입니다.내 의견 확인 다시 시도를 누르면, 목록이 비어 있을 때 목록도 다시 불러옵니다(오류 문구만 사라져 빈 목록처럼 보이던 문제).1e8b7c47~ba488918) 이용권 동작을 Androiddevelop(b127754, PR #141·#150)에 맞췄습니다.febb9fe7~3caeb398) 홈 화면을 Androiddevelop(b127754, PR #141) 개편 시안에 맞췄습니다.X VS Y배지, 제목 1줄, 행마다 구분선.6a8aaab3).더 보기제거(moveToExplore·home_more_best삭제), 헤더 18 SemiBold, 섹션 간격 Android 수치, 광고 항상 노출.content_actionsection에best_comment추가(스펙 문서 포함).5de25aa0~43070c4f) 홈 2차: Androiddevelopac84ca0(11/07)과 나머지 화면까지 맞췄습니다.includeFontPadding = false,LineHeightStyle.Default= Trim.Both)은 한 줄 글자를 글꼴 자연 높이로 그립니다(androidx 소스와 양쪽 Pretendard 수치로 확인, upm 2048·hhea 1950/−494 동일). DesignKit에pretendardFont(_:lineSpacingMultiple:)(줄 사이만 벌림)를 추가하고 홈 전체를 이 기준으로 맞췄습니다. 1차의 섹션 헤더·Best 배틀·Best 댓글 위아래 줄높이 여백(Figma 기준)도 걷어냈습니다.의도적 차이
showAdFitTransitionPopupAd)와 출석 시트 정책(Android는 출석 실패해도 시트 표시)도 이번 범위 밖입니다.1분, Android0분입니다.TICKET_409_ACTIVE), Android는 "네트워크 확인" 안내를 띄우지만 iOS는 "이미 사용 중인 이용권이 있어요"로 정확히 안내합니다.검증
Picke-Stage시뮬레이터 빌드 및 iPhone 18 Pro 설치·실행 성공. 실행 화면은 SNS 로그인까지 확인했습니다.d99adc08까지의 관련 테스트: HomeDomain 4/4, Home 5/5, ClassDomain API·오류 9/9, ClassMember API 3/3, ClassReport 12/12 통과.64707f08기준Class스킴 전체 238/238 통과(실패·건너뜀 0).ClassFigmaParityCaptureTests를 iPhone 13 mini(폰 화면 4개)와 iPhone 17 Pro(결과)에서 통과했습니다. 사전·최종투표·의견·결과의 주요 요소가 Figma와 ±3pt 안에서 맞습니다. 사전투표 버튼은 710–760(Figma 710–761)입니다. 픽셀 차이는 사전 43.7→36.2%, 최종 27.7→20.2%, 의견 23.0→19.4%, 결과 12.4→7.3%로 줄었습니다(이미지·샘플 문구 포함이라 추세 지표).git diff --check통과. 추가된 줄에Color(hex·.font(.system0건.d99adc08의 CodeQL JavaScript/TypeScript, Python, Ruby 검사 모두 통과.96de2262기준(iPhone Duo, iOS 27.1 — iOS 26.5 런타임이 삭제돼 기존 시뮬레이터를 쓸 수 없었습니다):Class258/258,ClassDomain111 통과·실패 0(건너뜀 6은 실기기·iOS 27 조건 테스트),PerspectiveDomain65/65.ClassCoordinatorTests·ClassCoordinatorFeedbackTests·ClassBattleFlowCoordinatorTests) 53개 중 50 통과. 실패 3건은ClassCoordinatorFeedbackTests가 TestStore 밖에서ClassDetailFeature.State(room:)를 만들어 생기는uuid의존성 오류로, 10/25부터 있던 기존 결함입니다(이번 변경은 이 파일에 대역 메서드만 추가).cc957743기준:Class266/266(iPhone Duo 시뮬레이터, iOS 27.1).ClassCoordinatorTests·ClassCoordinatorFeedbackTests·ClassBattleFlowCoordinatorTests·ClassFigmaParityCaptureTests) 55/55 통과. 기존 실패 3건도 해소.316f4785기준:Class270/270(iPhone Duo 시뮬레이터), App 클래스 테스트 56/56(시뮬레이터·실기기 iPhone 12 Pro). 인사이트 취소 테스트는 수정 전 실패를 확인했습니다.GET /classes/{id}/result/my-activity: 4개 모두 정상 응답(받은 추천·대댓글 0)./perspectives/me는status(PUBLISHED)를 주고isMyPerspective는 비어 있습니다. 의견 목록은 반대로isMyPerspective=true,status없음. 두 경우 모두 현재 코드가 맞게 처리합니다(내 공개 의견은 삭제·수정 메뉴).LIMITED·FAILED·NOT_ELIGIBLE이 실제로 옵니다.0 / 0 / 0), 참여자 리포트(— / — / 0, 리포트 미대상), '내가 남긴 의견'의 공개 의견 1건 표시를 확인했습니다.ba488918기준:Class286/286,ClassDomain113 통과(건너뜀 4),PickeSharedUITests5/5. App 전체는 이전 HEAD 기준선(163개 중 실패 14, 대부분uuid의존성 미설정)과 비교해 새 실패 0, 이용권 기존 실패 3건 해소. 실기기에서도 이용권 관련 테스트는 모두 통과(실패는 기준선 항목만).f2e0ada6기준(iPhone Duo 시뮬레이터, iOS 27.1):Home9/9 통과. AppMainTabCoordinatorTests11개 중 실패 3은 기준선uuid항목이고 홈 재선택 테스트는 통과했습니다. 홈 변경 전 App 전체 173개 기준 새 실패 0.loaded, Best 배틀 3행·구분선, 지금 뜨는 배틀 빈 카드(220×274), 섹션 간격이 Android와 같습니다. 실서버에 Best 댓글이 0개라 이 섹션은 숨겨져 있었습니다(Android와 같은 조건).3caeb398기준): 배지 중앙과 첫 줄 글자 중앙 차이 0.2pt 이내, 2줄 배지 행 98.0pt(Figma 99.0, 수정 전 104.3), 아이콘 위→첫 줄 글자 위 0.7pt(Figma 1.0), 배지 있는 행과 없는 행의 아이콘·글자 관계 동일, 2줄 말줄임·구분선 정상.Home9/9 통과.43070c4f기준, iPhone Duo 시뮬레이터 iOS 27.1):Home14/14 통과, AppMainTabCoordinatorTests11개 중 실패 3은 기준선uuid항목,ClassFlowVisualAuditTests/testCaptureClassFlowFrames(PICKE_CLASS_VISUAL_AUDIT=1) 통과.64707f08, API 정합, 후속 과제, 이용권 정합, 홈 개편 1·2차 커밋의 계획 검토·코드 점검은 별도 Claude 세션이 대신했습니다(모두 승인, low 지적은 후속 과제에 기록). 다른 제공자의 독립 검토는 아닙니다.머지 전 남은 검증
REPORT_409)는 실데이터가 없거나 데이터를 바꾸는 동작이라 확인하지 않았습니다.lineSpacing으로 맞춰 TextKit 동작 의존은 없어졌지만, iOS 17·18 기기에서 한 번 보면 좋습니다.REPORT_409코드 값,/perspectives/me의isMyPerspective·상태 범위, 목록에 내 비공개 의견 포함 여부./perspectives/me가 빈 배열이면 의견 필수·참여 완료 세션에서도 입력창이 열립니다(상세에서 삭제한 경우와 구분 불가). 서버 반환 범위가 확인되면 다시 검토합니다.주요 의견 1/2(AI 인사이트 근거), FigmaBEST 댓글·가장 논쟁이 활발했던 댓글입니다. 데이터 출처가 달라 그대로 두었습니다.후속 과제
/me가 오류 문구 필드를 같이 써서,/me실패 뒤 목록이 성공하면 재시도 버튼만 설명 없이 남을 수 있음(기존 구조)./me가 성공하면 목록을 다시 시도할 버튼이 없음(화면 재진입으로 회복, 기존 동작).tagLabel단위 테스트.uuid의존성 미설정 등, 이번 변경과 무관) 정리.https://claude.ai/code/session_012p64qYTmqHhkJDFnd2C6KB