Skip to content

feat: 빠른배틀(오늘의 배틀) — API 연동 + 화면/세로페이저/공유/ChatRoom 진입 + 댓글 진영 버그 수정#38

Merged
Roy-wonji merged 22 commits into
developfrom
feature/battle
Jun 6, 2026
Merged

feat: 빠른배틀(오늘의 배틀) — API 연동 + 화면/세로페이저/공유/ChatRoom 진입 + 댓글 진영 버그 수정#38
Roy-wonji merged 22 commits into
developfrom
feature/battle

Conversation

@Roy-wonji

Copy link
Copy Markdown
Contributor

요약

빠른배틀 탭(오늘의 배틀) 신규 화면 + /battles/today API 연동, 댓글 진영 버그 수정.

작업 내용

오늘의 배틀 API (GET /api/v1/battles/today)

  • Service/API(today) → DTO/Mapper(TodayBattlePageDataDTO, BattleInfoDTO 재사용) → Entity(TodayBattlePage) → Interface/UseCase/Repository(fetchTodayBattles)
  • 옵션 응답에 tags 누락 → BattleOptionDTO.tags 옵셔널화 (디코딩 에러 수정)

오늘의 배틀 화면 (picke.pen 매칭)

  • 배경 이미지 + 그라데이션 + 태그/제목/질문/소요시간 + 세로 VS 선택지 + "배틀 입장하기"
  • 오늘의 배틀 여러 건이면 세로 스크롤 페이징
  • 옵션 선택(골드 강조) / 미선택 시 입장 버튼 비활성 / 입장(투표) 후 복귀 시 옵션 비활성
  • 상단 공유 → 시스템 공유 시트(현재 보이는 배틀, 데이터 없으면 숨김)
  • "배틀 입장하기" → ChatRoom 직접 진입 → 다 들으면 ChatCoordinator(PreVote) 연결
  • 다크 스켈레톤 / 빈 상태("오늘의 배틀이 없습니다")

공용화 / 네비

  • ShareItem/ShareContentEntity 모듈 이동(공용), 공유 시트 높이 50% 통일
  • 메인탭: 빠른배틀 백탭 시 직전 탭으로 복귀(previousTab)

댓글 진영 버그 수정 (Comment)

  • 등록: vote-stats optionId가 0이면 배틀상세 값 유지 → 투표 진영으로 잘못 등록되던 버그 수정
  • 표시: .a/.b 를 label 문자열이 아닌 optionId 로 판별 → 반대 진영 표시 버그 수정
  • 옵션 탭 → 해당 진영 / 전체 탭 → 최종 투표 진영(myOptionId)

Refs #8

Roy-wonji added 19 commits June 5, 2026 12:10
@Roy-wonji Roy-wonji added ✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업 🐛 버그 버그 수정 labels Jun 6, 2026
@Roy-wonji Roy-wonji mentioned this pull request Jun 6, 2026
3 tasks

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 빠른배틀(오늘의 배틀) 기능 구현 및 기존 댓글 진영 버그 수정이 잘 이루어졌습니다. TCA 컨벤션과 SwiftUI/Swift 코드 품질은 높은 수준으로 유지되고 있습니다. 다만, Domain/Entity 모듈이 Presentation 레이어에 속해야 할 로직과 엔티티를 포함하고 있는 부분이 있어 모듈 아키텍처 원칙에 따라 개선이 필요합니다.

// DailyBattle.swift
// Entity
//
// 오늘의 배틀 화면 모델 — picke.pen `오늘의 배틀`. `BattleInfo` 에서 매핑.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

DailyBattle 엔티티는 주석에 명시된 대로 "오늘의 배틀 화면 모델"이며, BattleInfo 엔티티를 화면에 맞게 변환하는 static func from(_ info: BattleInfo) -> DailyBattle 메서드를 포함하고 있습니다. 이러한 화면 특화된 모델과 그 매핑 로직은 Presentation/Battle/Model과 같은 프리젠테이션 레이어에 위치해야 합니다. 현재 Domain/Entity 모듈에 정의되어 있어, Domain 레이어가 Presentation 레이어의 세부 사항에 의존하는 모듈 아키텍처 위반입니다.

Suggested change
// 오늘의 배틀 화면 모델 — picke.pen `오늘의 배틀`. `BattleInfo` 에서 매핑.
// DailyBattle 엔티티와 해당 `from` 매핑 메서드를 Projects/Presentation/Battle/Sources/Main/Model/DailyBattle.swift (신규 생성) 등 Presentation 레이어로 이동시켜야 합니다.
// 그리고 BattleFeature Reducer에서 해당 매핑을 수행해야 합니다.
// Projects/Presentation/Battle/Sources/Main/Model/DailyBattle.swift (예시)
import Entity // BattleInfo 사용을 위해 필요
public struct DailyBattle: Equatable, Identifiable { /* ... */ }
public extension DailyBattle {
static func from(_ info: BattleInfo) -> DailyBattle { /* ... */ }
}
// Projects/Presentation/Battle/Sources/Main/Reducer/BattleFeature.swift 내에서 사용 예시:
// state.battles = page.items.map(DailyBattle.from)


public extension BattleScenario {
/// 화면에 노출할 노드들. `visibleNodeIds` 우선, 비어있으면 현재/시작 노드 폴백.
func chatVisibleNodes(visibleNodeIds: [Int], currentNodeId: Int?) -> [ScenarioNode] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

BattleScenario 엔티티에 chatVisibleNodes와 같이 특정 프리젠테이션(ChatRoomFeature)에 특화된 로직을 확장하는 것은 모듈 아키텍처 원칙(Presentation → Domain)에 위배됩니다. 도메인 엔티티는 특정 UI나 프리젠테이션 레이어에 종속되지 않는 순수한 비즈니스 로직과 데이터 구조를 포함해야 합니다. 이 로직은 ChatRoomFeature 내의 private 메서드 또는 Presentation/Chat 모듈 내의 별도 매퍼/빌더에서 구현되어야 합니다.

Suggested change
func chatVisibleNodes(visibleNodeIds: [Int], currentNodeId: Int?) -> [ScenarioNode] {
// 이 로직은 BattleScenario 엔티티의 확장 기능에서 제거하고 Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift 내의 private 메서드로 옮겨야 합니다.
// (이 PR에서 BattleScenario+.swift로 이동되기 전의 ChatRoomFeature에 있던 로직을 복원하는 것이 바람직합니다.)
// Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift 내 추가 예시:
private extension ChatRoomFeature.State {
func visibleNodes(in scenario: BattleScenario) -> [ScenarioNode] {
let ids = visibleNodeIds.isEmpty ? [currentNodeId ?? scenario.startNodeId] : visibleNodeIds
return ids.compactMap { id in
scenario.nodes.first { $0.nodeId == id }
}
}
}

}

/// 대사(script) → 화면 발화자(ChatSpeaker) 매핑.
func chatSpeaker(for script: ScenarioScript) -> ChatSpeaker {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [P2] Major

chatSpeaker와 같이 Chat 프리젠테이션 레이어의 특정 뷰 모델/엔티티(ChatSpeakerPresentation/Chat 모듈에 정의됨)를 생성하는 로직이 Domain/Entity 모듈에 위치하는 것은 모듈 의존성 역전을 야기하며 아키텍처 원칙에 위배됩니다. Domain 레이어는 Presentation 레이어를 알지 못해야 합니다. 이 매핑 로직은 ChatRoomFeature 내의 private 메서드 또는 Presentation/Chat 모듈 내의 별도 매퍼/빌더에서 구현되어야 합니다.

Suggested change
func chatSpeaker(for script: ScenarioScript) -> ChatSpeaker {
// 이 로직은 BattleScenario 엔티티의 확장 기능에서 제거하고 Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift 내의 private 메서드로 옮겨야 합니다.
// (이 PR에서 BattleScenario+.swift로 이동되기 전의 ChatRoomFeature에 있던 로직을 복원하는 것이 바람직합니다.)
// Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift 내 추가 예시:
private extension ChatRoomFeature.State {
func speaker(for script: ScenarioScript, in scenario: BattleScenario) -> ChatSpeaker {
switch script.speakerType {
case .a:
return speaker(label: "A", side: .left, fallbackName: script.speakerName, in: scenario)
case .b:
return speaker(label: "B", side: .right, fallbackName: script.speakerName, in: scenario)
case .narrator:
return ChatSpeaker(name: script.speakerName, side: .center)
case .philosopher, .unknown:
if let philosopher = scenario.philosophers.first(where: { $0.name == script.speakerName }) {
let side: ChatSpeakerSide = philosopher.label == "B" ? .right : .left
return ChatSpeaker(
label: philosopher.label,
name: philosopher.name,
imageURL: philosopher.imageUrl,
side: side
)
}
return ChatSpeaker(name: script.speakerName, side: .center)
}
}
private func speaker(
label: String,
side: ChatSpeakerSide,
fallbackName: String,
in scenario: BattleScenario
) -> ChatSpeaker {
guard let philosopher = scenario.philosophers.first(where: { $0.label == label }) else {
return ChatSpeaker(label: label, name: fallbackName, side: side)
}
return ChatSpeaker(
label: philosopher.label,
name: philosopher.name,
imageURL: philosopher.imageUrl,
side: side
)
}
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f7ec083f6

ℹ️ 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".

Comment on lines +76 to +77
case let .routeAction(_, action: .chatRoom(.delegate(.requestFinalVote(battleId)))):
state.routes.push(.chat(.init(battleId: battleId)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route final vote into the post-vote screen

When a user enters a quick battle, finishes the ChatRoom, and accepts the final-vote prompt, this pushes a fresh ChatCoordinator.State(battleId:); that initializer roots the flow at PreVoteFeature with the default pre-vote mode, whereas the normal ChatCoordinator chat-room path pushes .preVote(.init(battleId: battleId, voteMode: .post)). In this quick-battle path the user is sent back to the pre-vote screen, and submitting it opens another ChatRoom instead of submitting the final vote/reaching comments.

Useful? React with 👍 / 👎.

Comment on lines +93 to +99
Text(tab.title)
.pretendardFont(family: .Medium, size: 12)
.foregroundStyle(.gray200)
} icon: {
tabIcon(for: tab)
}
.foregroundStyle(.gray200)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't pin selected tab labels to the inactive color

Because the custom tab label always applies .foregroundStyle(.gray200) to the Label, the currently selected tab is rendered with the same inactive color even though the router is tinted to .neutral900. This affects every tab after the custom label is used, so users lose the visual indication of which tab is active; let the selected tint/appearance drive the color instead of hard-coding gray here.

Useful? React with 👍 / 👎.

Roy-wonji added 3 commits June 6, 2026 13:51
오늘의 배틀 화면 모델과 채팅방 전용 시나리오 매핑을 Presentation 레이어로 되돌려 Domain 의존 방향을 유지한다.

Rejected: BattleScenario Entity 확장에 ChatSpeaker 매핑 유지 | Presentation 타입 의존이 생김
vote-stats 응답 배열 순서가 배틀 상세 옵션 순서와 달라도 상세에서 채운 optionId 기준으로 A/B 요약을 유지한다.
커스텀 탭 Label에서 gray200을 고정하지 않고 선택/미선택 색상은 UITabBarAppearance와 tint 설정을 따르게 한다.
@Roy-wonji Roy-wonji merged commit d90cad6 into develop Jun 6, 2026
2 of 3 checks passed
@Roy-wonji Roy-wonji deleted the feature/battle branch June 6, 2026 04:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27df5037b6

ℹ️ 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".

Comment on lines +136 to +140
case let .enterBattleTapped(battleId):
// 선택해야만 입장 가능. 입장 시 투표 확정 → 복귀 시 옵션 변경 비활성화.
guard state.selectedOptionByBattle[battleId] != nil else { return .none }
state.votedBattleIds.insert(battleId)
return .send(.delegate(.openBattle(battleId: battleId)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Submit the quick-battle vote before entering

When a user picks an option and taps “배틀 입장하기”, this marks the battle as voted locally and navigates to ChatRoom, but the selected optionId is never sent to submitPreVote or passed to the chat flow. In the quick-battle path, the server therefore has no pre-vote/perspective for the user even though the UI has disabled further option changes, so later comment/final-vote state that depends on the user's selected side is wrong.

Useful? React with 👍 / 👎.

Comment on lines +534 to +538
let optionAId = state.voteSummary.optionA.optionId
let mapped = page.items.enumerated().map { idx, item -> CommentItem in
var comment = CommentItem(item: item, order: idx)
if optionAId > 0 {
comment.option = item.option.optionId == optionAId ? .a : .b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-map comments after option ids are available

This only fixes the A/B side when voteSummary.optionA.optionId is already populated, but onAppear starts fetchBattle, fetchVoteStats, and fetchPerspectives concurrently. If the perspectives response arrives first, optionAId is still the default 0, so comments keep the old label-based fallback; for the non-A/B labels this patch is addressing, the list and filters remain misclassified until another fetch happens. Gate the perspectives fetch on the battle/options response or re-map/refetch once the option ids arrive.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업 🐛 버그 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant