From a7ee879a3bf36e7743a2aa4ec6151736f5114952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Wed, 10 Dec 2025 15:27:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EC=A7=80=EC=B6=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Response/TravelExpenseResponseDTO.swift | 103 ++++++++---------- .../Remote/ExpenseRemoteDataSource.swift | 23 ++-- .../Expense/ExpenseRepository.swift | 14 +-- .../ExpenseRepositoryProtocol.swift | 2 +- .../Mock/MockExpenseRepository.swift | 4 +- .../Expense/FetchTravelExpenseUseCase.swift | 4 +- 6 files changed, 65 insertions(+), 85 deletions(-) diff --git a/Data/Sources/DTO/Expense/Response/TravelExpenseResponseDTO.swift b/Data/Sources/DTO/Expense/Response/TravelExpenseResponseDTO.swift index b3b4376..e7f762f 100644 --- a/Data/Sources/DTO/Expense/Response/TravelExpenseResponseDTO.swift +++ b/Data/Sources/DTO/Expense/Response/TravelExpenseResponseDTO.swift @@ -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 + ) } } diff --git a/Data/Sources/DataSource/Remote/ExpenseRemoteDataSource.swift b/Data/Sources/DataSource/Remote/ExpenseRemoteDataSource.swift index 51baee1..792b040 100644 --- a/Data/Sources/DataSource/Remote/ExpenseRemoteDataSource.swift +++ b/Data/Sources/DataSource/Remote/ExpenseRemoteDataSource.swift @@ -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 @@ -26,12 +26,10 @@ public struct ExpenseRemoteDataSource: ExpenseRemoteDataSourceProtocol { } public func fetchTravelExpenses( - travelId: String, - page: Int, - limit: Int - ) async throws -> TravelExpenseResponseDTO { - let response: BaseResponse = - 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 @@ -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) @@ -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" @@ -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): diff --git a/Data/Sources/Repository/Expense/ExpenseRepository.swift b/Data/Sources/Repository/Expense/ExpenseRepository.swift index 3575d1b..a69467e 100644 --- a/Data/Sources/Repository/Expense/ExpenseRepository.swift +++ b/Data/Sources/Repository/Expense/ExpenseRepository.swift @@ -22,9 +22,7 @@ public final class ExpenseRepository: ExpenseRepositoryProtocol { } public func fetchTravelExpenses( - travelId: String, - page: Int, - limit: Int + travelId: String ) -> AsyncStream> { AsyncStream { continuation in Task { @@ -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) diff --git a/Domain/Sources/Repository/ExpenseRepositoryProtocol.swift b/Domain/Sources/Repository/ExpenseRepositoryProtocol.swift index 15047fe..e810e68 100644 --- a/Domain/Sources/Repository/ExpenseRepositoryProtocol.swift +++ b/Domain/Sources/Repository/ExpenseRepositoryProtocol.swift @@ -10,7 +10,7 @@ import Dependencies public protocol ExpenseRepositoryProtocol { // 여행의 지출 내역 조회 - func fetchTravelExpenses(travelId: String, page: Int, limit: Int) -> AsyncStream> + func fetchTravelExpenses(travelId: String) -> AsyncStream> // 지출 내역 저장 func save(travelId: String, expense: Expense) async throws diff --git a/Domain/Sources/Repository/Mock/MockExpenseRepository.swift b/Domain/Sources/Repository/Mock/MockExpenseRepository.swift index cdeffb2..31a15b1 100644 --- a/Domain/Sources/Repository/Mock/MockExpenseRepository.swift +++ b/Domain/Sources/Repository/Mock/MockExpenseRepository.swift @@ -36,9 +36,7 @@ final public actor MockExpenseRepository: ExpenseRepositoryProtocol { } nonisolated public func fetchTravelExpenses( - travelId: String, - page: Int, - limit: Int + travelId: String ) -> AsyncStream> { AsyncStream { continuation in Task { diff --git a/Domain/Sources/UseCase/Expense/FetchTravelExpenseUseCase.swift b/Domain/Sources/UseCase/Expense/FetchTravelExpenseUseCase.swift index 958ca65..47c9aa5 100644 --- a/Domain/Sources/UseCase/Expense/FetchTravelExpenseUseCase.swift +++ b/Domain/Sources/UseCase/Expense/FetchTravelExpenseUseCase.swift @@ -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 From 83dff4231b23b82cfd98562e98c3ece0223bc44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Wed, 10 Dec 2025 15:28:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=A7=80=EC=B6=9C=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1=20=EC=9E=85=EB=A0=A5=20=EC=8B=9C=20=EC=B2=9C=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=EC=BD=A4=EB=A7=88=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=86=8C=EC=88=98=EC=A0=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/AmountInputField.swift | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/Features/SaveExpense/Sources/Components/AmountInputField.swift b/Features/SaveExpense/Sources/Components/AmountInputField.swift index b671105..aa8a46b 100644 --- a/Features/SaveExpense/Sources/Components/AmountInputField.swift +++ b/Features/SaveExpense/Sources/Components/AmountInputField.swift @@ -12,7 +12,7 @@ public struct AmountInputField: View { @Binding var amount: String let baseCurrency: String let convertedAmountKRW: String - + public init( amount: Binding, baseCurrency: String = "", @@ -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)) } From e3f18ef9f3520a475061141771fb1f461277664b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Wed, 10 Dec 2025 15:36:50 +0900 Subject: [PATCH 3/3] chore: --- Data/Sources/DTO/Expense/Local/ExpenseCache.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Data/Sources/DTO/Expense/Local/ExpenseCache.swift b/Data/Sources/DTO/Expense/Local/ExpenseCache.swift index 9d0bac9..0ee97d4 100644 --- a/Data/Sources/DTO/Expense/Local/ExpenseCache.swift +++ b/Data/Sources/DTO/Expense/Local/ExpenseCache.swift @@ -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 @@ -19,7 +19,7 @@ public struct ExpenseCache: Codable { init( travelId: String, - expenses: [TravelExpenseResponseDTO.ExpenseDTO], + expenses: [ExpenseDTO], ) { self.travelId = travelId self.expenses = expenses