feat(gift): 선물 응답 모델을 추가한다

This commit is contained in:
Yu Sung
2026-10-08 12:32:44 +09:00
parent fc15203722
commit afc423ff40
3 changed files with 212 additions and 0 deletions
@@ -0,0 +1,135 @@
import Foundation
struct GiftFormOptionsResponse: Decodable {
let sizes: [GiftSizeOptionResponse]
let categories: [GiftCategoryOptionResponse]
}
struct GiftSizeOptionResponse: Decodable {
let sizeCode: String
let name: String
let basePriceCan: Int
let salePriceCan: Int
}
struct GiftCategoryOptionResponse: Decodable {
let categoryId: Int
let name: String
let requiresDamageWaiver: Bool
}
struct CreateGiftRequest: Encodable {
let recipientMemberId: Int
let senderName: String
let senderPhoneNumber: String
let senderZipCode: String
let senderAddress: String
let senderAddressDetail: String
let sizeCode: String
let categoryId: Int
let senderTermsAgreed: Bool
let senderPrivacyAgreed: Bool
let damageWaiverAgreed: Bool
}
struct CreateGiftResponse: Decodable {
let applicationNo: String
let status: String
let statusName: String
let priceCan: Int
let trackingDeadlineAt: String
}
struct GiftListResponse: Decodable {
let totalCount: Int
let items: [GiftListItemResponse]
let page: Int
let size: Int
let hasNext: Bool
}
struct GiftListItemResponse: Decodable {
let applicationNo: String
let direction: String
let status: String
let statusName: String
let priceCan: Int
let categoryName: String
let sizeName: String
let counterpartMemberId: Int
let counterpartNickname: String
let counterpartProfileImageUrl: String?
let createdAt: String
}
struct GiftDetailResponse: Decodable {
let applicationNo: String
let direction: String
let status: String
let statusName: String
let giftInfo: GiftInfoResponse
let senderInfo: GiftSenderInfoResponse?
let recipientAddress: GiftRecipientAddressResponse?
let mailbox: GiftMailboxResponse?
let trackingRequired: Bool
let recipientAddressRequired: Bool
let recipientAddressDeadlineAt: String?
let delivery: GiftDeliveryResponse
let statusTimeline: [GiftStatusTimelineResponse]
}
struct GiftInfoResponse: Decodable {
let recipientCreatorNickname: String?
let senderNickname: String?
let sizeName: String
let categoryName: String
let applicationNo: String?
let paidCan: Int?
let tracking: String?
let shippingRequestedAt: String?
}
struct GiftSenderInfoResponse: Decodable {
let name: String
let phoneNumber: String
let address: String
}
struct GiftRecipientAddressResponse: Decodable {
let name: String
let phoneNumber: String
let address: String
}
struct GiftMailboxResponse: Decodable {
let name: String
let address: String
let phoneNumber: String
}
struct GiftDeliveryResponse: Decodable {
let canceledAt: String?
let undeliverableAt: String?
let undeliverableReason: String?
}
struct GiftStatusTimelineResponse: Decodable {
let status: String
let statusName: String
let occurredAt: String?
}
struct RegisterGiftTrackingRequest: Encodable {
let courierCompanyName: String
let trackingNumber: String
}
struct SubmitGiftRecipientAddressRequest: Encodable {
let recipientName: String
let recipientPhoneNumber: String
let recipientZipCode: String
let recipientAddress: String
let recipientAddressDetail: String
let recipientTermsAgreed: Bool
let recipientPrivacyAgreed: Bool
}
@@ -0,0 +1,62 @@
import Foundation
@main
enum GiftDecodingContractCheck {
static var assertions = 0
static func check(_ condition: Bool, _ message: String) {
guard condition else { print("FAIL: \(message)"); exit(1) }
assertions += 1
}
static func decode<T: Decodable>(_ type: T.Type, _ json: String) throws -> T {
try JSONDecoder().decode(type, from: Data(json.utf8))
}
static func main() {
let form = #"{"success":true,"message":"Options loaded","data":{"sizes":[{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}],"categories":[{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}]}}"#
let create = #"{"success":true,"message":"Gift created","data":{"applicationNo":"G-1001","status":"CREATED","statusName":"Created","priceCan":80,"trackingDeadlineAt":"2026-10-10T12:00:00Z"}}"#
let list = #"{"success":true,"message":"Gifts loaded","data":{"totalCount":1,"items":[{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","priceCan":80,"categoryName":"Flowers","sizeName":"Small","counterpartMemberId":42,"counterpartNickname":"Mina","counterpartProfileImageUrl":null,"createdAt":"2026-10-07T12:00:00Z"}],"page":1,"size":20,"hasNext":false}}"#
let detail = #"{"success":true,"message":"Gift detail loaded","data":{"applicationNo":"G-1001","direction":"SENT","status":"CREATED","statusName":"Created","giftInfo":{"recipientCreatorNickname":"Mina","senderNickname":"Jae","sizeName":"Small","categoryName":"Flowers","applicationNo":null,"paidCan":80,"tracking":"123456","shippingRequestedAt":null},"senderInfo":{"name":"Jae","phoneNumber":"010-1234-5678","address":"Seoul"},"recipientAddress":null,"mailbox":{"name":"Mina","address":"Busan","phoneNumber":"010-9876-5432"},"trackingRequired":true,"recipientAddressRequired":false,"recipientAddressDeadlineAt":null,"delivery":{"canceledAt":null,"undeliverableAt":null,"undeliverableReason":null},"statusTimeline":[{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}]}}"#
let options = try! decode(ApiResponse<GiftFormOptionsResponse>.self, form)
check(options.success, "form success")
check(options.data?.sizes.first?.sizeCode == "S" && options.data?.sizes.first?.name == "Small", "size fields")
check(options.data?.categories.first?.categoryId == 7 && options.data?.categories.first?.name == "Flowers", "category fields")
check(options.data?.categories.first?.requiresDamageWaiver == true, "waiver")
let created = try! decode(ApiResponse<CreateGiftResponse>.self, create)
check(created.data?.applicationNo == "G-1001" && created.data?.statusName == "Created", "create fields")
check(created.data?.priceCan == 80 && created.data?.trackingDeadlineAt == "2026-10-10T12:00:00Z", "create values")
let gifts = try! decode(ApiResponse<GiftListResponse>.self, list)
check(gifts.data?.totalCount == 1 && gifts.data?.page == 1 && gifts.data?.size == 20 && gifts.data?.hasNext == false, "list paging")
check(gifts.data?.items.first?.categoryName == "Flowers" && gifts.data?.items.first?.counterpartNickname == "Mina", "list values")
check(gifts.data?.items.first?.counterpartProfileImageUrl == nil, "list nullable profile image")
let gift = try! decode(ApiResponse<GiftDetailResponse>.self, detail).data!
check(gift.applicationNo == "G-1001" && gift.giftInfo.categoryName == "Flowers", "detail values")
check(gift.applicationNo == "G-1001" && gift.giftInfo.applicationNo == nil, "detail application numbers")
check(gift.senderInfo?.name == "Jae" && gift.mailbox?.name == "Mina", "detail nested values")
check(gift.recipientAddress == nil && gift.recipientAddressDeadlineAt == nil && gift.giftInfo.shippingRequestedAt == nil, "detail nullable values")
check(gift.statusTimeline.first?.statusName == "Created", "timeline status name")
for json in [
#"{"success":true,"message":"Gift canceled","data":{"applicationNo":"G-1001","status":"CANCELED","statusName":"Canceled","priceCan":80,"canceledAt":"2026-10-07T13:00:00Z"}}"#,
#"{"success":true,"message":"Tracking registered","data":{"applicationNo":"G-1001","status":"TRACKING_REGISTERED","statusName":"Tracking registered","courierCompanyName":"Fast Express","trackingNumber":"123456","trackingRegisteredAt":"2026-10-07T13:00:00Z","recipientAddressDeadlineAt":"2026-10-10T12:00:00Z"}}"#,
#"{"success":true,"message":"Recipient address registered","data":{"applicationNo":"G-1001","status":"ADDRESS_REGISTERED","statusName":"Address registered","recipientAddressRegisteredAt":"2026-10-07T14:00:00Z"}}"#
] {
check(try! decode(ApiResponseWithoutData.self, json).success, "mutation success envelope")
}
check(try! decode(ApiResponseWithoutData.self, #"{"success":true}"#).success, "envelope without data")
check(!(try! decode(ApiResponseWithoutData.self, #"{"success":false,"data":{"applicationNo":"G-1001"}}"#).success), "false success")
let size = try! decode(GiftSizeOptionResponse.self, #"{"sizeCode":"S","name":"Small","basePriceCan":100,"salePriceCan":80}"#)
check(size.name == "Small", "direct size.name")
let category = try! decode(GiftCategoryOptionResponse.self, #"{"categoryId":7,"name":"Flowers","requiresDamageWaiver":true}"#)
check(category.name == "Flowers", "direct category.name")
let timeline = try! decode(GiftStatusTimelineResponse.self, #"{"status":"CREATED","statusName":"Created","occurredAt":"2026-10-07T12:00:00Z"}"#)
check(timeline.statusName == "Created", "direct timeline.statusName")
print("PASS: \(assertions) assertions")
}
}
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
swiftc \
"$root/SodaLive/Sources/Common/ApiResponse.swift" \
"$root/SodaLive/Sources/Common/ApiResponseWithoutData.swift" \
"$root/SodaLive/Sources/V2/Gift/Models/GiftModels.swift" \
"$root/work/tests/GiftDecodingContractCheck.swift" \
-o "$tmp/gift-decoding-contract-check"
"$tmp/gift-decoding-contract-check"