Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Data/Sources/DTO/Expense/Local/ExpenseCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

public struct ExpenseCache: Codable {
let travelId: String
let expenses: [TravelExpenseResponseDTO.ExpenseDTO]
let expenses: [ExpenseDTO]
let cachedAt: Date
let expiredAt: Date

Expand All @@ -19,7 +19,7 @@ public struct ExpenseCache: Codable {

init(
travelId: String,
expenses: [TravelExpenseResponseDTO.ExpenseDTO],
expenses: [ExpenseDTO],
) {
self.travelId = travelId
self.expenses = expenses
Expand Down
103 changes: 48 additions & 55 deletions Data/Sources/DTO/Expense/Response/TravelExpenseResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,68 +8,61 @@
import Foundation
import Domain

public struct TravelExpenseResponseDTO: Decodable {
let total: Int
let page: Int
let limit: Int
let items: [ExpenseDTO]
public struct ExpenseDTO: Codable {
let id: String
let title: String
let amount: Double
let currency: String
let convertedAmount: Double
let expenseDate: String
let category: String
let payerId: String
let payerName: String
let authorId: String
let participants: [ParticipantDTO]

struct ExpenseDTO: Codable {
let id: String
let title: String
let amount: Double
let currency: String
let convertedAmount: Double
let expenseDate: String
let category: String
let payerId: String
let payerName: String
let authorId: String
let participants: [ParticipantDTO]
struct ParticipantDTO: Codable {
let memberId: String
let name: String

struct ParticipantDTO: Codable {
let memberId: String
let name: String

func toDomain() -> TravelMember {
TravelMember(
id: memberId,
name: name,
role: "member"
)
}
func toDomain() -> TravelMember {
TravelMember(
id: memberId,
name: name,
role: "member"
)
}
}

func toDomain() -> Expense? {
// yyyy-MM-dd 날짜 파싱 (서버에서 받은 날짜 그대로 사용)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
// Plain date는 timezone 변환하지 않음 (기본값 = TimeZone.current)
func toDomain() -> Expense? {
// yyyy-MM-dd 날짜 파싱 (서버에서 받은 날짜 그대로 사용)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
// Plain date는 timezone 변환하지 않음 (기본값 = TimeZone.current)

guard let date = dateFormatter.date(from: expenseDate) else {
return nil
}
guard let date = dateFormatter.date(from: expenseDate) else {
return nil
}

// 카테고리 파싱 (없으면 .other)
let expenseCategory = ExpenseCategory(rawValue: category) ?? .other
// 카테고리 파싱 (없으면 .other)
let expenseCategory = ExpenseCategory(rawValue: category) ?? .other

// Participant 변환
let members = participants.map { $0.toDomain() }
// Participant 변환
let members = participants.map { $0.toDomain() }

return Expense(
id: id,
title: title,
amount: amount,
currency: currency,
convertedAmount: convertedAmount,
expenseDate: date,
category: expenseCategory,
payerId: payerId,
payerName: payerName,
participants: members
)
}
return Expense(
id: id,
title: title,
amount: amount,
currency: currency,
convertedAmount: convertedAmount,
expenseDate: date,
category: expenseCategory,
payerId: payerId,
payerName: payerName,
participants: members
)
}
}
23 changes: 9 additions & 14 deletions Data/Sources/DataSource/Remote/ExpenseRemoteDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Moya
import NetworkService

public protocol ExpenseRemoteDataSourceProtocol {
func fetchTravelExpenses(travelId: String, page: Int, limit: Int) async throws -> TravelExpenseResponseDTO
func fetchTravelExpenses(travelId: String) async throws -> [ExpenseDTO]
func createExpense(travelId: String, body: CreateExpenseRequestDTO) async throws
func updateExpense(travelId: String, expenseId: String, body: UpdateExpenseRequestDTO) async throws
func deleteExpense(travelId: String, expenseId: String) async throws
Expand All @@ -26,12 +26,10 @@ public struct ExpenseRemoteDataSource: ExpenseRemoteDataSourceProtocol {
}

public func fetchTravelExpenses(
travelId: String,
page: Int,
limit: Int
) async throws -> TravelExpenseResponseDTO {
let response: BaseResponse<TravelExpenseResponseDTO> =
try await provider.request(.fetchTravelExpenses(travelId: travelId, page: page, limit: limit))
travelId: String
) async throws -> [ExpenseDTO] {
let response: BaseResponse<[ExpenseDTO]> =
try await provider.request(.fetchTravelExpenses(travelId: travelId))

guard let data = response.data else {
throw NetworkError.noData
Expand All @@ -55,7 +53,7 @@ public struct ExpenseRemoteDataSource: ExpenseRemoteDataSourceProtocol {

// MARK: - ExpenseAPI
public enum ExpenseAPI {
case fetchTravelExpenses(travelId: String, page: Int, limit: Int)
case fetchTravelExpenses(travelId: String)
case createExpense(travelId: String, body: CreateExpenseRequestDTO)
case updateExpense(travelId: String, expenseId: String, body: UpdateExpenseRequestDTO)
case deleteExpense(travelId: String, expenseId: String)
Expand All @@ -71,7 +69,7 @@ extension ExpenseAPI: BaseTargetType {

public var urlPath: String {
switch self {
case .fetchTravelExpenses(let travelId, _, _):
case .fetchTravelExpenses(let travelId):
return "/\(travelId)/expenses"
case .createExpense(let travelId, _):
return "/\(travelId)/expenses"
Expand All @@ -93,11 +91,8 @@ extension ExpenseAPI: BaseTargetType {

public var parameters: [String: Any]? {
switch self {
case .fetchTravelExpenses(_, let page, let limit):
return [
"page": page,
"limit": limit
]
case .fetchTravelExpenses:
return nil
case .createExpense(_, let body):
return body.toDictionary
case .updateExpense(_, _, let body):
Expand Down
14 changes: 5 additions & 9 deletions Data/Sources/Repository/Expense/ExpenseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ public final class ExpenseRepository: ExpenseRepositoryProtocol {
}

public func fetchTravelExpenses(
travelId: String,
page: Int,
limit: Int
travelId: String
) -> AsyncStream<Result<[Expense], Error>> {
AsyncStream { continuation in
Task {
Expand All @@ -36,17 +34,15 @@ public final class ExpenseRepository: ExpenseRepositoryProtocol {

// 2. 네트워크
do {
let responseDTO = try await remote.fetchTravelExpenses(
travelId: travelId,
page: page,
limit: limit
let expensesDTO = try await remote.fetchTravelExpenses(
travelId: travelId
)
let expenses = responseDTO.items.compactMap { $0.toDomain() }
let expenses = expensesDTO.compactMap { $0.toDomain() }

Task.detached { [weak self] in
let cache = ExpenseCache(
travelId: travelId,
expenses: responseDTO.items
expenses: expensesDTO
)

try? await self?.local.saveCachedExpenses(cache)
Expand Down
2 changes: 1 addition & 1 deletion Domain/Sources/Repository/ExpenseRepositoryProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Dependencies

public protocol ExpenseRepositoryProtocol {
// 여행의 지출 내역 조회
func fetchTravelExpenses(travelId: String, page: Int, limit: Int) -> AsyncStream<Result<[Expense], Error>>
func fetchTravelExpenses(travelId: String) -> AsyncStream<Result<[Expense], Error>>

// 지출 내역 저장
func save(travelId: String, expense: Expense) async throws
Expand Down
4 changes: 1 addition & 3 deletions Domain/Sources/Repository/Mock/MockExpenseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ final public actor MockExpenseRepository: ExpenseRepositoryProtocol {
}

nonisolated public func fetchTravelExpenses(
travelId: String,
page: Int,
limit: Int
travelId: String
) -> AsyncStream<Result<[Expense], any Error>> {
AsyncStream { continuation in
Task {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ public struct FetchTravelExpenseUseCase: FetchTravelExpenseUseCaseProtocol {
AsyncStream { continuation in
Task {
for await result in repository.fetchTravelExpenses(
travelId: travelId,
page: 1,
limit: 1000
travelId: travelId
) {
// 날짜 필터링 적용
let filteredResult = result.map { expenses in
Expand Down
45 changes: 32 additions & 13 deletions Features/SaveExpense/Sources/Components/AmountInputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct AmountInputField: View {
@Binding var amount: String
let baseCurrency: String
let convertedAmountKRW: String

public init(
amount: Binding<String>,
baseCurrency: String = "",
Expand All @@ -22,35 +22,54 @@ public struct AmountInputField: View {
self.baseCurrency = baseCurrency
self.convertedAmountKRW = convertedAmountKRW
}

private var shouldShowConversion: Bool {
baseCurrency != "KRW"
baseCurrency != "KRW"
}


// 천단위 콤마 표시용 (소수점 제거)
private var formattedAmount: String {
guard let number = Double(amount.replacingOccurrences(of: ",", with: "")) else {
return amount
}
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0 // 소수점 제거
formatter.groupingSeparator = ","
return formatter.string(from: NSNumber(value: number)) ?? amount
}

public var body: some View {
VStack(alignment: .leading, spacing: 8) {
FormLabel("지출 금액")

HStack(spacing: 8) {
InputContainer {
TextField("-", text: $amount)
.font(.app(.body, weight: .medium))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
TextField("-", text: Binding(
get: { formattedAmount },
set: { newValue in
// 콤마 제거하고 숫자만 필터링
let filtered = newValue.replacingOccurrences(of: ",", with: "").filter { $0.isNumber }
amount = filtered
}
))
.font(.app(.body, weight: .medium))
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
}

Text(baseCurrency.isEmpty ? "-" : baseCurrency)
.font(.app(.body, weight: .medium))
.foregroundStyle(Color.primary800)
}

if shouldShowConversion {
HStack {
Text("KRW 환산 금액")
.font(.app(.body, weight: .medium))

Spacer()

Text("₩\(convertedAmountKRW)")
.font(.app(.body, weight: .medium))
}
Expand Down
Loading