import Foundation // MARK: - MemberProfile Response struct MemberProfileResponse: Codable { let code: Int let data: MemberProfile enum CodingKeys: String, CodingKey { case code, data } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(code, forKey: .code) try container.encode(data, forKey: .data) } } // MARK: - TitleRanking struct TitleRanking: Codable { let displayName: String let ranking: Int let value: Int let materialType: String let userId: String let region: String let userAvatarUrl: String? let userNickName: String? enum CodingKeys: String, CodingKey { case displayName = "display_name" case ranking case value case materialType = "material_type" case userId = "user_id" case region case userAvatarUrl = "user_avatar_url" case userNickName = "user_nick_name" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(displayName, forKey: .displayName) try container.encode(ranking, forKey: .ranking) try container.encode(value, forKey: .value) try container.encode(materialType, forKey: .materialType) try container.encode(userId, forKey: .userId) try container.encode(region, forKey: .region) try container.encodeIfPresent(userAvatarUrl, forKey: .userAvatarUrl) try container.encodeIfPresent(userNickName, forKey: .userNickName) } } // MARK: - MemberProfile struct MemberProfile: Codable { let materialCounter: MaterialCounter let userInfo: MemberUserInfo let storiesCount: Int let conversationsCount: Int let remainPoints: Int let totalPoints: Int let usedBytes: Int let totalBytes: Int let titleRankings: [TitleRanking] let medalInfos: [MedalInfo] let membershipLevel: String let membershipEndAt: String enum CodingKeys: String, CodingKey { case materialCounter = "material_counter" case userInfo = "user_info" case storiesCount = "stories_count" case conversationsCount = "conversations_count" case remainPoints = "remain_points" case totalPoints = "total_points" case usedBytes = "used_bytes" case totalBytes = "total_bytes" case titleRankings = "title_rankings" case medalInfos = "medal_infos" case membershipLevel = "membership_level" case membershipEndAt = "membership_end_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) materialCounter = try container.decode(MaterialCounter.self, forKey: .materialCounter) userInfo = try container.decode(MemberUserInfo.self, forKey: .userInfo) storiesCount = try container.decode(Int.self, forKey: .storiesCount) conversationsCount = try container.decode(Int.self, forKey: .conversationsCount) remainPoints = try container.decode(Int.self, forKey: .remainPoints) totalPoints = try container.decode(Int.self, forKey: .totalPoints) usedBytes = try container.decode(Int.self, forKey: .usedBytes) totalBytes = try container.decode(Int.self, forKey: .totalBytes) titleRankings = try container.decode([TitleRanking].self, forKey: .titleRankings) if let medalInfos = try? container.decode([MedalInfo].self, forKey: .medalInfos) { self.medalInfos = medalInfos } else { self.medalInfos = [] } membershipLevel = try container.decode(String.self, forKey: .membershipLevel) membershipEndAt = try container.decode(String.self, forKey: .membershipEndAt) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(materialCounter, forKey: .materialCounter) try container.encode(userInfo, forKey: .userInfo) try container.encode(storiesCount, forKey: .storiesCount) try container.encode(conversationsCount, forKey: .conversationsCount) try container.encode(remainPoints, forKey: .remainPoints) try container.encode(totalPoints, forKey: .totalPoints) try container.encode(usedBytes, forKey: .usedBytes) try container.encode(totalBytes, forKey: .totalBytes) try container.encode(titleRankings, forKey: .titleRankings) try container.encode(medalInfos, forKey: .medalInfos) try container.encode(membershipLevel, forKey: .membershipLevel) try container.encode(membershipEndAt, forKey: .membershipEndAt) } } // MARK: - MemberUserInfo struct MemberUserInfo: Codable { let userId: String let accessToken: String let avatarFileUrl: String? let nickname: String let account: String let email: String let refreshToken: String? enum CodingKeys: String, CodingKey { case userId = "user_id" case accessToken = "access_token" case avatarFileUrl = "avatar_file_url" case nickname, account, email case refreshToken = "refresh_token" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(userId, forKey: .userId) try container.encode(accessToken, forKey: .accessToken) try container.encodeIfPresent(avatarFileUrl, forKey: .avatarFileUrl) try container.encode(nickname, forKey: .nickname) try container.encode(account, forKey: .account) try container.encode(email, forKey: .email) try container.encodeIfPresent(refreshToken, forKey: .refreshToken) } } // MARK: - MaterialCounter struct MaterialCounter: Codable { let userId: Int64 let totalCount: TotalCount let categoryCount: [String: CategoryCount] enum CodingKeys: String, CodingKey { case userId = "user_id" case totalCount = "total_count" case categoryCount = "category_count" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(userId, forKey: .userId) try container.encode(totalCount, forKey: .totalCount) try container.encode(categoryCount, forKey: .categoryCount) } } // MARK: - TotalCount struct TotalCount: Codable { let videoCount: Int let photoCount: Int let liveCount: Int let videoLength: Double let coverUrl: String? enum CodingKeys: String, CodingKey { case videoCount = "video_count" case photoCount = "photo_count" case liveCount = "live_count" case videoLength = "video_length" case coverUrl = "cover_url" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(videoCount, forKey: .videoCount) try container.encode(photoCount, forKey: .photoCount) try container.encode(liveCount, forKey: .liveCount) try container.encode(videoLength, forKey: .videoLength) try container.encodeIfPresent(coverUrl, forKey: .coverUrl) } } // MARK: - CategoryCount struct CategoryCount: Codable { let videoCount: Int let photoCount: Int let liveCount: Int let videoLength: Double let coverUrl: String? enum CodingKeys: String, CodingKey { case videoCount = "video_count" case photoCount = "photo_count" case liveCount = "live_count" case videoLength = "video_length" case coverUrl = "cover_url" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(videoCount, forKey: .videoCount) try container.encode(photoCount, forKey: .photoCount) try container.encode(liveCount, forKey: .liveCount) try container.encode(videoLength, forKey: .videoLength) try container.encodeIfPresent(coverUrl, forKey: .coverUrl) } } // MARK: - MedalInfo struct MedalInfo: Codable, Identifiable { let id: Int let url: String enum CodingKeys: String, CodingKey { case id, url } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(url, forKey: .url) } } // MARK: - API Response Wrapper struct MemberAPIResponse: Codable { let code: Int let message: String let data: T enum CodingKeys: String, CodingKey { case code, message, data } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(code, forKey: .code) try container.encode(message, forKey: .message) try container.encode(data, forKey: .data) } } // MARK: - Date Formatter class DateFormatterManager { static let shared = DateFormatterManager() let iso8601Full: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() private init() {} } // MARK: - JSON Decoder Extension extension JSONDecoder { static let `default`: JSONDecoder = { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .custom { decoder -> Date in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) if let date = DateFormatterManager.shared.iso8601Full.date(from: dateString) { return date } throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)") } return decoder }() }