Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bf34ab5
feat: 채팅방 대사 오디오 재생시간 싱크 — 목소리 나올 때 대화창 한 줄씩 노출 #4
Roy-wonji Jun 5, 2026
1ada094
feat: 채팅방 문장별 말풍선 음성 싱크(글자수 비례) + 재생중 버블 강조/파형 + 출렁임 수정 #4
Roy-wonji Jun 5, 2026
7a1264a
refactor: 문장 분할 유틸 Utill(String+Sentence) 공통화 + AGENTS.md Extension 네…
Roy-wonji Jun 5, 2026
951df58
refactor: ChatRoom 순수 로직 분리 — UUID.deterministic(Utill) + BattleScena…
Roy-wonji Jun 5, 2026
7f4898b
refactor: Extension 파일명 Type+.swift 로 통일(Int+/String+/UUID+/BattleSce…
Roy-wonji Jun 5, 2026
ac61a33
feat: 스플래시 — 저장된 인증 여부로 MainTab/Auth 분기 활성화 #4
Roy-wonji Jun 5, 2026
c1d0cb5
feat: Battle 모듈 스캐폴딩(BattleCoordinator/BattleFeature/View) + MainTab …
Roy-wonji Jun 5, 2026
328d983
feat: DesignSystem — vs(versus_home) 에셋 + UIColor.gray200 토큰 추가 / 온보딩…
Roy-wonji Jun 5, 2026
c2adde4
design: 홈 — Best 배틀 카드 재디자인(구분선/VS 칩/우측 메타) + Hero·NewBattle VS 이미지화 #4
Roy-wonji Jun 5, 2026
9ccad7f
design: 탐색 — picke.pen 기준 카테고리 탭/sort 칩/배경/말줄임 정리 #4
Roy-wonji Jun 5, 2026
0c837c9
design: 채팅방 나레이션 문장단위·이탤릭/말풍선 폭 + 사전투표 간격 동적화 #4
Roy-wonji Jun 5, 2026
80ed0ef
design: 메인탭 — 미선택 탭 gray200 적용(아이콘 template 렌더링) #4
Roy-wonji Jun 5, 2026
3b43d31
design: 메인탭 — 미선택 gray200 기본색 + 선택 탭 .tint(neutral900) 제어 #4
Roy-wonji Jun 5, 2026
47d9c6a
feat: 오늘의 배틀 API(/battles/today) 연동 — Service/DTO/Mapper/Entity/UseCa…
Roy-wonji Jun 6, 2026
bfd6b3e
feat: 빠른배틀(오늘의 배틀) 화면 — 세로 페이저/옵션 선택·비활성/공유/ChatRoom 진입, picke.pen 매칭 #8
Roy-wonji Jun 6, 2026
31c2d1e
fix: 댓글 등록 진영 — vote-stats optionId 0 시 배틀상세 값 유지(투표 진영으로 잘못 등록되던 버그)
Roy-wonji Jun 6, 2026
01f8ff1
feat: 메인탭 — 빠른배틀 백탭 시 직전 탭으로 복귀(previousTab 추적)
Roy-wonji Jun 6, 2026
a5f9e2c
refactor: ShareItem/ShareContent Entity 모듈 추가(공용) + 공유 시트 50% 통일
Roy-wonji Jun 6, 2026
3f7ec08
fix: 댓글 진영 표시 — optionId 비교로 .a/.b 판별(서버 label 의존 제거, 반대 진영 표시 버그)
Roy-wonji Jun 6, 2026
7ed633b
refactor: 빠른배틀 화면 모델을 Presentation으로 이동 #8
Roy-wonji Jun 6, 2026
7a65491
fix: 댓글 통계 순서와 무관하게 진영을 매칭 #8
Roy-wonji Jun 6, 2026
27df503
fix: 선택 탭 색상을 탭바 appearance가 결정하게 수정 #8
Roy-wonji Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,39 @@ public struct PreVoteFeature {
- 파일 위치: `Projects/Presentation/<모듈>/Sources/<도메인>/Model/<TypeName>.swift`
- 레퍼런스: `Projects/Presentation/Chat/Sources/Vote/Model/ShareItem.swift`, `Projects/Presentation/Chat/Sources/Vote/Model/ShareContent.swift`

#### 🧰 공통 유틸 — `Utill` 모듈 + `Type+.swift` 네이밍 (필수)

도메인/화면에 종속되지 않는 **순수 유틸** (숫자 포맷, 문자열 분할, 날짜 변환 등) 은 Feature 안에 private helper 로 두지 말고 **`Shared/Utill` 모듈에 타입 확장**으로 분리한다. 여러 모듈(Chat / Home / Auth / Hifi 등)에서 공용으로 쓴다.

```swift
// ✅ 올바른 패턴 — Utill 모듈의 타입 확장. 파일명은 `Type+.swift` (기능 접미사 없음, 한 타입당 한 파일)
// Projects/Shared/Utill/Sources/Extension/Int+.swift
public extension Int {
var decimalFormatted: String { ... } // 1340 → "1,340"
}

// Projects/Shared/Utill/Sources/Extension/String+.swift
public extension String {
func splitIntoSentences() -> [String] { ... }
}

// 사용처: import Utill 후 그대로
import Utill
let count = likeCount.decimalFormatted
let lines = script.text.splitIntoSentences()

// ❌ 금지 — Feature/View 안에 private static helper 로 중복 선언
private static func formattedCount(_ n: Int) -> String { ... } // ← Utill 로 이동
private static func splitSentences(_ t: String) -> [String] { ... } // ← Utill 로 이동
```

규칙:
- **Extension 파일 네이밍은 항상 `Type+.swift`** — 확장 대상 타입 + `+` 만 (기능 접미사 없음). 한 타입 확장은 한 파일에 모은다 (`Int+.swift`, `String+.swift`, `UUID+.swift`, `Color+.swift`, `View+.swift`). 기존 `ShapeStyle+.swift`, `UIColor+.swift` 와 동일 규칙.
- 순수 유틸 (Foundation 만 의존, UI/도메인 무관) → `Projects/Shared/Utill/Sources/Extension/` 에 둔다
- 쓰는 모듈은 `.Shared(implements: .Utill)` 의존성 추가 후 `import Utill`
- DesignSystem 전용 확장(컬러/폰트/모디파이어)은 DesignSystem 모듈에 두되 파일명은 동일하게 `Type+.swift`
- 레퍼런스: `Projects/Shared/Utill/Sources/Extension/Int+.swift`, `String+.swift`, `UUID+.swift`

#### ⚡ AsyncAction — `Result { try await }` + `mapError` + 단일 `Response` Inner 액션

`do/catch + 별도 Loaded / Failed 액션` 분리하지 말고, `Result` 로 감싸서 단일 `xxxResponse(Result<Success, AuthError>)` Inner 액션으로 보낸다. State 캡쳐는 `[키 = state.xxx]` 형태.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public extension ModulePath {
case MainTab
case Home
case Chat

case Hifi
case Web
case Battle

public static let name: String = "Presentation"

case Hifi
case Web
}
}

Expand Down
2 changes: 1 addition & 1 deletion Projects/App/Sources/Reducer/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public struct AppReducer: Sendable {
private func handleScopeNavigation(action: ScopeAction) -> Effect<Action> {
switch action {
case .splash(.view(.onAppear)):
return .send(.view(.presentAuth))
return .none

case .splash(.delegate(.presentAuth)):
return .run { send in
Expand Down
3 changes: 3 additions & 0 deletions Projects/Data/API/Sources/Battle/BattleAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Foundation

public enum BattleAPI {
case today
case detail(battleId: Int)
case preVote(battleId: Int)
case postVote(battleId: Int)
Expand All @@ -17,6 +18,8 @@ public enum BattleAPI {

public var description: String {
switch self {
case .today:
return "today"
case let .detail(battleId):
return "\(battleId)"
case let .preVote(battleId):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public struct BattleOptionDTO: Decodable {
public let stance: String
public let representative: String
public let imageUrl: String
public let tags: [BattleTagDTO]
// today 배틀 옵션 응답엔 tags 가 없으므로 옵셔널.
public let tags: [BattleTagDTO]?
}

public struct BattleTagDTO: Decodable {
Expand Down
15 changes: 15 additions & 0 deletions Projects/Data/Model/Sources/Battle/DTO/TodayBattleDataDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// TodayBattleDataDTO.swift
// Model
//
// `GET /api/v1/battles/today` 응답 DTO. 아이템은 BattleInfoDTO 재사용.
//

import Foundation

public struct TodayBattlePageDataDTO: Decodable {
public let items: [BattleInfoDTO]
public let totalCount: Int
}

public typealias TodayBattlePageResponseDTO = BaseResponseDTO<TodayBattlePageDataDTO>
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public extension BattleOptionDTO {
stance: stance,
representative: representative,
imageUrl: imageUrl,
tags: tags.map { $0.toDomain() }
tags: (tags ?? []).map { $0.toDomain() }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// TodayBattleDataDTO+.swift
// Model
//

import Entity
import Foundation

public extension TodayBattlePageDataDTO {
func toDomain() -> TodayBattlePage {
TodayBattlePage(
items: items.map { $0.toDomain() },
totalCount: totalCount
)
}
}
12 changes: 12 additions & 0 deletions Projects/Data/Repository/Sources/Battle/BattleRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ public final class BattleRepositoryImpl: BattleInterface, @unchecked Sendable {
self.provider = provider
}

public func fetchTodayBattles() async throws -> TodayBattlePage {
let dto: TodayBattlePageResponseDTO = try await provider.request(.today)

guard let data = dto.data else {
let message = dto.error?.message ?? "오늘의 배틀 응답이 비어 있습니다"
Log.error("[BattleRepositoryImpl] empty todayBattles payload: \(message)")
throw BattleError.backendError(message)
}

return data.toDomain()
}

public func fetchBattle(battleId: Int) async throws -> BattleDetail {
let dto: BattleDetailResponseDTO = try await provider.request(
.detail(battleId: battleId)
Expand Down
7 changes: 6 additions & 1 deletion Projects/Data/Service/Sources/Battle/BattleService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Foundations
import AsyncMoya

public enum BattleService {
case today
case detail(battleId: Int)
case preVote(battleId: Int, body: PreVoteRequest)
case postVote(battleId: Int, body: PreVoteRequest)
Expand All @@ -29,6 +30,8 @@ extension BattleService: BaseTargetType {

public var urlPath: String {
switch self {
case .today:
return BattleAPI.today.description
case let .detail(battleId):
return BattleAPI.detail(battleId: battleId).description
case let .preVote(battleId, _):
Expand All @@ -54,7 +57,7 @@ extension BattleService: BaseTargetType {

public var method: Moya.Method {
switch self {
case .detail, .scenario, .voteStats, .perspectives, .myPerspective, .recommendations:
case .today, .detail, .scenario, .voteStats, .perspectives, .myPerspective, .recommendations:
return .get
case .preVote, .postVote, .createPerspective:
return .post
Expand All @@ -63,6 +66,8 @@ extension BattleService: BaseTargetType {

public var parameters: [String: Any]? {
switch self {
case .today:
return nil
case .detail:
return nil
case let .preVote(_, body):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Foundation
import WeaveDI

public protocol BattleInterface: Sendable {
func fetchTodayBattles() async throws -> TodayBattlePage
func fetchBattle(battleId: Int) async throws -> BattleDetail
func submitPreVote(
battleId: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import Foundation
public struct DefaultBattleRepositoryImpl: BattleInterface {
public init() {}

public func fetchTodayBattles() async throws -> TodayBattlePage {
TodayBattlePage(items: [], totalCount: 0)
}

public func fetchBattle(battleId _: Int) async throws -> BattleDetail {
BattleDetail(
battleInfo: BattleInfo(
Expand Down
21 changes: 21 additions & 0 deletions Projects/Domain/Entity/Sources/Battle/TodayBattlePage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// TodayBattlePage.swift
// Entity
//
// `GET /api/v1/battles/today` 응답 도메인 모델. 아이템은 BattleInfo 재사용.
//

import Foundation

public struct TodayBattlePage: Equatable {
public let items: [BattleInfo]
public let totalCount: Int

public init(
items: [BattleInfo],
totalCount: Int
) {
self.items = items
self.totalCount = totalCount
}
}
22 changes: 22 additions & 0 deletions Projects/Domain/Entity/Sources/Home/Scenario/BattleScenario+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// BattleScenario+Timeline.swift
// Entity
//
// 시나리오 타임라인 계산.
//

import Foundation

public extension BattleScenario {
/// 노드 시작 시간(초) = 노드 내 대사 startTimeMs 최소값.
func nodeStartTime(for nodeId: Int) -> TimeInterval {
guard let node = nodes.first(where: { $0.nodeId == nodeId }) else { return 0 }
return TimeInterval(node.scripts.map(\.startTimeMs).min() ?? 0) / 1000
}

/// 노드 종료 시간(초) = 노드 시작 + audioDuration.
func nodeEndTime(for node: ScenarioNode) -> TimeInterval {
let start = TimeInterval(node.scripts.map(\.startTimeMs).min() ?? 0) / 1000
return start + TimeInterval(node.audioDuration)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//
// ShareContent.swift
// Chat
// Entity
//
// 공유 시트에 실어 보낼 컨텐츠 묶음. 카드 스냅샷(우선) + 썸네일(fallback) + 본문 텍스트 + URL.
// (PreVote / 오늘의 배틀 등 공용)
//

import Foundation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//
// ShareItem.swift
// Chat
// Entity
//
// 공유 시트 트리거. `.sheet(item:)` 에 바로 바인딩한다.
// 공유 시트 트리거. `.sheet(item:)` 에 바로 바인딩한다. (PreVote / 오늘의 배틀 등 공용)
//

import Foundation
Expand Down
4 changes: 4 additions & 0 deletions Projects/Domain/UseCase/Sources/Battle/BattleUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public struct BattleUseCaseImpl: BattleInterface {

public init() {}

public func fetchTodayBattles() async throws -> TodayBattlePage {
return try await battleRepository.fetchTodayBattles()
}

public func fetchBattle(battleId: Int) async throws -> BattleDetail {
return try await battleRepository.fetchBattle(battleId: battleId)
}
Expand Down
20 changes: 20 additions & 0 deletions Projects/Presentation/Battle/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import DependencyPackagePlugin
import DependencyPlugin
import Foundation
import ProjectDescription
import ProjectTemplatePlugin

let project = Project.makeAppModule(
name: "Battle",
bundleId: .appBundleID(name: ".Battle"),
product: .staticFramework,
settings: .settings(),
dependencies: [
.Presentation(implements: .Chat),
.Shared(implements: .Shared),
.Domain(implements: .UseCase),
.SPM.composableArchitecture,
.SPM.tcaFlow,
],
sources: ["Sources/**"]
)
Loading
Loading