feat: 채팅방 / 사전 투표 흐름 완성 + Chat 모듈 분리 + 배틀 상세·시나리오 API 연동#35
Conversation
- SwiftUI Convention 항목을 프로젝트 AGENTS.md 의 `@ViewBuilder private func/var` 패턴이 정식임을 명시하도록 수정 (SubView struct 강제 의견 절대 생성 금지) - Ignore 목록 확장 — 빈 줄/whitespace/trailing comma/주석 줄/import 순서/들여쓰기/`description` 같은 BaseTargetType 정식 네이밍 의견 모두 제외 - Severity Format 에서 P5 (Nitpick) 등급 완전 제거, P1 ~ P4 만 사용 - JSON example body label `[P1~P5]` → `[P1~P4]`
- Entity: ChatSpeakerSide / ChatSpeaker / ChatMessage / ChatRoomBundle (.mock 7개 메시지, .pen `채팅방` k3lIx 1:1) - ChatRoomFeature: bundle/isPlaying/currentTime State + back · refresh · togglePlay · seek(±15) · scrub · DelegateAction.dismiss - ChatRoomView: PickeNavigationBar 헤더(back + 배틀 타이틀 + refresh) · 같은 speaker 메시지 그룹화한 좌/우 말풍선 · 진행 progress + 시간 + AudioPlayerControlView - DesignSystem: AudioPlayerControlView (.pen `Group 26` 매핑) — backward.end · play/pause · forward.end + "15초" 라벨, 채팅방 외에도 오디오 재생 필요한 화면에서 재사용 가능
- POST /api/v1/battles/{battleId}/votes/pre 연동
- BattleAPI / BattleService / PreVoteRequest 추가
- PreVoteResult Entity + DTO 매퍼
- BattleRepositoryImpl 추가 + WeaveDI 등록
- PreVoteFeature: submitPreVote 액션 + voteSubmitted 델리게이트
- selectedSide → poll.options 의 optionId 매핑
- HomeCoordinator: chatRoom 라우트 추가
- 투표 완료 델리게이트 수신 시 ChatRoomView push
- swiftformat:disable extensionAccessControl 로 @Reducer 매크로 호환 유지
- ChatRoomView.progressBar 에 DragGesture(minimumDistance: 0) 부착 - 가로 위치 비율 → currentTime 매핑 후 .scrub(time) 전송 - 썸 크기 16px 로 확대 + contentShape 으로 트랙 전체 히트박스 - AGENTS.md: - Coordinator 섹션에 swiftformat:disable extensionAccessControl 디렉티브 필수 명시 - DomainType url switch 모든 case 에 return 명시 규칙 추가
- ChatRoomFeature - ContinuousClock 기반 1초 tick → currentTime 증가 - 재생 종료 시 hasFinishedListening = true, 자동 정지 - 재생 중에는 seekBackward / seekForward / scrub 잠금 (canScrub gate) - togglePlayTapped / refreshTapped / backButton 시 tick 시작·정지 동기화 - ChatRoomView.progressBar - allowsHitTesting(canScrub) 로 드래그 차단 + opacity 0.6 시각 피드백
- GET /api/v1/battles/{battleId}/scenario 연동
- BattleAPI / BattleService.scenario 추가 (GET)
- BattleScenarioDataDTO + 매퍼 (philosophers / nodes / scripts / interactiveOptions)
- BattleScenario Entity + ScenarioPhilosopher / ScenarioNode / ScenarioScript / ScenarioInteractiveOption
- ScenarioSpeakerType / RecommendedPathKey enum (unknown fallback)
- BattleInterface.fetchScenario + Default/Repository 구현
- ChatRoomFeature
- State.scenario / isLoadingScenario 추가, battleTitle 은 scenario.title 우선
- onAppear → fetchScenario 비동기 호출 (cancellable)
- scenarioResponse(Result) 핸들러로 단일 분기
- 부수: DI 키 computed property 에 explicit return 일관화 (Auth/Home/Poll/OAuth Provider)
- DomainType.url 한정이던 기존 규칙을 BaseTargetType (urlPath / method / parameters / task / sampleData) 와 DependencyKey (liveValue / testValue) 까지 일반화 - 새 case 추가 시 기존 case 까지 explicit return 으로 정렬하라는 가이드 명시 - 포맷터가 반복 깨면 swiftformat:disable redundantReturn 페어 사용 권고
- HStack alignment .top + 모든 버튼이 동일한 VStack(Image + Caption) 구조 - play 버튼은 caption=nil 로 자리만 차지(opacity 0) → backward/forward 의 "15초" 레이블과 위치 어긋남 해소 - Image frame 을 40x32 로 고정해 SF Symbol 너비 차이로 인한 좌우 비대칭 제거
- ChatRoomFeature
- State.currentNodeId / selectedOptionLabel / interactiveOptions / shouldShowOptions / isConfirmEnabled 추가
- scenarioResponse 시 startNodeId 로 currentNodeId 초기화
- View.optionTapped(label) / confirmOptionTapped 추가
- 확정 시 currentNodeId 를 nextNodeId 로 전환 + currentTime / hasFinishedListening 리셋해 다음 노드 청취 사이클 진입
- ChatRoomView.interactiveOptionsSection
- shouldShowOptions == true 일 때만 메시지 리스트와 재생바 사이에 노출
- 헤더 (가는 선 + "당신의 입장을 선택해주세요" + 가는 선)
- 선택지 카드 — 선택 시 stroke #E1B974, 텍스트 neutral800
- "입장 선택하기" Primary CTA, 선택 전엔 비활성
- ChatRoomView 옵션 카드 stroke - hardcoded "E1B974" / .beige600 → .borderSecondarySelected / .borderBeigeDefault - 배경도 .beige400 (디자인 #f7f4ee) 로 정정 - ChatRoomFeature.canScrub 를 임시 true 강제 - 음원 실재생 연결 전까지 진행바 드래그/탭 시킹 항상 가능 - 실재생 연동 후 hasFinishedListening 게이트로 복원 (TODO 코멘트)
- ChatRoomFeature.shouldShowOptions 에서 hasFinishedListening 조건 제거 - currentNode 의 interactiveOptions 가 있으면 즉시 노출 - 음원 실재생 연동 후 완청 시점 게이트로 복원 예정
- AuthRepositoryImpl.login 진입 직전 JSONEncoder 로 body 직렬화 후 Log.debug - provider 별 페이로드 차이(identityToken / redirectUri 포함 여부) 콘솔에서 즉시 확인
- HotBattleCardView / HeroCarouselView / BestBattleCardView / NewBattleCardView
- Text(tag.name) → Text("#\(tag.name)")
- HomeBundle.replacingEmptySectionsWithMocks 의 hotBattles fallback 제거 (API trendingBattles=[] 일 때 mock 으로 치환하지 않고 빈 배열 유지) - HomeView 의 hotBattlesSection 호출부에 store.hotBattles.isEmpty 가드 추가
- HomeBundle.replacingEmptySectionsWithMocks 를 self 반환으로 단순화 (mock 치환 제거 — 서버 응답 그대로 노출) - HomeView 각 섹션 호출부에 isEmpty 가드 - heroes / hotBattles / bestBattles / newBattles - todayPicke 는 quizzes / votes 둘 다 비어 있을 때 숨김
- Entity 신설 - BattleDetail / BattleInfo / BattleOption / UserVoteStatus / BattleStep - 기존 BattleTag.TagType 에 case value = "VALUE" 추가 - Data 신설 - BattleDetailDataDTO + 매퍼 (battleInfo / option / tag) - BattleAPI.detail, BattleService.detail (GET, no body) - DomainInterface - BattleInterface.fetchBattle(battleId:) + Default/Repository 구현 - PreVoteFeature - PollDetail / pollRepository / pollId 의존 전부 제거 - State.init(battleId:) 외부 주입 필수, fetchBattleDetail 으로 갱신 - shareTapped 의 URL 은 응답 shareUrl 우선 - makeBattle 을 BattleDetail.options + representative 기반으로 재작성 - categoryTags 는 화면에서 "#태그" 형태로 그대로 매핑 - HomeCoordinator: presentPreVote(battleId) 델리게이트에서 battleId 를 .preVote(.init(battleId:)) 로 전파 - DI: PollRepositoryImpl 등록 제거 - 삭제: Data/API/Poll, Data/Model/Poll, Data/Repository/Poll, Data/Service/Poll, DomainInterface/Poll, Entity/Poll 디렉터리 전체 - PreVoteView.shouldShowSkeleton: store.poll → store.battleDetail
- BattleDetailDataDTO.userVoteStatus / currentStep 을 Optional 로 변경 (서버가 null 로 내려주는 케이스 디코딩 실패 해결) - 매퍼: nil 이면 UserVoteStatus.none / BattleStep.none 으로 폴백
- 본문 콘텐츠를 ScrollView 로 감싸고 navigationBar 를 safeAreaInset(top) 으로 분리 - 콘텐츠가 길어져도 화면 밖으로 밀려 잘리지 않음 - back / share 가 hero 이미지 위에 항상 표시 - contentArea 의 .padding(.top, 380) 으로 hero 노출 영역 확보
- ZStack(top alignment) + padding(top: 380) 으로 인한 width 추론 / padding 누락 문제 해결 - ScrollView 안에 VStack(spacing: -120) 으로 hero 와 콘텐츠가 자연스럽게 겹치도록 변경 - backgroundImage: frame(maxWidth: .infinity) 명시 + ignoresSafeArea 는 ScrollView 한 곳에서만 적용
- ChatRoomFeature.State - messages: scenario.nodes[*].scripts 를 ChatMessage 로 매핑 (speakerType A→left, B→right) - totalDuration: scenario.nodes.audioDuration 합산값 우선 사용 - audioUrl: scenario.audios[recommendedPathKey] 우선, 폴백으로 첫 항목 - 화자(speakerName) 으로 PhilosopherAvatar 매칭, 매칭 실패 시 .plato 폴백 - PreVoteView - loadedContent 를 GeometryReader 안 ScrollView 로 감싸 width 안정화 - contentArea 패딩 / titleText·summaryText multilineTextAlignment & lineLimit(nil) - optionCard minHeight 121 + minimumScaleFactor 로 글자 잘림 방지
- Domain/DomainInterface - AudioPlayerInterface 프로토콜 + DefaultAudioPlayerImpl + AudioPlayerDependency - DependencyValues.audioPlayer 노출 - Data/Repository - AudioPlayerRepositoryImpl + 내부 AudioPlayerService (AVPlayer @mainactor) - addPeriodicTimeObserver → AsyncStream 으로 currentTime 멀티 구독 - load / play / pause / seek / duration / currentTimes - App DI: AudioPlayerRepositoryImpl as AudioPlayerInterface 등록 - ChatRoomFeature - mock tick(startTicking/stopTicking) 제거 → audioPlayer 실재생 연결 - onAppear 에서 subscribePlayer + fetchScenario 트리거 - scenarioResponse 성공 시 audios[recommendedPathKey] URL 로 player.load - togglePlay / seekBackward·Forward / scrub / refresh / back 모두 player API 호출 - playerDuration / playerTimeUpdated InnerAction 으로 실시간 갱신, totalDuration 우선 적용
- safeAreaInset(.top) 안 navigationBar 는 ignoresSafeArea 적용된 ScrollView 와 히트 영역이 겹쳐 터치가 스크롤로 흘러갔다 - overlay(.top) + 명시 zIndex(10) 로 분리, topInset 만큼만 padding 적용 - topInset: UIWindowScene keyWindow 의 safeAreaInsets.top (없으면 47)
- Entity - PreVoteBattle / ScenarioPhilosopher 등 화자 메타데이터 확장 - ChatMessage / ChatSpeaker 에 label·name·imageURL·center side 지원 - ChatRoomFeature - speakerType 별 화자 매핑 헬퍼 정리 (A→left, B→right, NARRATOR→center) - philosophers 배열의 imageUrl 을 ChatSpeaker.imageURL 로 연결 - ChatRoomView / PreVoteView / PreVoteSkeletonView - 화자 아바타를 KFImage URL 기반으로 변경 - 사전 투표 화면 CTA / 콘텐츠 패딩 분리 (overlay + bottom CTA 고정)
- ChatMessage.startTimeMs (Optional) 추가, 시나리오 매핑 시 script.startTimeMs 전달 - ChatRoomFeature.State.activeMessageId computed - currentTime(초) * 1000 이상의 startTimeMs 를 가지지 않은 마지막 메시지 id - ChatRoomView.messageList 에 ScrollViewReader + onChange(activeMessageId) - 활성 메시지가 속한 SpeakerGroup 의 마지막 id 로 scrollTo(anchor: .center) - 0.25s easeInOut 애니메이션
- onChange(of: store.activeMessageId) 는 ObservableState 의 computed property 라 변경 감지가 안정적이지 않아 스크롤이 발생하지 않았다 - onChange(of: store.currentTime) 으로 변경, 핸들러 내부에서 activeMessageId → scrollTargetId 로 계산 후 scrollTo
- ChatMessage.messageId 를 scriptId 기반 deterministic UUID 로 생성
- 매 렌더링마다 새 UUID 가 만들어져 ForEach id 가 흔들리고
ScrollViewReader 의 scrollTo 가 stale target 으로 무효화되던 문제 해결
- SpeakerGroup.id 를 messages.first?.id 로 도출, UUID() 매번 생성 제거
- ChatRoomFeature.canScrub 를 hasFinishedListening 게이트로 복원
- 음원 처음 한 번 완청한 뒤에만 진행바 드래그 / 15초 시킹 허용
- ChatRoomSkeletonView (별도 파일) 추가, .pen `채팅방 - Skeleton Loader` (2CRRg) 매핑 - 좌측 메시지 그룹 / 우측 메시지 그룹 / 좌측 메시지 그룹 + 재생바 placeholder - 모든 박스를 SkeletonView 로 구성 → 좌→우 shimmer 애니메이션 자동 적용 - ChatRoomView: store.isLoadingScenario && scenario == nil 일 때 skeleton 표시 - AudioPlayerControlView 사이즈 조정 - center play/pause: 55x55 - backward / forward: 24x55 - controlColumn 에 iconSize 파라미터 추가, Image scaledToFit + contentShape
- VoteCardView.answerSlot 의 선택 후 라벨이 고정 폭(52pt)에 묶여 잘리던 문제 해결 - 라벨 길이에 맞춰 가변 폭(fixedSize horizontal) + horizontal padding 8 - 최소 폭(minWidth 52) 은 placeholder 와 동일 유지
- ModulePath.Presentations 에 Chat 케이스 추가 - Projects/Presentation/Chat 신규 모듈 (스캐폴드 단계, 본 코드 이동은 후속 커밋) - Home Project.swift 에 Presentation(.Chat) 의존 추가 - ChatRoomFeature: customAlert 통합 (CustomAlertState / CustomConfirmAlert) 진입 준비 - ChatRoomView: customAlert presentation 연결, shouldShowOptions 시각 확인용 조건 임시 완화 - PreVoteView / PreVoteSkeletonView 잔여 정리 - DesignSystem Project / Alert 디렉터리 신규 자산
- Projects/Presentation/Chat 신규 모듈
- ChatCoordinator (Flow) + ChatCoordinatorView 추가
- ChatRoomFeature / ChatRoomView / ChatRoomSkeletonView 를 Home 에서 이동
- 의존: ComposableArchitecture, TCAFlow, Kingfisher, LogMacro,
Domain/UseCase, Shared/DesignSystem
- HomeCoordinator
- HomeScreen.chatRoom(ChatRoomFeature) 제거, chat(ChatCoordinator) 추가
- preVote.voteSubmitted 시 .chat(.init(battleId:)) push
- chat 의 delegate.dismiss 수신 시 backAction
- HomeCoordinatorView: chat 케이스에서 ChatCoordinatorView 사용
- swift format 친화적으로 모든 switch 분기에 explicit return 유지
…흐름 라우팅 #4 - Vote 디렉터리 (PreVoteFeature / PreVoteView / Skeleton) Home → Chat 으로 이동 - ChatCoordinator - root 를 .preVote 로 변경 (battleId 전달) - preVote.voteSubmitted 수신 시 chatRoom push - preVote.dismiss → delegate.dismiss 로 외부에 전파 - chatRoom.dismiss → 자체 goBack - ChatCoordinatorView: preVote / chatRoom 두 케이스 모두 렌더 - HomeCoordinator - HomeScreen 에서 preVote 케이스 제거 (단일 chat 진입점) - presentPreVote(battleId) 수신 시 chat 만 push - 모든 switch 에 explicit return 유지
- PreVoteSkeletonView - GeometryReader + 절대좌표(offset x/y) ZStack 매핑 제거 - hero / contentSection(tagsRow·titleBlock·summaryBlock·optionsRow) / ctaButton 의 의미적 VStack 으로 재구성 - SkeletonView shimmer 는 그대로 유지 (모든 박스에 자동 적용) - AGENTS.md `지원 스킬 목록` 에 SwiftUI Expert Skill 항목 추가 - 출처(AvdLee/SwiftUI-Agent-Skill) / 로컬 설치 경로 (Claude Code, Codex) - 재설치·업데이트 명령 / 호출 시점 가이드
- prompt 빌드를 외부 template literal 에서 string array + .join('\n') 로 변경
annotated_diff 안에 backtick / \${} 가 포함되면 template literal 이 깨져
SyntaxError: Invalid or unexpected token 이 발생하던 문제 제거
- ```suggestion 같은 triple-backtick 표현은 const TRIPLE 로 분리 후 concat
- pr_title / pr_body / annotated_diff 모두 string concat 으로 안전하게 결합
There was a problem hiding this comment.
전반적으로 잘 구성된 PR입니다. Chat 모듈 분리, 배틀 상세/시나리오 API 연동, 오디오 재생 기능, 그리고 사전 투표 흐름 구현이 깔끔하게 처리되었습니다. 특히 ChatRoomFeature 및 PreVoteFeature의 TCA Reducer 구현과 SwiftUI View 구성은 프로젝트의 아키텍처 가이드라인을 잘 따르고 있습니다. Skeleton Loader UI, 오디오 컨트롤, 사전 투표 오버레이 등 UI 디테일도 신경 쓴 점이 인상적입니다.
다만, DesignSystem 모듈이 ComposableArchitecture에 의존하게 된 부분과 테스트 파일명 오타는 개선이 필요합니다.
| // | ||
|
|
||
| import Testing | ||
| @testable import hat |
There was a problem hiding this comment.
🔴 [P1] Critical
@testable import hat 에서 모듈 이름이 hat으로 잘못 표기되었습니다. 실제 모듈 이름은 Chat이어야 합니다. 이 오타는 테스트 빌드 실패나 런타임 문제를 야기할 수 있습니다.
| @testable import hat | |
| @testable import Chat |
| import Testing | ||
| @testable import hat | ||
|
|
||
| struct hatTests { |
There was a problem hiding this comment.
🔴 [P1] Critical
테스트 파일의 struct 이름이 hatTests로 잘못 표기되었습니다. 실제 모듈 이름에 맞춰 ChatTests로 수정해야 합니다.
| struct hatTests { | |
| struct ChatTests { |
| product: .staticFramework, | ||
| settings: .settings(), | ||
| dependencies: [ | ||
| .SPM.composableArchitecture, |
There was a problem hiding this comment.
🟡 [P3] Minor
DesignSystem 모듈에 ComposableArchitecture (.SPM.composableArchitecture) 프레임워크 의존성을 추가했습니다. CustomAlertState와 CustomConfirmAlert가 TCA의 @ObservableState와 @Reducer를 사용하고 있기 때문입니다.
일반적으로 DesignSystem 모듈은 특정 아키텍처 프레임워크에 종속되지 않고 순수한 UI 컴포넌트만을 포함하여 재사용성을 높이는 것이 권장됩니다. TCA 요소를 직접적으로 DesignSystem에 포함하면, 이 DesignSystem 모듈을 TCA를 사용하지 않는 다른 프로젝트나 다른 아키텍처 패턴에서 재사용하기 어려워집니다.
프로젝트의 전반적인 아키텍처와 TCA 활용도를 고려할 때 의도된 설계일 수 있으나, DesignSystem의 역할에 대한 명확한 정의가 필요합니다. 만약 DesignSystem이 프레임워크 독립적이어야 한다면, TCA 기반의 알림 로직은 Presentation/Shared와 같은 TCA 종속적인 Presentation 모듈로 분리하는 것이 좋습니다.
| .SPM.composableArchitecture, | |
| // Projects/Shared/DesignSystem/Project.swift | |
| // dependencies: [ | |
| // .Shared(implements: .ThirdParty) | |
| // ], | |
| // ... | |
| // NOTE: DesignSystem에서 TCA 의존성을 제거하고, CustomAlert 관련 로직을 Presentation 계층의 적절한 모듈 (예: Presentation/Shared 또는 관련 Feature 모듈)로 이동하거나, TCA를 사용하지 않는 형태로 리팩토링하는 것을 고려해주세요. | |
| // | |
| // Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift | |
| // (CustomAlert 관련 코드) | |
| // .ifLet(\$customAlert, action: \.scope.customAlert) { | |
| // // CustomConfirmAlert() 대신, TCA에 독립적인 Alert 컴포넌트 또는 별도의 TCA 기반 Alert Feature를 사용하도록 변경 | |
| // // 예시: AlertState와 TCA의 기본 제공 Alert reducer 사용 | |
| // } |
There was a problem hiding this comment.
전반적으로 모듈 분리, API 연동, TCA 적용 및 SwiftUI 구현이 깔끔하게 잘 수행되었습니다. 특히 TCAFlow 기반 코디네이터 패턴과 AVPlayer 기반 오디오 재생 구현, 그리고 스켈레톤 로더 UI 구성이 인상 깊습니다.
다만, DesignSystem 모듈이 TCA에 의존하는 부분과 PreVoteFeature에서 API 실패 시 성공처럼 위임 액션을 보내는 부분은 아키텍처 및 로직상 개선이 필요해 보입니다. 기타 사소한 Swift 문법 및 UI 일관성 관련 P4 피드백이 있습니다.
| return"api/v1/home" | ||
| case .poll: | ||
| return"api/v1/poll" | ||
| return "api/v1/poll" |
There was a problem hiding this comment.
🔵 [P4] Readability
computed property에서 return 키워드는 Swift 5.1부터 단일 표현식의 경우 생략 가능합니다. 명시적으로 작성하는 것도 일관성 측면에서 좋지만, 프로젝트 전반의 규칙을 확인하고 따르는 것이 좋습니다.
| switch self { | ||
| case .login, .refresh, .logout: | ||
| .post | ||
| return .post |
There was a problem hiding this comment.
🔵 [P4] Readability
computed property에서 return 키워드는 Swift 5.1부터 단일 표현식의 경우 생략 가능합니다. 명시적으로 작성하는 것도 일관성 측면에서 좋지만, 프로젝트 전반의 규칙을 확인하고 따르는 것이 좋습니다.
| return .post | ||
| case .withdraw: | ||
| .delete | ||
| return .delete |
There was a problem hiding this comment.
🔵 [P4] Readability
computed property에서 return 키워드는 Swift 5.1부터 단일 표현식의 경우 생략 가능합니다. 명시적으로 작성하는 것도 일관성 측면에서 좋지만, 프로젝트 전반의 규칙을 확인하고 따르는 것이 좋습니다.
| switch self { | ||
| case let .login(_, body): | ||
| body.toDictionary | ||
| return body.toDictionary |
There was a problem hiding this comment.
🔵 [P4] Readability
computed property에서 return 키워드는 Swift 5.1부터 단일 표현식의 경우 생략 가능합니다. 명시적으로 작성하는 것도 일관성 측면에서 좋지만, 프로젝트 전반의 규칙을 확인하고 따르는 것이 좋습니다.
| return body.toDictionary | ||
| case .refresh: | ||
| nil | ||
| return nil |
There was a problem hiding this comment.
🔵 [P4] Readability
computed property에서 return 키워드는 Swift 5.1부터 단일 표현식의 경우 생략 가능합니다. 명시적으로 작성하는 것도 일관성 측면에서 좋지만, 프로젝트 전반의 규칙을 확인하고 따르는 것이 좋습니다.
| HStack(spacing: 4) { | ||
| ForEach(hero.tags) { tag in | ||
| Text(tag.name) | ||
| Text("#\(tag.name)") |
There was a problem hiding this comment.
🔵 [P4] Readability
태그 이름 앞에 # 접두사를 붙여 표시하는 것은 일관된 UI/UX를 제공하므로 좋은 개선입니다.
| VStack(alignment: .leading, spacing: 6) { | ||
| if let tag = battle.tags.first { | ||
| Text(tag.name) | ||
| Text("#\(tag.name)") |
There was a problem hiding this comment.
🔵 [P4] Readability
태그 이름 앞에 # 접두사를 붙여 표시하는 것은 일관된 UI/UX를 제공하므로 좋은 개선입니다.
| HStack(spacing: 10) { | ||
| if let tag = battle.tags.first { | ||
| Text(tag.name) | ||
| Text("#\(tag.name)") |
There was a problem hiding this comment.
🔵 [P4] Readability
태그 이름 앞에 # 접두사를 붙여 표시하는 것은 일관된 UI/UX를 제공하므로 좋은 개선입니다.
| VoteCardView(question: vote) | ||
| .contentShape(Rectangle()) | ||
| .onTapGesture { } | ||
| .onTapGesture {} |
There was a problem hiding this comment.
🔵 [P4] Readability
빈 onTapGesture {}는 뷰를 탭 가능하게 만들거나 상위 뷰의 제스처 전파를 막기 위해 사용되지만, 명시적인 목적 없이 사용될 경우 의도를 파악하기 어려울 수 있습니다. 만약 아무런 액션이 없다면 해당 제스처가 필요한지 다시 검토하거나, Button과 같은 명확한 인터랙티브 컴포넌트를 사용하는 것이 더 좋습니다. 현재로서는 주석으로 의도를 명시하여 혼동을 줄일 수 있습니다.
| product: .staticFramework, | ||
| settings: .settings(), | ||
| dependencies: [ | ||
| .SPM.composableArchitecture, |
There was a problem hiding this comment.
🟠 [P2] Major
DesignSystem 모듈이 ComposableArchitecture에 직접적으로 의존하는 것은 모듈 아키텍처 원칙에 위배됩니다. DesignSystem은 순수한 UI 컴포넌트와 디자인 토큰을 포함해야 하며, 특정 비즈니스 로직 프레임워크(TCA)를 알아서는 안 됩니다. TCA 관련 로직(CustomAlertState, CustomAlertAction, CustomConfirmAlert 리듀서, customAlert ViewModifier 등)은 Presentation 계층의 Chat 모듈이나 Shared/Presentation과 같은 별도의 모듈로 분리되어야 합니다.
DesignSystem의 알림 팝업(CustomConfirmationPopup)은 Bool 바인딩과 클로저 기반의 onConfirm, onCancel 액션을 통해 프레임워크에 독립적으로 동작하도록 설계하는 것이 좋습니다. 이렇게 하면 DesignSystem 모듈의 재사용성과 아키텍처적 무결성을 유지할 수 있습니다.
| .SPM.composableArchitecture, | |
| // Projects/Shared/DesignSystem/Project.swift 에서 | |
| // .SPM.composableArchitecture 라인 제거 | |
| // TCA 관련 알림 로직 (CustomAlertState, CustomAlertAction, CustomConfirmAlert, customAlert ViewModifier)을 | |
| // Presentation 계층의 적절한 모듈로 이동 |
Summary
/api/v1/battles/{id}+ 시나리오/api/v1/battles/{id}/scenario+ 사전 투표/api/v1/battles/{id}/votes/preAPI 전체 연동, Poll 도메인 전면 제거AudioPlayerInterfaceDomain /AudioPlayerRepositoryImplData) — 채팅방 진입 시 시나리오 mp3 자동 로드, 재생 진행도와 메시지 자동 스크롤 동기화ChatRoom+PreVote를 신규Projects/Presentation/Chat모듈로 분리,ChatCoordinator가 PreVote → ChatRoom 흐름 자체 라우팅 (오늘의 배틀 탭 등 다른 진입점에서 재사용 가능)#접두사 일관 적용return명시 규칙 확장, Coordinatorswiftformat:disable extensionAccessControl가이드,SwiftUI Expert Skill사용 안내 추가Test plan
Closes #4
Refs #20