diff --git a/SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift b/SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift new file mode 100644 index 00000000..676fa000 --- /dev/null +++ b/SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift @@ -0,0 +1,105 @@ +import Foundation + +enum GiftDirection: String, Decodable { + case sent = "SENT" + case received = "RECEIVED" +} + +enum GiftStatus: String, Decodable, CaseIterable { + case received = "RECEIVED" + case trackingRegistered = "TRACKING_REGISTERED" + case arrivedAtMailbox = "ARRIVED_AT_MAILBOX" + case inspectionCompleted = "INSPECTION_COMPLETED" + case delivered = "DELIVERED" + case undeliverable = "UNDELIVERABLE" + case canceled = "CANCELED" +} + +enum GiftProgressStep: String, CaseIterable { + case received = "RECEIVED" + case trackingRegistered = "TRACKING_REGISTERED" + case arrivedAtMailbox = "ARRIVED_AT_MAILBOX" + case inspectionCompleted = "INSPECTION_COMPLETED" + case delivered = "DELIVERED" + + var title: String { + switch self { + case .received: + return "신청 접수" + case .trackingRegistered: + return "발송 확인" + case .arrivedAtMailbox: + return "사서함 도착" + case .inspectionCompleted: + return "검수 완료" + case .delivered: + return "전달 완료" + } + } +} + +enum GiftSize: String, Decodable { + case small = "SMALL" + case medium = "MEDIUM" + case large = "LARGE" +} + +struct GiftListStatusDisplayModel { + let title: String + let subtitle: String? +} + +struct GiftListCardDisplayModel { + let applicationNo: String + let direction: GiftDirection + let status: GiftStatus + let counterpartNickname: String + let counterpartProfileImageUrl: String? + let categoryName: String + let sizeName: String + let createdDateText: String? + let statusDisplay: GiftListStatusDisplayModel +} + +struct GiftProgressDisplayModel { + let step: GiftProgressStep + let occurredDateText: String? +} + +struct GiftAddressDisplayModel { + let name: String + let phoneNumber: String + let address: String +} + +struct SentGiftDetailDisplayModel { + let status: GiftStatus + let statusName: String + let undeliverableReason: String? + let recipientCreatorNickname: String? + let sizeName: String + let categoryName: String + let applicationNo: String + let paidCan: Int? + let tracking: String? + let shippingRequestedDateText: String? + let applicationDateText: String? + let canceledDateText: String? + let senderInfo: GiftAddressDisplayModel? + let mailbox: GiftAddressDisplayModel? + let trackingRequired: Bool + let progress: [GiftProgressDisplayModel] +} + +struct ReceivedGiftDetailDisplayModel { + let status: GiftStatus + let statusName: String + let undeliverableReason: String? + let senderNickname: String? + let sizeName: String + let categoryName: String + let shippingRequestedDateText: String? + let recipientAddressRequired: Bool + let recipientAddress: GiftAddressDisplayModel? + let progress: [GiftProgressDisplayModel] +} diff --git a/SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift b/SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift new file mode 100644 index 00000000..be7ff4ed --- /dev/null +++ b/SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift @@ -0,0 +1,164 @@ +import Foundation + +enum GiftDateFormatter { + static func yyyyMMdd(fromUTC text: String?) -> String? { + format(text, format: "yyyy.MM.dd") + } + + static func mmDD(fromUTC text: String?) -> String? { + format(text, format: "MM.dd") + } + + private static func format(_ text: String?, format: String) -> String? { + guard let text, let date = DateParser.parse(text) else { return nil } + + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = .autoupdatingCurrent + formatter.dateFormat = format + return formatter.string(from: date) + } +} + +enum GiftDisplayPolicy { + static func listCard(from item: GiftListItemResponse) -> GiftListCardDisplayModel? { + guard let direction = GiftDirection(rawValue: item.direction), + let status = GiftStatus(rawValue: item.status) else { + return nil + } + + return GiftListCardDisplayModel( + applicationNo: item.applicationNo, + direction: direction, + status: status, + counterpartNickname: item.counterpartNickname, + counterpartProfileImageUrl: item.counterpartProfileImageUrl, + categoryName: item.categoryName, + sizeName: item.sizeName, + createdDateText: GiftDateFormatter.yyyyMMdd(fromUTC: item.createdAt), + statusDisplay: listStatus(direction: direction, status: status, statusName: item.statusName) + ) + } + + static func sentDetail(from detail: GiftDetailResponse) -> SentGiftDetailDisplayModel? { + guard let status = status(from: detail, expectedDirection: .sent) else { return nil } + + return SentGiftDetailDisplayModel( + status: status, + statusName: detail.statusName, + undeliverableReason: detail.delivery.undeliverableReason, + recipientCreatorNickname: detail.giftInfo.recipientCreatorNickname, + sizeName: detail.giftInfo.sizeName, + categoryName: detail.giftInfo.categoryName, + applicationNo: detail.applicationNo, + paidCan: detail.giftInfo.paidCan, + tracking: detail.giftInfo.tracking, + shippingRequestedDateText: GiftDateFormatter.yyyyMMdd(fromUTC: detail.giftInfo.shippingRequestedAt), + applicationDateText: GiftDateFormatter.yyyyMMdd( + fromUTC: detail.statusTimeline.first { $0.status == GiftStatus.received.rawValue }?.occurredAt + ), + canceledDateText: GiftDateFormatter.yyyyMMdd(fromUTC: detail.delivery.canceledAt), + senderInfo: detail.senderInfo.map(GiftAddressDisplayModel.init), + mailbox: detail.mailbox.map(GiftAddressDisplayModel.init), + trackingRequired: detail.trackingRequired, + progress: progress(from: detail.statusTimeline) + ) + } + + static func receivedDetail(from detail: GiftDetailResponse) -> ReceivedGiftDetailDisplayModel? { + guard let status = status(from: detail, expectedDirection: .received) else { return nil } + + return ReceivedGiftDetailDisplayModel( + status: status, + statusName: detail.statusName, + undeliverableReason: detail.delivery.undeliverableReason, + senderNickname: detail.giftInfo.senderNickname, + sizeName: detail.giftInfo.sizeName, + categoryName: detail.giftInfo.categoryName, + shippingRequestedDateText: GiftDateFormatter.yyyyMMdd(fromUTC: detail.giftInfo.shippingRequestedAt), + recipientAddressRequired: status == .undeliverable ? false : detail.recipientAddressRequired, + recipientAddress: status == .undeliverable ? nil : detail.recipientAddress.map(GiftAddressDisplayModel.init), + progress: progress(from: detail.statusTimeline) + ) + } + + static func sizeDescription(for sizeCode: String) -> String? { + guard let size = GiftSize(rawValue: sizeCode) else { return nil } + + switch size { + case .small: + return "세 변의 합 100cm 이하 · 5kg 이하" + case .medium: + return "세 변의 합 120cm 이하 · 15kg 이하" + case .large: + return "세 변의 합 160cm 이하 · 20kg 이하" + } + } + + private static func listStatus( + direction: GiftDirection, + status: GiftStatus, + statusName: String + ) -> GiftListStatusDisplayModel { + switch (direction, status) { + case (.sent, .received): + return GiftListStatusDisplayModel(title: "운송장 등록 필요", subtitle: nil) + case (.sent, .trackingRegistered): + return GiftListStatusDisplayModel(title: "발송 확인", subtitle: nil) + case (.sent, .arrivedAtMailbox): + return GiftListStatusDisplayModel(title: "사서함 도착", subtitle: nil) + case (.sent, .inspectionCompleted): + return GiftListStatusDisplayModel(title: "선물 검수 완료", subtitle: nil) + case (.sent, .delivered): + return GiftListStatusDisplayModel(title: "전달 완료", subtitle: nil) + case (.sent, .undeliverable): + return GiftListStatusDisplayModel(title: "전달 불가", subtitle: nil) + case (.sent, .canceled): + return GiftListStatusDisplayModel(title: "신청 취소", subtitle: nil) + case (.received, .trackingRegistered): + return GiftListStatusDisplayModel(title: "배송지 입력 필요", subtitle: "전달 예정") + case (.received, .arrivedAtMailbox): + return GiftListStatusDisplayModel(title: "선물 검수", subtitle: "선물 확인 중") + case (.received, .inspectionCompleted): + return GiftListStatusDisplayModel(title: "선물 검수 완료", subtitle: "배송 중") + case (.received, .delivered): + return GiftListStatusDisplayModel(title: "전달 완료", subtitle: nil) + case (.received, .undeliverable): + return GiftListStatusDisplayModel(title: "전달 불가", subtitle: nil) + default: + return GiftListStatusDisplayModel(title: statusName, subtitle: nil) + } + } + + private static func status( + from detail: GiftDetailResponse, + expectedDirection: GiftDirection + ) -> GiftStatus? { + guard GiftDirection(rawValue: detail.direction) == expectedDirection else { return nil } + return GiftStatus(rawValue: detail.status) + } + + private static func progress(from timeline: [GiftStatusTimelineResponse]) -> [GiftProgressDisplayModel] { + GiftProgressStep.allCases.map { step in + let occurredAt = timeline.first { $0.status == step.rawValue }?.occurredAt + return GiftProgressDisplayModel( + step: step, + occurredDateText: GiftDateFormatter.mmDD(fromUTC: occurredAt) + ) + } + } +} + +private extension GiftAddressDisplayModel { + init(_ senderInfo: GiftSenderInfoResponse) { + self.init(name: senderInfo.name, phoneNumber: senderInfo.phoneNumber, address: senderInfo.address) + } + + init(_ recipientAddress: GiftRecipientAddressResponse) { + self.init(name: recipientAddress.name, phoneNumber: recipientAddress.phoneNumber, address: recipientAddress.address) + } + + init(_ mailbox: GiftMailboxResponse) { + self.init(name: mailbox.name, phoneNumber: mailbox.phoneNumber, address: mailbox.address) + } +}