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
73 changes: 73 additions & 0 deletions CALENDAR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## Calendar Feature 구조와 흐름
CalendarView -> dori가 있는 날을 터치시 바텀시트가 올라옴
바텀시트: DayDetailSheet


## API
GET /dori/list
diection은 DoriSegmentControl 로 결정 됨
judori -> Out, baddori -> In

- [query]:
direction: String (IN/OUT)
year: String
month: String

- [header]
Authorization: Bearer JWT - AuthInterceptor에게 위임된 작업

- 응답
SuccessResponse<data에 맞는 새로운구조체> 작성할 것
- 달력의 year/month에 따른 응답 값 inDoriDayList를 달력에 점으로 표시하면 됨
- inDoriList, outDoriList 로 바텀시트에 row로 사용하면 됨
- DoriSegmentControl direction에 따른 inDoriTotalAmount, outDoriTotalAmount가 상단 총 주도리에 표시됨
{
"success": true,
"data": {
"userId": 1,
"year": 2024,
"month": 5,
"inDoriTotalAmount": 100000,
"inDoriDayList": [
1,
2
],
"inDoriList": [
{
"doriId": 1,
"userId": 1,
"partnerId": 1,
"direction": "IN",
"partnerName": "홍길동",
"relationship": "지인",
"eventType": "생일",
"amount": 50000,
"eventDate": "2024-05-01",
"isVisited": true,
"memo": "메모",
"createdAt": "2026-02-17T09:27:50.658160145"
}
],
"outDoriTotalAmount": 50000,
"outDoriDayList": [
3
],
"outDoriList": [
{
"doriId": 2,
"userId": 1,
"partnerId": 2,
"direction": "OUT",
"partnerName": "김영희",
"relationship": "동료",
"eventType": "결혼",
"amount": 50000,
"eventDate": "2024-05-03",
"isVisited": false,
"memo": null,
"createdAt": "2026-02-17T09:27:50.658193668"
}
]
},
"error": null
}
4 changes: 2 additions & 2 deletions Projects/App/Sources/DoriApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import FeatureMyPage
import FeatureOnboarding
import FeatureAddDori
import FeatureHistory
import FeatureCalendar
import PlatformKakaoAuth
import PlatformKeychain

Expand Down Expand Up @@ -47,9 +48,8 @@ struct DoriApp: App {
)

$0.addDoriAPIClient = .live(networkService: networkService)

$0.calendarClient = .live(networkService: networkService)
$0.historyAPIClient = .live(networkService: networkService)

$0.myPageAPIClient = .live(
networkService: networkService,
tokenStore: tokenStore
Expand Down
11 changes: 0 additions & 11 deletions Projects/Feature/AddDori/Sources/Modifier/AddDoriModifiers.swift

This file was deleted.

256 changes: 256 additions & 0 deletions Projects/Feature/Calendar/Sources/CalendarClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
//
// CalendarClient.swift
// Dori-iOS
//
// Created by 강동영 on 2/17/26.
//

import Foundation
import ComposableArchitecture
import DoriCore
import DoriNetwork

public struct CalendarDori: Identifiable, Sendable, Codable, Hashable {
public let id: Int64
public let type: TransactionType
public let partnerName: String
public let relationship: String
public let eventType: String
public let amount: Int
public let eventDate: Date
public let isVisited: Bool
public let memo: String?

public init(
id: Int64,
type: TransactionType,
partnerName: String,
relationship: String,
eventType: String,
amount: Int,
eventDate: Date,
isVisited: Bool,
memo: String?
) {
self.id = id
self.type = type
self.partnerName = partnerName
self.relationship = relationship
self.eventType = eventType
self.amount = amount
self.eventDate = eventDate
self.isVisited = isVisited
self.memo = memo
}
}

public struct CalendarMonthlyData: Sendable, Equatable {
public let year: Int
public let month: Int
public let inDoriTotalAmount: Int
public let inDoriDayList: [Int]
public let inDoriList: [CalendarDori]
public let outDoriTotalAmount: Int
public let outDoriDayList: [Int]
public let outDoriList: [CalendarDori]

public init(
year: Int,
month: Int,
inDoriTotalAmount: Int,
inDoriDayList: [Int],
inDoriList: [CalendarDori],
outDoriTotalAmount: Int,
outDoriDayList: [Int],
outDoriList: [CalendarDori]
) {
self.year = year
self.month = month
self.inDoriTotalAmount = inDoriTotalAmount
self.inDoriDayList = inDoriDayList
self.inDoriList = inDoriList
self.outDoriTotalAmount = outDoriTotalAmount
self.outDoriDayList = outDoriDayList
self.outDoriList = outDoriList
}

public static func empty(for month: Date) -> Self {
Self(
year: month.year,
month: month.month,
inDoriTotalAmount: 0,
inDoriDayList: [],
inDoriList: [],
outDoriTotalAmount: 0,
outDoriDayList: [],
outDoriList: []
)
}
}

@DependencyClient
public struct CalendarClient: Sendable {
public var fetchMonthlyData: @Sendable (
_ month: Date,
_ type: TransactionType
) async throws -> CalendarMonthlyData
}

private enum CalendarClientError: LocalizedError {
case unconfigured
case invalidResponse
case backendError(String)
case invalidEventDate(String)

var errorDescription: String? {
switch self {
case .unconfigured:
return "CalendarClient가 구성되지 않았습니다."
case .invalidResponse:
return "서버 응답이 올바르지 않습니다."
case .backendError(let message):
return message
case .invalidEventDate(let value):
return "올바르지 않은 날짜 형식입니다: \(value)"
}
}
}

extension CalendarClient: DependencyKey {
public static let liveValue = Self(
fetchMonthlyData: { _, _ in throw CalendarClientError.unconfigured }
)

public static let previewValue = Self(
fetchMonthlyData: { month, _ in
let calendar = Calendar(identifier: .gregorian)

func makeDate(day: Int) -> Date {
calendar.date(from: DateComponents(year: month.year, month: month.month, day: day)) ?? month
}

return CalendarMonthlyData(
year: month.year,
month: month.month,
inDoriTotalAmount: 100_000,
inDoriDayList: [1, 2, 8],
inDoriList: [
CalendarDori(
id: 1,
type: .baddori,
partnerName: "홍길동",
relationship: "지인",
eventType: "생일",
amount: 50_000,
eventDate: makeDate(day: 1),
isVisited: true,
memo: "메모"
)
],
outDoriTotalAmount: 50_000,
outDoriDayList: [3, 5],
outDoriList: [
CalendarDori(
id: 2,
type: .judori,
partnerName: "김영희",
relationship: "동료",
eventType: "결혼",
amount: 50_000,
eventDate: makeDate(day: 3),
isVisited: false,
memo: nil
)
]
)
}
)

public static let testValue = Self()
}

public extension CalendarClient {
static func live(networkService: any NetworkService) -> Self {
Self(
fetchMonthlyData: { month, type in
let request = DoriListRequest(
direction: type.calendarDirection,
year: String(month.year),
month: String(month.month)
)
let endpoint = DoriListEndpoint(request: request)
let response = try await networkService.request(
endpoint,
responseType: SuccessResponse<DoriListResponseDTO>.self
)

if let apiError = response.error {
throw CalendarClientError.backendError(apiError.message ?? apiError.code)
}

guard response.success, let data = response.data else {
throw CalendarClientError.invalidResponse
}

return CalendarMonthlyData(
year: data.year,
month: data.month,
inDoriTotalAmount: data.inDoriTotalAmount,
inDoriDayList: data.inDoriDayList,
inDoriList: try data.inDoriList.map { try $0.toDomain(type: .baddori) },
outDoriTotalAmount: data.outDoriTotalAmount,
outDoriDayList: data.outDoriDayList,
outDoriList: try data.outDoriList.map { try $0.toDomain(type: .judori) }
)
}
)
}
}

public extension DependencyValues {
var calendarClient: CalendarClient {
get { self[CalendarClient.self] }
set { self[CalendarClient.self] = newValue }
}
}

private extension TransactionType {
var calendarDirection: String {
switch self {
case .judori:
return "OUT"
case .baddori:
return "IN"
}
}
}

private extension DoriListItemDTO {
func toDomain(type: TransactionType) throws -> CalendarDori {
guard let parsedEventDate = eventDate.calendarEventDate else {
throw CalendarClientError.invalidEventDate(eventDate)
}

return CalendarDori(
id: doriId,
type: type,
partnerName: partnerName,
relationship: relationship,
eventType: eventType,
amount: amount,
eventDate: parsedEventDate,
isVisited: isVisited,
memo: memo
)
}
}

private extension String {
var calendarEventDate: Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: self)
}
}
Loading