diff --git a/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCaseProtocol.swift b/Domain/Sources/Entity/Settlement/SettlementCalculation.swift
similarity index 78%
rename from Domain/Sources/UseCase/Settlement/CalculateSettlementUseCaseProtocol.swift
rename to Domain/Sources/Entity/Settlement/SettlementCalculation.swift
index 80a0fa9c..983431ef 100644
--- a/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCaseProtocol.swift
+++ b/Domain/Sources/Entity/Settlement/SettlementCalculation.swift
@@ -1,5 +1,5 @@
//
-// CalculateSettlementUseCaseProtocol.swift
+// SettlementCalculation.swift
// Domain
//
// Created by 홍석현 on 12/10/25.
@@ -14,9 +14,9 @@ public struct SettlementCalculation: Equatable {
public let totalPersonCount: Int // 인원수
public let averagePerPerson: Double // 1인 평균 지출
public let myNetBalance: Double // 내 순 차액 (Pay - Owe)
- public let memberBalances: [String: Double] // 각 멤버의 순 차액
public let paymentsToMake: [PaymentInfo] // 지급 예정 금액
public let paymentsToReceive: [PaymentInfo] // 수령 예정 금액
+ public let memberDetails: [MemberSettlementDetail] // 멤버별 정산 상세
public init(
totalExpenseAmount: Double,
@@ -24,18 +24,18 @@ public struct SettlementCalculation: Equatable {
totalPersonCount: Int,
averagePerPerson: Double,
myNetBalance: Double,
- memberBalances: [String: Double],
paymentsToMake: [PaymentInfo],
- paymentsToReceive: [PaymentInfo]
+ paymentsToReceive: [PaymentInfo],
+ memberDetails: [MemberSettlementDetail]
) {
self.totalExpenseAmount = totalExpenseAmount
self.myShareAmount = myShareAmount
self.totalPersonCount = totalPersonCount
self.averagePerPerson = averagePerPerson
self.myNetBalance = myNetBalance
- self.memberBalances = memberBalances
self.paymentsToMake = paymentsToMake
self.paymentsToReceive = paymentsToReceive
+ self.memberDetails = memberDetails
}
}
@@ -58,11 +58,3 @@ public struct PaymentInfo: Equatable, Identifiable {
self.amount = amount
}
}
-
-public protocol CalculateSettlementUseCaseProtocol {
- func execute(
- expenses: [Expense],
- members: [TravelMember],
- currentUserId: String?
- ) -> SettlementCalculation
-}
diff --git a/Domain/Sources/Entity/Settlement/SettlementDetail.swift b/Domain/Sources/Entity/Settlement/SettlementDetail.swift
new file mode 100644
index 00000000..3b862e73
--- /dev/null
+++ b/Domain/Sources/Entity/Settlement/SettlementDetail.swift
@@ -0,0 +1,66 @@
+//
+// SettlementDetail.swift
+// Domain
+//
+// Created by SseuDam on 2025.
+//
+
+import Foundation
+
+// MARK: - Member Settlement Detail
+public struct MemberSettlementDetail: Equatable, Identifiable {
+ public let id: String
+ public let memberId: String
+ public let memberName: String
+ public let netBalance: Double
+ public let totalPaid: Double
+ public let totalOwe: Double
+ public let paidExpenses: [ExpenseDetail]
+ public let sharedExpenses: [ExpenseDetail]
+
+ public init(
+ id: String,
+ memberId: String,
+ memberName: String,
+ netBalance: Double,
+ totalPaid: Double,
+ totalOwe: Double,
+ paidExpenses: [ExpenseDetail],
+ sharedExpenses: [ExpenseDetail]
+ ) {
+ self.id = id
+ self.memberId = memberId
+ self.memberName = memberName
+ self.netBalance = netBalance
+ self.totalPaid = totalPaid
+ self.totalOwe = totalOwe
+ self.paidExpenses = paidExpenses
+ self.sharedExpenses = sharedExpenses
+ }
+}
+
+// MARK: - Expense Detail
+public struct ExpenseDetail: Equatable, Identifiable {
+ public let id: String
+ public let title: String
+ public let amount: Double
+ public let shareAmount: Double
+ public let participantCount: Int
+ public let expenseDate: Date
+
+ public init(
+ id: String,
+ title: String,
+ amount: Double,
+ shareAmount: Double,
+ participantCount: Int,
+ expenseDate: Date
+ ) {
+ self.id = id
+ self.title = title
+ self.amount = amount
+ self.shareAmount = shareAmount
+ self.participantCount = participantCount
+ self.expenseDate = expenseDate
+ }
+}
diff --git a/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCase.swift b/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCase.swift
index 0099b407..e986ec03 100644
--- a/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCase.swift
+++ b/Domain/Sources/UseCase/Settlement/CalculateSettlementUseCase.swift
@@ -8,19 +8,27 @@
import Foundation
import ComposableArchitecture
-public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
+public protocol CalculateSettlementUseCaseProtocol {
+ func execute(
+ expenses: [Expense],
+ members: [TravelMember],
+ currentUserId: String?
+ ) -> SettlementCalculation
+}
+public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
+
public init() {}
-
+
public func execute(
expenses: [Expense],
members: [TravelMember],
currentUserId: String?
) -> SettlementCalculation {
-
+
// 1. 총 지출 금액
let totalExpenseAmount = expenses.reduce(0) { $0 + $1.convertedAmount }
-
+
// 2. 내가 부담해야 할 금액 (내가 참여한 지출들의 분담금 합계)
let myShareAmount: Double
if let userId = currentUserId {
@@ -36,40 +44,40 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
} else {
myShareAmount = 0
}
-
+
// 3. 인원수
let totalPersonCount = members.count
-
+
// 4. 1인 평균 지출
let averagePerPerson = totalPersonCount > 0 ? totalExpenseAmount / Double(totalPersonCount) : 0
-
+
// 5. 각 멤버의 순 차액 계산 (Net Balance = Pay - Owe)
var memberBalances: [String: Double] = [:]
-
+
// 모든 멤버 초기화
for member in members {
memberBalances[member.id] = 0.0
}
-
+
// 각 지출에 대해 계산
for expense in expenses {
let participantCount = Double(expense.participants.count)
guard participantCount > 0 else { continue }
-
+
let amountPerPerson = expense.convertedAmount / participantCount
-
+
// 결제자는 전체 금액을 지불한 것으로 (+)
memberBalances[expense.payerId, default: 0] += expense.convertedAmount
-
+
// 참여자들은 각자 분담금을 빚진 것으로 (-)
for participant in expense.participants {
memberBalances[participant.id, default: 0] -= amountPerPerson
}
}
-
+
// 6. 내 순 차액
let myNetBalance = currentUserId.flatMap { memberBalances[$0] } ?? 0
-
+
// 7. 지급 예정 금액 계산
let paymentsToMake = calculatePaymentsToMake(
myBalance: myNetBalance,
@@ -77,7 +85,7 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
members: members,
currentUserId: currentUserId
)
-
+
// 8. 수령 예정 금액 계산
let paymentsToReceive = calculatePaymentsToReceive(
myBalance: myNetBalance,
@@ -85,21 +93,28 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
members: members,
currentUserId: currentUserId
)
-
+
+ // 9. 멤버별 정산 상세 계산
+ let memberDetails = calculateMemberDetails(
+ expenses: expenses,
+ members: members,
+ memberBalances: memberBalances
+ )
+
return SettlementCalculation(
totalExpenseAmount: totalExpenseAmount,
myShareAmount: myShareAmount,
totalPersonCount: totalPersonCount,
averagePerPerson: averagePerPerson,
myNetBalance: myNetBalance,
- memberBalances: memberBalances,
paymentsToMake: paymentsToMake,
- paymentsToReceive: paymentsToReceive
+ paymentsToReceive: paymentsToReceive,
+ memberDetails: memberDetails
)
}
-
+
// MARK: - Private Helper Methods
-
+
// 지급 예정 금액 (내가 빚진 사람들에게 갚아야 할 돈)
private func calculatePaymentsToMake(
myBalance: Double,
@@ -109,20 +124,20 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
) -> [PaymentInfo] {
guard currentUserId != nil else { return [] }
guard myBalance < 0 else { return [] } // 내가 받을 돈이 있으면 지급할 것이 없음
-
+
// 양수 잔액을 가진 멤버들 (받을 돈이 있는 사람들)
let creditors = memberBalances
.filter { $0.value > 0 }
.sorted { $0.value > $1.value } // 많이 받을 사람부터
-
+
var payments: [PaymentInfo] = []
var remainingDebt = abs(myBalance)
-
+
for (memberId, creditAmount) in creditors {
guard remainingDebt > 0.01 else { break } // 소수점 오차 고려
-
+
let paymentAmount = min(remainingDebt, creditAmount)
-
+
if let member = members.first(where: { $0.id == memberId }) {
payments.append(PaymentInfo(
id: UUID().uuidString,
@@ -133,10 +148,10 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
remainingDebt -= paymentAmount
}
}
-
+
return payments
}
-
+
// 수령 예정 금액 (나에게 빚진 사람들로부터 받을 돈)
private func calculatePaymentsToReceive(
myBalance: Double,
@@ -146,20 +161,20 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
) -> [PaymentInfo] {
guard currentUserId != nil else { return [] }
guard myBalance > 0 else { return [] } // 내가 빚진 돈이 있으면 받을 것이 없음
-
+
// 음수 잔액을 가진 멤버들 (빚진 사람들)
let debtors = memberBalances
.filter { $0.value < 0 }
.sorted { $0.value < $1.value } // 많이 빚진 사람부터
-
+
var receipts: [PaymentInfo] = []
var remainingCredit = myBalance
-
+
for (memberId, debtAmount) in debtors {
guard remainingCredit > 0.01 else { break } // 소수점 오차 고려
-
+
let receiptAmount = min(remainingCredit, abs(debtAmount))
-
+
if let member = members.first(where: { $0.id == memberId }) {
receipts.append(PaymentInfo(
id: UUID().uuidString,
@@ -170,17 +185,74 @@ public struct CalculateSettlementUseCase: CalculateSettlementUseCaseProtocol {
remainingCredit -= receiptAmount
}
}
-
+
return receipts
}
+
+ // 멤버별 정산 상세 계산
+ // 시간 복잡도: O(E * P + M) - E: 지출 수, P: 평균 참여자 수, M: 멤버 수
+ private func calculateMemberDetails(
+ expenses: [Expense],
+ members: [TravelMember],
+ memberBalances: [String: Double]
+ ) -> [MemberSettlementDetail] {
+ // 1. 멤버별로 결제한 지출과 참여한 지출을 그룹화
+ // O(E * P) - 지출을 한 번만 순회
+ var memberPaidExpenses: [String: [ExpenseDetail]] = [:]
+ var memberSharedExpenses: [String: [ExpenseDetail]] = [:]
+
+ for expense in expenses {
+ let participantCount = expense.participants.count
+ let shareAmount = participantCount > 0 ? expense.convertedAmount / Double(participantCount) : 0
+
+ let expenseDetail = ExpenseDetail(
+ id: expense.id,
+ title: expense.title,
+ amount: expense.convertedAmount,
+ shareAmount: shareAmount,
+ participantCount: participantCount,
+ expenseDate: expense.expenseDate
+ )
+
+ // 결제자에게 추가
+ memberPaidExpenses[expense.payerId, default: []].append(expenseDetail)
+
+ // 참여자들에게 추가
+ for participant in expense.participants {
+ memberSharedExpenses[participant.id, default: []].append(expenseDetail)
+ }
+ }
+
+ // 2. 각 멤버의 정산 상세 생성
+ // O(M)
+ return members.map { member in
+ let paidExpenses = memberPaidExpenses[member.id] ?? []
+ let sharedExpenses = memberSharedExpenses[member.id] ?? []
+
+ let totalPaid = paidExpenses.reduce(0) { $0 + $1.amount }
+ let totalOwe = sharedExpenses.reduce(0) { $0 + $1.shareAmount }
+ let netBalance = memberBalances[member.id] ?? 0
+
+ return MemberSettlementDetail(
+ id: member.id,
+ memberId: member.id,
+ memberName: member.name,
+ netBalance: netBalance,
+ totalPaid: totalPaid,
+ totalOwe: totalOwe,
+ paidExpenses: paidExpenses,
+ sharedExpenses: sharedExpenses
+ )
+ }
+ }
}
// MARK: - DependencyKey
public enum CalculateSettlementUseCaseDependencyKey: DependencyKey {
public static var liveValue: any CalculateSettlementUseCaseProtocol = CalculateSettlementUseCase()
-
+
public static var testValue: any CalculateSettlementUseCaseProtocol = CalculateSettlementUseCase()
-
+
public static var previewValue: any CalculateSettlementUseCaseProtocol = CalculateSettlementUseCase()
}
diff --git a/Domain/Tests/UseCase/Settlement/CalculateSettlementUseCaseTests.swift b/Domain/Tests/UseCase/Settlement/CalculateSettlementUseCaseTests.swift
index 7d5dea11..0ca43e27 100644
--- a/Domain/Tests/UseCase/Settlement/CalculateSettlementUseCaseTests.swift
+++ b/Domain/Tests/UseCase/Settlement/CalculateSettlementUseCaseTests.swift
@@ -43,6 +43,16 @@ struct CalculateSettlementUseCaseTests {
#expect(result.myNetBalance == 0)
#expect(result.paymentsToMake.isEmpty)
#expect(result.paymentsToReceive.isEmpty)
+
+ // memberDetails 검증
+ #expect(result.memberDetails.count == 3)
+ for detail in result.memberDetails {
+ #expect(detail.totalPaid == 0)
+ #expect(detail.totalOwe == 0)
+ #expect(detail.netBalance == 0)
+ #expect(detail.paidExpenses.isEmpty)
+ #expect(detail.sharedExpenses.isEmpty)
+ }
}
@Test("총 지출 금액 계산 - 단일 지출")
@@ -410,6 +420,33 @@ struct CalculateSettlementUseCaseTests {
// 줄 돈 없음
#expect(result.paymentsToMake.isEmpty)
+
+ // memberDetails 검증
+ #expect(result.memberDetails.count == 3)
+
+ // 나 (홍석현) - 모든 지출을 결제함
+ let myDetail = try #require(result.memberDetails.first(where: { $0.memberId == "user1" }))
+ #expect(Int(myDetail.totalPaid) == 90_000)
+ #expect(Int(myDetail.totalOwe) == 개인당지불해야할돈)
+ #expect(Int(myDetail.netBalance) == 90_000 - 개인당지불해야할돈)
+ #expect(myDetail.paidExpenses.count == 2) // 호텔, 식사
+ #expect(myDetail.sharedExpenses.count == 2) // 모든 지출에 참여
+
+ // 철수 - 아무것도 결제 안함
+ let 철수Detail = try #require(result.memberDetails.first(where: { $0.memberId == "user2" }))
+ #expect(철수Detail.totalPaid == 0)
+ #expect(Int(철수Detail.totalOwe) == 개인당지불해야할돈)
+ #expect(Int(철수Detail.netBalance) == -개인당지불해야할돈)
+ #expect(철수Detail.paidExpenses.isEmpty)
+ #expect(철수Detail.sharedExpenses.count == 2)
+
+ // 영희 - 아무것도 결제 안함
+ let 영희Detail = try #require(result.memberDetails.first(where: { $0.memberId == "user3" }))
+ #expect(영희Detail.totalPaid == 0)
+ #expect(Int(영희Detail.totalOwe) == 개인당지불해야할돈)
+ #expect(Int(영희Detail.netBalance) == -개인당지불해야할돈)
+ #expect(영희Detail.paidExpenses.isEmpty)
+ #expect(영희Detail.sharedExpenses.count == 2)
}
@Test("일부 지출에만 참여 - 참여하지 않은 지출은 제외")
@@ -467,6 +504,33 @@ struct CalculateSettlementUseCaseTests {
#expect(Int(영희에게받을돈) == 60_000)
#expect(result.paymentsToMake.isEmpty)
+
+ // memberDetails 검증
+ #expect(result.memberDetails.count == 3)
+
+ // 나 (홍석현) - 호텔만 결제, 호텔만 참여
+ let myDetail = try #require(result.memberDetails.first(where: { $0.memberId == "user1" }))
+ #expect(Int(myDetail.totalPaid) == 90_000)
+ #expect(Int(myDetail.totalOwe) == 30_000) // 90,000 / 3
+ #expect(Int(myDetail.netBalance) == 60_000)
+ #expect(myDetail.paidExpenses.count == 1) // 호텔만
+ #expect(myDetail.sharedExpenses.count == 1) // 호텔만 참여
+
+ // 철수 - 술집 결제, 호텔+술집 참여
+ let 철수Detail = try #require(result.memberDetails.first(where: { $0.memberId == "user2" }))
+ #expect(Int(철수Detail.totalPaid) == 60_000)
+ #expect(Int(철수Detail.totalOwe) == 60_000) // 30,000 + 30,000
+ #expect(Int(철수Detail.netBalance) == 0)
+ #expect(철수Detail.paidExpenses.count == 1) // 술집만
+ #expect(철수Detail.sharedExpenses.count == 2) // 호텔+술집
+
+ // 영희 - 아무것도 결제 안함, 호텔+술집 참여
+ let 영희Detail = try #require(result.memberDetails.first(where: { $0.memberId == "user3" }))
+ #expect(영희Detail.totalPaid == 0)
+ #expect(Int(영희Detail.totalOwe) == 60_000) // 30,000 + 30,000
+ #expect(Int(영희Detail.netBalance) == -60_000)
+ #expect(영희Detail.paidExpenses.isEmpty)
+ #expect(영희Detail.sharedExpenses.count == 2) // 호텔+술집
}
@Test("결제자가 여러 번 바뀌는 복잡한 시나리오")
diff --git a/Features/ExpenseList/Sources/ExpenseListView.swift b/Features/ExpenseList/Sources/ExpenseListView.swift
index ecca1629..1d47aa6e 100644
--- a/Features/ExpenseList/Sources/ExpenseListView.swift
+++ b/Features/ExpenseList/Sources/ExpenseListView.swift
@@ -38,9 +38,17 @@ public struct ExpenseListView: View {
.onTapGesture {
send(.onTapExpense(expense))
}
+ .transition(.asymmetric(
+ insertion: .scale(scale: 0.95, anchor: .top)
+ .combined(with: .opacity)
+ .combined(with: .move(edge: .top)),
+ removal: .scale(scale: 0.95, anchor: .top)
+ .combined(with: .opacity)
+ ))
}
}
.padding(.vertical, 10)
+ .animation(.spring(response: 0.35, dampingFraction: 0.75), value: store.currentExpense.count)
}
.scrollIndicators(.hidden)
} else {
diff --git a/Features/SettlementDetail/Demo/Resources/LaunchScreen.storyboard b/Features/SettlementDetail/Demo/Resources/LaunchScreen.storyboard
new file mode 100644
index 00000000..dc004f91
--- /dev/null
+++ b/Features/SettlementDetail/Demo/Resources/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Features/SettlementDetail/Demo/Sources/SettlementDetailDemoApp.swift b/Features/SettlementDetail/Demo/Sources/SettlementDetailDemoApp.swift
new file mode 100644
index 00000000..4671b3fd
--- /dev/null
+++ b/Features/SettlementDetail/Demo/Sources/SettlementDetailDemoApp.swift
@@ -0,0 +1,21 @@
+//
+// SettlementDetailDemoApp.swift
+// SseuDam
+//
+// Created by SseuDam on2025.
+// Copyright ©2025 com.testdev. All rights reserved.
+//
+
+import SwiftUI
+import SettlementDetailFeature
+
+@main
+struct SettlementDetailDemoApp: App {
+ var body: some Scene {
+ WindowGroup {
+ NavigationView {
+ SettlementDetailView()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Features/SettlementDetail/Project.swift b/Features/SettlementDetail/Project.swift
new file mode 100644
index 00000000..de645ea4
--- /dev/null
+++ b/Features/SettlementDetail/Project.swift
@@ -0,0 +1,11 @@
+import ProjectDescription
+import SseuDamPlugin
+
+let project = Project.makeFeature(
+ name: .SettlementDetail,
+ dependencies: [
+ .Domain,
+ .DesignSystem
+ ],
+ hasTests: true
+)
diff --git a/Features/SettlementDetail/Sources/Components/ExpenseBreakdownSection.swift b/Features/SettlementDetail/Sources/Components/ExpenseBreakdownSection.swift
new file mode 100644
index 00000000..6e93bba9
--- /dev/null
+++ b/Features/SettlementDetail/Sources/Components/ExpenseBreakdownSection.swift
@@ -0,0 +1,148 @@
+//
+// ExpenseBreakdownSection.swift
+// SettlementDetailFeature
+//
+// Created by SseuDam on 2025.
+//
+
+import SwiftUI
+import DesignSystem
+import Domain
+
+struct ExpenseBreakdownSection: View {
+ let title: String
+ let totalAmount: Double
+ let expenses: [ExpenseDetail]
+ let showEmpty: Bool
+
+ // 날짜 오름차순 정렬된 지출 목록
+ private var sortedExpenses: [ExpenseDetail] {
+ expenses.sorted { $0.expenseDate < $1.expenseDate }
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ // 섹션 헤더
+ HStack {
+ Text(title)
+ .font(.app(.body, weight: .semibold))
+ .foregroundStyle(Color.black)
+
+ Spacer()
+
+ Text("₩\(Int(totalAmount).formatted())")
+ .font(.app(.title3, weight: .semibold))
+ .foregroundStyle(Color.primary500)
+ }
+
+ // 지출 내역
+ if expenses.isEmpty && showEmpty {
+ Text("결제한 내역이 없습니다")
+ .font(.app(.caption1, weight: .medium))
+ .foregroundStyle(Color.gray7)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 12)
+ } else {
+ VStack(spacing: 8) {
+ ForEach(sortedExpenses) { expense in
+ ExpenseRow(expense: expense)
+ }
+ }
+ }
+ }
+ .padding(16)
+ .background(Color.primary50)
+ .cornerRadius(12)
+ }
+}
+
+#Preview("지출이 있는 경우") {
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+ let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today)!
+
+ return ExpenseBreakdownSection(
+ title: "결제한 금액",
+ totalAmount: 280000,
+ expenses: [
+ ExpenseDetail(
+ id: "1",
+ title: "호텔",
+ amount: 120000,
+ shareAmount: 40000,
+ participantCount: 3,
+ expenseDate: twoDaysAgo
+ ),
+ ExpenseDetail(id: "2", title: "저녁식사", amount: 60000, shareAmount: 30000, participantCount: 2, expenseDate: yesterday),
+ ExpenseDetail(
+ id: "3",
+ title: "커피",
+ amount: 100000,
+ shareAmount: 33333,
+ participantCount: 3,
+ expenseDate: today
+ )
+ ],
+ showEmpty: true
+ )
+ .padding(16)
+ .background(Color.white)
+}
+
+#Preview("지출이 없는 경우") {
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+ let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today)!
+
+ return ExpenseBreakdownSection(
+ title: "결제한 금액",
+ totalAmount: 0,
+ expenses: [],
+ showEmpty: true
+ )
+ .padding(16)
+ .background(Color.white)
+}
+
+#Preview("부담 금액") {
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+ let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today)!
+
+ return ExpenseBreakdownSection(
+ title: "부담 금액",
+ totalAmount: 226773,
+ expenses: [
+ ExpenseDetail(
+ id: "1",
+ title: "호텔",
+ amount: 120000,
+ shareAmount: 40000,
+ participantCount: 3,
+ expenseDate: twoDaysAgo
+ ),
+ ExpenseDetail(
+ id: "2",
+ title: "저녁식사",
+ amount: 60000,
+ shareAmount: 30000,
+ participantCount: 2,
+ expenseDate: yesterday
+ ),
+ ExpenseDetail(
+ id: "3",
+ title: "커피",
+ amount: 100000,
+ shareAmount: 33333,
+ participantCount: 3,
+ expenseDate: today
+ )
+ ],
+ showEmpty: false
+ )
+ .padding(16)
+ .background(Color.white)
+}
diff --git a/Features/SettlementDetail/Sources/Components/ExpenseRow.swift b/Features/SettlementDetail/Sources/Components/ExpenseRow.swift
new file mode 100644
index 00000000..85320439
--- /dev/null
+++ b/Features/SettlementDetail/Sources/Components/ExpenseRow.swift
@@ -0,0 +1,59 @@
+//
+// ExpenseRow.swift
+// SettlementDetailFeature
+//
+// Created by 홍석현 on 12/11/25.
+//
+
+import SwiftUI
+import Domain
+
+struct ExpenseRow: View {
+ let expense: ExpenseDetail
+
+ var body: some View {
+ HStack(spacing: 8) {
+ // 지출 제목 + 날짜
+ VStack(alignment: .leading, spacing: 4) {
+ Text(expense.title)
+ .font(.app(.body, weight: .medium))
+ .foregroundStyle(Color.black)
+
+ Text(formatDate(expense.expenseDate))
+ .font(.app(.caption2, weight: .medium))
+ .foregroundStyle(Color.gray7)
+ }
+
+ Spacer()
+
+ // 내 부담 금액
+ VStack(alignment: .trailing, spacing: 2) {
+ Text("₩\(Int(expense.shareAmount).formatted())")
+ .font(.app(.body, weight: .semibold))
+ .foregroundStyle(Color.black)
+
+ Text("(전체 ₩\(Int(expense.amount).formatted()) ÷ \(expense.participantCount))")
+ .font(.app(.caption2, weight: .medium))
+ .foregroundStyle(Color.gray7)
+ }
+ }
+ }
+
+ private func formatDate(_ date: Date) -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy.MM.dd"
+ return formatter.string(from: date)
+ }
+}
+
+
+#Preview {
+ ExpenseRow(expense: ExpenseDetail(
+ id: "3",
+ title: "커피",
+ amount: 100000,
+ shareAmount: 33333,
+ participantCount: 3,
+ expenseDate: .now
+ ))
+}
diff --git a/Features/SettlementDetail/Sources/Components/MemberDetailCard.swift b/Features/SettlementDetail/Sources/Components/MemberDetailCard.swift
new file mode 100644
index 00000000..41853ceb
--- /dev/null
+++ b/Features/SettlementDetail/Sources/Components/MemberDetailCard.swift
@@ -0,0 +1,184 @@
+//
+// MemberDetailCard.swift
+// SettlementDetailFeature
+//
+// Created by SseuDam on 2025.
+//
+
+import SwiftUI
+import DesignSystem
+import Domain
+
+struct MemberDetailCard: View {
+ let detail: MemberSettlementDetail
+ let isExpanded: Bool
+ let isCurrentUser: Bool
+ let onToggle: () -> Void
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // 헤더 (이름, 순 차액, 펼치기 버튼)
+ Button(action: {
+ withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
+ onToggle()
+ }
+ }) {
+ HStack(spacing: 12) {
+ // 프로필 아이콘
+ Image(asset: .profile)
+ .resizable()
+ .foregroundStyle(Color.primary500)
+ .frame(width: 40, height: 40)
+
+ // 이름
+ Text(detail.memberName)
+ .font(.app(.title3, weight: .semibold))
+ .foregroundStyle(Color.black)
+
+ Spacer()
+
+ // 순 차액
+ VStack(alignment: .trailing, spacing: 4) {
+ Text(netBalanceLabel)
+ .font(.app(.caption1, weight: .medium))
+ .foregroundStyle(Color.gray7)
+
+ Text(formatAmount(detail.netBalance))
+ .font(.app(.title3, weight: .semibold))
+ .foregroundStyle(netBalanceColor)
+ }
+
+ // 펼치기 아이콘
+ Image(systemName: "chevron.up")
+ .font(.system(size: 14, weight: .semibold))
+ .foregroundStyle(Color.gray7)
+ .rotationEffect(.degrees(isExpanded ? 180 : 0))
+ }
+ .padding(16)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+
+ // 상세 내용 (펼쳤을 때만 표시)
+ if isExpanded {
+ VStack(spacing: 16) {
+ Divider()
+ .background(Color.gray2)
+
+ // 결제 금액
+ ExpenseBreakdownSection(
+ title: "결제한 금액",
+ totalAmount: detail.totalPaid,
+ expenses: detail.paidExpenses,
+ showEmpty: true
+ )
+ .opacity(isExpanded ? 1 : 0)
+ .scaleEffect(isExpanded ? 1 : 0.95, anchor: .top)
+
+ // 부담 금액
+ ExpenseBreakdownSection(
+ title: "부담 금액",
+ totalAmount: detail.totalOwe,
+ expenses: detail.sharedExpenses,
+ showEmpty: false
+ )
+ .opacity(isExpanded ? 1 : 0)
+ .scaleEffect(isExpanded ? 1 : 0.95, anchor: .top)
+ }
+ .padding(.horizontal, 16)
+ .padding(.bottom, 16)
+ .transition(.asymmetric(
+ insertion: .scale(scale: 0.95, anchor: .top).combined(with: .opacity),
+ removal: .scale(scale: 0.95, anchor: .top).combined(with: .opacity)
+ ))
+ }
+ }
+ .background(Color.white)
+ .cornerRadius(16)
+ .overlay(
+ RoundedRectangle(cornerRadius: 16)
+ .stroke(Color.gray1, lineWidth: 1)
+ )
+ }
+
+ private var netBalanceLabel: String {
+ if detail.netBalance > 0 {
+ return "받을 돈"
+ } else if detail.netBalance < 0 {
+ return "줄 돈"
+ } else {
+ return "정산 완료"
+ }
+ }
+
+ private var netBalanceColor: Color {
+ if detail.netBalance > 0 {
+ return .primary500
+ } else if detail.netBalance < 0 {
+ return .red
+ } else {
+ return .black
+ }
+ }
+
+ private func formatAmount(_ amount: Double) -> String {
+ let absAmount = abs(amount)
+ return "₩\(Int(absAmount).formatted())"
+ }
+}
+
+#Preview {
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+ let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today)!
+
+ return VStack(spacing: 16) {
+ // 받을 돈이 있는 경우
+ MemberDetailCard(
+ detail: MemberSettlementDetail(
+ id: "1",
+ memberId: "user1",
+ memberName: "홍석현",
+ netBalance: 53227,
+ totalPaid: 280000,
+ totalOwe: 226773,
+ paidExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "2", title: "저녁식사", amount: 60000, shareAmount: 30000, participantCount: 2, expenseDate: yesterday),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ],
+ sharedExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "2", title: "저녁식사", amount: 60000, shareAmount: 30000, participantCount: 2, expenseDate: yesterday),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ]
+ ),
+ isExpanded: true,
+ isCurrentUser: true,
+ onToggle: {}
+ )
+
+ // 줄 돈이 있는 경우
+ MemberDetailCard(
+ detail: MemberSettlementDetail(
+ id: "2",
+ memberId: "user2",
+ memberName: "김철수",
+ netBalance: -52000,
+ totalPaid: 0,
+ totalOwe: 52000,
+ paidExpenses: [],
+ sharedExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ]
+ ),
+ isExpanded: false,
+ isCurrentUser: false,
+ onToggle: {}
+ )
+ }
+ .padding(16)
+ .background(Color.primary50)
+}
diff --git a/Features/SettlementDetail/Sources/SettlementDetailFeature.swift b/Features/SettlementDetail/Sources/SettlementDetailFeature.swift
new file mode 100644
index 00000000..fa96159c
--- /dev/null
+++ b/Features/SettlementDetail/Sources/SettlementDetailFeature.swift
@@ -0,0 +1,58 @@
+//
+// SettlementDetailFeature.swift
+// SettlementDetailFeature
+//
+// Created by 홍석현 on 2025.
+//
+
+import Foundation
+import ComposableArchitecture
+import Domain
+
+@Reducer
+public struct SettlementDetailFeature {
+ @ObservableState
+ public struct State: Equatable {
+ var memberDetails: [MemberSettlementDetail]
+ var currentUserId: String
+ var expandedMemberIds: Set = []
+
+ public init(
+ memberDetails: [MemberSettlementDetail],
+ currentUserId: String
+ ) {
+ self.memberDetails = memberDetails
+ self.currentUserId = currentUserId
+ // 내 정보는 기본으로 펼쳐놓기
+ self.expandedMemberIds = [currentUserId]
+ }
+
+ var myDetail: MemberSettlementDetail? {
+ memberDetails.first { $0.memberId == currentUserId }
+ }
+
+ var otherMemberDetails: [MemberSettlementDetail] {
+ memberDetails.filter { $0.memberId != currentUserId }
+ }
+ }
+
+ public enum Action: Equatable {
+ case toggleMemberExpansion(String)
+ }
+
+ public init() {}
+
+ public var body: some Reducer {
+ Reduce { state, action in
+ switch action {
+ case .toggleMemberExpansion(let memberId):
+ if state.expandedMemberIds.contains(memberId) {
+ state.expandedMemberIds.remove(memberId)
+ } else {
+ state.expandedMemberIds.insert(memberId)
+ }
+ return .none
+ }
+ }
+ }
+}
diff --git a/Features/SettlementDetail/Sources/SettlementDetailView.swift b/Features/SettlementDetail/Sources/SettlementDetailView.swift
new file mode 100644
index 00000000..c98bc346
--- /dev/null
+++ b/Features/SettlementDetail/Sources/SettlementDetailView.swift
@@ -0,0 +1,122 @@
+//
+// SettlementDetailView.swift
+// SettlementDetailFeature
+//
+// Created by SseuDam on 2025.
+//
+
+import SwiftUI
+import DesignSystem
+import ComposableArchitecture
+import Domain
+
+public struct SettlementDetailView: View {
+ private let store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some View {
+ ScrollView {
+ VStack(spacing: 16) {
+ // 내 정산 상세
+ if let myDetail = store.myDetail {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("내 정산 내역")
+ .font(.app(.title3, weight: .semibold))
+ .foregroundStyle(Color.black)
+
+ MemberDetailCard(
+ detail: myDetail,
+ isExpanded: store.expandedMemberIds.contains(myDetail.memberId),
+ isCurrentUser: true,
+ onToggle: {
+ store.send(.toggleMemberExpansion(myDetail.memberId))
+ }
+ )
+ }
+ }
+
+ // 다른 멤버들
+ if !store.otherMemberDetails.isEmpty {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("다른 멤버")
+ .font(.app(.title3, weight: .semibold))
+ .foregroundStyle(Color.black)
+
+ VStack(spacing: 12) {
+ ForEach(store.otherMemberDetails) { detail in
+ MemberDetailCard(
+ detail: detail,
+ isExpanded: store.expandedMemberIds.contains(detail.memberId),
+ isCurrentUser: false,
+ onToggle: {
+ store.send(.toggleMemberExpansion(detail.memberId))
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ .padding(16)
+ }
+ .scrollIndicators(.hidden)
+ .background(Color.primary50)
+ .navigationTitle("정산 상세")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+}
+
+#Preview {
+ let calendar = Calendar.current
+ let today = Date()
+ let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
+ let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: today)!
+
+ return NavigationView {
+ SettlementDetailView(
+ store: Store(
+ initialState: SettlementDetailFeature.State(
+ memberDetails: [
+ MemberSettlementDetail(
+ id: "1",
+ memberId: "user1",
+ memberName: "홍석현",
+ netBalance: 53227,
+ totalPaid: 280000,
+ totalOwe: 226773,
+ paidExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "2", title: "저녁식사", amount: 60000, shareAmount: 30000, participantCount: 2, expenseDate: yesterday),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ],
+ sharedExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "2", title: "저녁식사", amount: 60000, shareAmount: 30000, participantCount: 2, expenseDate: yesterday),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ]
+ ),
+ MemberSettlementDetail(
+ id: "2",
+ memberId: "user2",
+ memberName: "김철수",
+ netBalance: -52000,
+ totalPaid: 0,
+ totalOwe: 52000,
+ paidExpenses: [],
+ sharedExpenses: [
+ ExpenseDetail(id: "1", title: "호텔", amount: 120000, shareAmount: 40000, participantCount: 3, expenseDate: twoDaysAgo),
+ ExpenseDetail(id: "3", title: "커피", amount: 100000, shareAmount: 33333, participantCount: 3, expenseDate: today)
+ ]
+ )
+ ],
+ currentUserId: "user1"
+ )
+ ) {
+ SettlementDetailFeature()
+ }
+ )
+ }
+}
diff --git a/Features/SettlementDetail/Tests/SettlementDetailFeatureTests.swift b/Features/SettlementDetail/Tests/SettlementDetailFeatureTests.swift
new file mode 100644
index 00000000..eaed7ed1
--- /dev/null
+++ b/Features/SettlementDetail/Tests/SettlementDetailFeatureTests.swift
@@ -0,0 +1,7 @@
+import Testing
+@testable import SettlementDetailFeature
+
+@Suite("SettlementDetailFeature Tests")
+struct SettlementDetailFeatureTests {
+
+}
diff --git a/Features/SettlementResult/Project.swift b/Features/SettlementResult/Project.swift
index b96d4d11..46cdbbfd 100644
--- a/Features/SettlementResult/Project.swift
+++ b/Features/SettlementResult/Project.swift
@@ -4,8 +4,7 @@ import SseuDamPlugin
let project = Project.makeFeature(
name: .SettlementResult,
dependencies: [
- .Domain,
- .DesignSystem
+ .Features.SettlementDetail
],
hasTests: true
)
diff --git a/Features/SettlementResult/Sources/SettlementResultFeature.swift b/Features/SettlementResult/Sources/SettlementResultFeature.swift
index 2543f2e5..3ef4b585 100644
--- a/Features/SettlementResult/Sources/SettlementResultFeature.swift
+++ b/Features/SettlementResult/Sources/SettlementResultFeature.swift
@@ -8,6 +8,7 @@
import Foundation
import Domain
import ComposableArchitecture
+import SettlementDetailFeature
@Reducer
public struct SettlementResultFeature {
@@ -24,6 +25,7 @@ public struct SettlementResultFeature {
public var currentUserId: String?
@Presents public var alert: AlertState?
+ @Presents public var settlementDetail: SettlementDetailFeature.State?
// 정산 계산 결과
public var settlementCalculation: SettlementCalculation = SettlementCalculation(
@@ -32,9 +34,9 @@ public struct SettlementResultFeature {
totalPersonCount: 0,
averagePerPerson: 0,
myNetBalance: 0,
- memberBalances: [:],
paymentsToMake: [],
- paymentsToReceive: []
+ paymentsToReceive: [],
+ memberDetails: []
)
// 총 지출 금액
@@ -83,11 +85,13 @@ public struct SettlementResultFeature {
public enum ViewAction {
case onAppear
case backButtonTapped
+ case detailButtonTapped
}
@CasePathable
public enum ScopeAction {
case alert(PresentationAction)
+ case settlementDetail(PresentationAction)
}
@CasePathable
@@ -116,10 +120,22 @@ public struct SettlementResultFeature {
case .view(.backButtonTapped):
return .none
+ case .view(.detailButtonTapped):
+ // 상세보기 sheet 열기
+ guard let currentUserId = state.currentUserId else { return .none }
+ state.settlementDetail = SettlementDetailFeature.State(
+ memberDetails: state.settlementCalculation.memberDetails,
+ currentUserId: currentUserId
+ )
+ return .none
+
case .scope, .binding:
return .none
}
}
.ifLet(\.$alert, action: \.scope.alert)
+ .ifLet(\.$settlementDetail, action: \.scope.settlementDetail) {
+ SettlementDetailFeature()
+ }
}
}
diff --git a/Features/SettlementResult/Sources/SettlementResultView.swift b/Features/SettlementResult/Sources/SettlementResultView.swift
index 5aba4625..000426d1 100644
--- a/Features/SettlementResult/Sources/SettlementResultView.swift
+++ b/Features/SettlementResult/Sources/SettlementResultView.swift
@@ -8,14 +8,16 @@
import SwiftUI
import DesignSystem
import ComposableArchitecture
+import SettlementDetailFeature
+@ViewAction(for: SettlementResultFeature.self)
public struct SettlementResultView: View {
- @Bindable var store: StoreOf
-
+ @Bindable public var store: StoreOf
+
public init(store: StoreOf) {
self.store = store
}
-
+
public var body: some View {
VStack(spacing: 0) {
// 헤더 (총 지출, 통계)
@@ -24,7 +26,7 @@ public struct SettlementResultView: View {
myExpenseAmount: store.myExpenseAmount,
totalPersonCount: store.totalPersonCount
)
-
+
if !store.paymentsToMake.isEmpty || !store.paymentsToReceive.isEmpty {
// 지급/수령 예정 금액 섹션
ScrollView {
@@ -50,6 +52,27 @@ public struct SettlementResultView: View {
}
)
}
+
+ // 상세보기 버튼
+ Button {
+ send(.detailButtonTapped)
+ } label: {
+ HStack {
+ Text("멤버별 정산 상세보기")
+ .font(.app(.body, weight: .semibold))
+ .foregroundStyle(Color.primary500)
+
+ Spacer()
+
+ Image(systemName: "chevron.right")
+ .font(.system(size: 14, weight: .semibold))
+ .foregroundStyle(Color.primary500)
+ }
+ .padding(16)
+ .background(Color.white)
+ .cornerRadius(12)
+ }
+ .padding(.top, 8)
}
}
} else {
@@ -67,9 +90,14 @@ public struct SettlementResultView: View {
.background(Color.primary50)
.scrollIndicators(.hidden)
.onAppear {
- store.send(.view(.onAppear))
+ send(.onAppear)
}
.alert($store.scope(state: \.alert, action: \.scope.alert))
+ .sheet(item: $store.scope(state: \.settlementDetail, action: \.scope.settlementDetail)) { store in
+ NavigationView {
+ SettlementDetailView(store: store)
+ }
+ }
}
}
diff --git a/Makefile b/Makefile
index 233d143a..66c59b75 100644
--- a/Makefile
+++ b/Makefile
@@ -7,25 +7,67 @@ TUIST_PATH := $(shell command -v tuist 2>/dev/null || find /usr/local/bin /opt/h
# Create a new feature module
feature:
- @echo "🚀 새로운 Feature를 생성합니다..."
- @echo "💡 Tip: 'Feature' 접미사는 자동으로 추가됩니다 (예: main → MainFeature)"
+ @echo "========================================="
+ @echo "Feature 생성"
+ @echo "========================================="
+ @echo ""
+ @echo "💡 팁:"
+ @echo " - 공백이나 하이픈을 사용하면 자동으로 카멜케이스로 변환됩니다"
+ @echo " - 예: 'settlement detail' → 'SettlementDetail'"
+ @echo " - 취소하려면 빈 값을 입력하거나 Ctrl+C를 누르세요"
+ @echo ""
@if [ -z "$(TUIST_PATH)" ]; then \
echo "❌ Tuist를 찾을 수 없습니다."; \
echo "다음 명령어로 설치해주세요:"; \
echo "curl -Ls https://install.tuist.io | bash"; \
exit 1; \
fi
- @read -p "Feature 이름을 입력하세요: " input && \
- name=$$(echo "$$input" | sed -E 's/[Ff]eature$$//' | awk '{print toupper(substr($$0,1,1)) tolower(substr($$0,2))}') && \
- echo "📝 생성할 Feature: $${name}Feature" && \
+ @read -p "Feature 이름을 입력하세요: " input; \
+ if [ -z "$$input" ]; then \
+ echo ""; \
+ echo "❌ 취소되었습니다."; \
+ exit 0; \
+ fi; \
+ name=$$(echo "$$input" | sed -E 's/[-_[:space:]]+/ /g' | awk '{for(i=1;i<=NF;i++) $$i=toupper(substr($$i,1,1)) substr($$i,2)}1' | sed 's/ //g'); \
+ if [ "$$input" != "$$name" ]; then \
+ echo "✨ 자동 변환: '$$input' → '$$name'"; \
+ fi; \
+ if ! echo "$$name" | grep -qE '^[A-Za-z][A-Za-z0-9]*$$'; then \
+ echo ""; \
+ echo "❌ 잘못된 Feature 이름입니다: $$name"; \
+ echo " 알파벳으로 시작하고 알파벳과 숫자만 사용할 수 있습니다."; \
+ exit 1; \
+ fi; \
+ echo ""; \
+ echo "📦 생성할 Feature: $$name"; \
+ echo " 경로: Features/$$name/"; \
+ echo ""; \
+ read -p "계속하시겠습니까? (y/N): " confirm; \
+ if [ "$$confirm" != "y" ] && [ "$$confirm" != "Y" ]; then \
+ echo ""; \
+ echo "❌ 취소되었습니다."; \
+ exit 0; \
+ fi; \
+ echo ""; \
+ echo "🚀 Feature를 생성하는 중..."; \
+ echo ""; \
$(TUIST_PATH) scaffold feature --name $$name && \
./Scripts/update-modules.sh && \
- echo "✅ Feature '$${name}'이 성공적으로 생성되었습니다!" && \
- echo "📦 TargetDependency+Modules.swift가 자동으로 업데이트되었습니다!" && \
+ echo "" && \
+ echo "=========================================" && \
+ echo "✅ Feature '$$name'가 성공적으로 생성되었습니다!" && \
+ echo "=========================================" && \
+ echo "" && \
+ echo "📂 생성된 파일:" && \
+ echo " - Features/$$name/Project.swift" && \
+ echo " - Features/$$name/Sources/$${name}View.swift" && \
+ echo " - Features/$$name/Tests/$${name}FeatureTests.swift" && \
+ echo " - Features/$$name/Demo/" && \
echo "" && \
echo "다음 단계:" && \
- echo "1. 'make generate'로 Xcode 프로젝트 업데이트" && \
- echo "2. '$${name}FeatureDemo' 스킴을 선택해서 테스트"
+ echo " 1. make generate" && \
+ echo " 2. Xcode에서 $$name 작업 시작" && \
+ echo ""
# Generate Xcode project
generate:
diff --git a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+Modules.swift b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+Modules.swift
index 55e4a091..d77decc5 100644
--- a/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+Modules.swift
+++ b/Tuist/Plugins/SseuDamPlugin/ProjectDescriptionHelpers/TargetDependency+Modules.swift
@@ -16,12 +16,13 @@ public extension TargetDependency {
public extension TargetDependency.Features {
// MARK: - Feature Modules
- static let SaveExpense: TargetDependency = .project(target: "SaveExpenseFeature", path: .relativeToRoot("Features/SaveExpense"))
static let ExpenseList: TargetDependency = .project(target: "ExpenseListFeature", path: .relativeToRoot("Features/ExpenseList"))
static let Login: TargetDependency = .project(target: "LoginFeature", path: .relativeToRoot("Features/Login"))
static let Main: TargetDependency = .project(target: "MainFeature", path: .relativeToRoot("Features/Main"))
static let Profile: TargetDependency = .project(target: "ProfileFeature", path: .relativeToRoot("Features/Profile"))
+ static let SaveExpense: TargetDependency = .project(target: "SaveExpenseFeature", path: .relativeToRoot("Features/SaveExpense"))
static let Settlement: TargetDependency = .project(target: "SettlementFeature", path: .relativeToRoot("Features/Settlement"))
+ static let SettlementDetail: TargetDependency = .project(target: "SettlementDetailFeature", path: .relativeToRoot("Features/SettlementDetail"))
static let SettlementResult: TargetDependency = .project(target: "SettlementResultFeature", path: .relativeToRoot("Features/SettlementResult"))
static let Splash: TargetDependency = .project(target: "SplashFeature", path: .relativeToRoot("Features/Splash"))
static let Travel: TargetDependency = .project(target: "TravelFeature", path: .relativeToRoot("Features/Travel"))
@@ -30,12 +31,13 @@ public extension TargetDependency.Features {
// MARK: - Feature Names
public enum FeatureName: String {
- case SaveExpense
case ExpenseList
case Login
case Main
case Profile
+ case SaveExpense
case Settlement
+ case SettlementDetail
case SettlementResult
case Splash
case Travel