Skip to content

Commit b1a753e

Browse files
authored
Merge pull request #43 from ApptiveDev/dev
[Release] 1.0.17 업데이트
2 parents 57d6d25 + 6bbaa90 commit b1a753e

27 files changed

Lines changed: 1818 additions & 456 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "killingpart_music_icon_black.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "killingpart_music_icon_black 1.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"filename" : "killingpart_music_icon_black 2.png",
15+
"idiom" : "universal",
16+
"scale" : "3x"
17+
}
18+
],
19+
"info" : {
20+
"author" : "xcode",
21+
"version" : 1
22+
}
23+
}
326 Bytes
Loading
326 Bytes
Loading
326 Bytes
Loading

KillingPart/Models/DiaryModel.swift

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,266 @@ struct DiaryUpdateRequest: Encodable {
132132
struct DiaryOrderUpdateRequest: Encodable {
133133
let diaryIds: [Int]
134134
}
135+
136+
struct MyCalendarDiariesResponse: Decodable {
137+
let diariesByDate: [String: [DiaryFeedModel]]
138+
139+
var diaries: [DiaryFeedModel] {
140+
diariesByDate.values.flatMap { $0 }
141+
}
142+
143+
init(diariesByDate: [String: [DiaryFeedModel]]) {
144+
self.diariesByDate = Self.normalized(diariesByDate: diariesByDate)
145+
}
146+
147+
init(from decoder: Decoder) throws {
148+
if let feeds = try? [CalendarDiaryFeedResponse](from: decoder) {
149+
self.diariesByDate = Self.groupedByDate(from: feeds)
150+
return
151+
}
152+
153+
if let keyedFeeds = try? [String: [CalendarDiaryFeedResponse]](from: decoder) {
154+
self.diariesByDate = Self.normalized(diariesByDate: keyedFeeds)
155+
return
156+
}
157+
158+
let container = try decoder.container(keyedBy: CodingKeys.self)
159+
160+
if let feeds = container.decodeCalendarDiaryFeedArray() {
161+
self.diariesByDate = Self.groupedByDate(from: feeds)
162+
return
163+
}
164+
165+
if let groups = container.decodeCalendarGroups() {
166+
self.diariesByDate = Self.groupedByDate(from: groups)
167+
return
168+
}
169+
170+
if let dataContainer = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) {
171+
if let feeds = dataContainer.decodeCalendarDiaryFeedArray() {
172+
self.diariesByDate = Self.groupedByDate(from: feeds)
173+
return
174+
}
175+
176+
if let groups = dataContainer.decodeCalendarGroups() {
177+
self.diariesByDate = Self.groupedByDate(from: groups)
178+
return
179+
}
180+
}
181+
182+
self.diariesByDate = [:]
183+
}
184+
185+
enum CodingKeys: String, CodingKey {
186+
case content
187+
case diaries
188+
case data
189+
case items
190+
case list
191+
case result
192+
case calendar
193+
case dates
194+
}
195+
196+
private static func groupedByDate(from feeds: [CalendarDiaryFeedResponse]) -> [String: [DiaryFeedModel]] {
197+
var grouped: [String: [DiaryFeedModel]] = [:]
198+
for feed in feeds {
199+
let key = feed.calendarDateKey
200+
guard !key.isEmpty else { continue }
201+
grouped[key, default: []].append(feed.toDiaryFeedModel())
202+
}
203+
return grouped
204+
}
205+
206+
private static func groupedByDate(from groups: [CalendarDiaryGroupPayload]) -> [String: [DiaryFeedModel]] {
207+
var grouped: [String: [DiaryFeedModel]] = [:]
208+
for group in groups {
209+
let normalizedDate = normalizeDateKey(group.date)
210+
guard !normalizedDate.isEmpty else { continue }
211+
grouped[normalizedDate, default: []].append(contentsOf: group.diaries.map { $0.toDiaryFeedModel() })
212+
}
213+
return grouped
214+
}
215+
216+
private static func normalized(diariesByDate: [String: [DiaryFeedModel]]) -> [String: [DiaryFeedModel]] {
217+
var normalized: [String: [DiaryFeedModel]] = [:]
218+
for (rawKey, diaries) in diariesByDate {
219+
let key = normalizeDateKey(rawKey)
220+
guard !key.isEmpty else { continue }
221+
normalized[key, default: []].append(contentsOf: diaries)
222+
}
223+
return normalized
224+
}
225+
226+
private static func normalized(diariesByDate: [String: [CalendarDiaryFeedResponse]]) -> [String: [DiaryFeedModel]] {
227+
var normalized: [String: [DiaryFeedModel]] = [:]
228+
for (rawKey, diaries) in diariesByDate {
229+
let key = normalizeDateKey(rawKey)
230+
guard !key.isEmpty else { continue }
231+
normalized[key, default: []].append(contentsOf: diaries.map { $0.toDiaryFeedModel() })
232+
}
233+
return normalized
234+
}
235+
236+
private static func normalizeDateKey(_ rawDate: String) -> String {
237+
let trimmed = rawDate.trimmingCharacters(in: .whitespacesAndNewlines)
238+
guard !trimmed.isEmpty else { return "" }
239+
return trimmed.split(separator: "T").first.map(String.init) ?? trimmed
240+
}
241+
}
242+
243+
private extension KeyedDecodingContainer<MyCalendarDiariesResponse.CodingKeys> {
244+
func decodeCalendarDiaryFeedArray() -> [CalendarDiaryFeedResponse]? {
245+
if let value = try? decode([CalendarDiaryFeedResponse].self, forKey: .content) { return value }
246+
if let value = try? decode([CalendarDiaryFeedResponse].self, forKey: .diaries) { return value }
247+
if let value = try? decode([CalendarDiaryFeedResponse].self, forKey: .items) { return value }
248+
if let value = try? decode([CalendarDiaryFeedResponse].self, forKey: .list) { return value }
249+
if let value = try? decode([CalendarDiaryFeedResponse].self, forKey: .result) { return value }
250+
return nil
251+
}
252+
253+
func decodeCalendarGroups() -> [CalendarDiaryGroupPayload]? {
254+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .content) { return value }
255+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .data) { return value }
256+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .diaries) { return value }
257+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .items) { return value }
258+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .list) { return value }
259+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .calendar) { return value }
260+
if let value = try? decode([CalendarDiaryGroupPayload].self, forKey: .dates) { return value }
261+
return nil
262+
}
263+
}
264+
265+
private struct CalendarDiaryGroupPayload: Decodable {
266+
let date: String
267+
let diaries: [CalendarDiaryFeedResponse]
268+
269+
private enum CodingKeys: String, CodingKey {
270+
case date
271+
case day
272+
case diaryDate
273+
case targetDate
274+
case content
275+
case diaries
276+
case items
277+
case list
278+
}
279+
280+
init(from decoder: Decoder) throws {
281+
let container = try decoder.container(keyedBy: CodingKeys.self)
282+
283+
date = (try? container.decode(String.self, forKey: .date))
284+
?? (try? container.decode(String.self, forKey: .day))
285+
?? (try? container.decode(String.self, forKey: .diaryDate))
286+
?? (try? container.decode(String.self, forKey: .targetDate))
287+
?? ""
288+
289+
diaries = (try? container.decode([CalendarDiaryFeedResponse].self, forKey: .diaries))
290+
?? (try? container.decode([CalendarDiaryFeedResponse].self, forKey: .content))
291+
?? (try? container.decode([CalendarDiaryFeedResponse].self, forKey: .items))
292+
?? (try? container.decode([CalendarDiaryFeedResponse].self, forKey: .list))
293+
?? []
294+
}
295+
}
296+
297+
private struct CalendarDiaryFeedResponse: Decodable {
298+
let diaryId: Int
299+
let artist: String
300+
let musicTitle: String
301+
let albumImageUrl: String
302+
let content: String
303+
let videoUrl: String
304+
let scope: DiaryScope
305+
let duration: String
306+
let totalDuration: String
307+
let start: String
308+
let end: String
309+
let createDate: String
310+
let updateDate: String
311+
312+
private enum CodingKeys: String, CodingKey {
313+
case diaryId
314+
case artist
315+
case musicTitle
316+
case albumImageUrl
317+
case content
318+
case videoUrl
319+
case scope
320+
case duration
321+
case totalDuration
322+
case start
323+
case end
324+
case createDate
325+
case updateDate
326+
}
327+
328+
var calendarDateKey: String {
329+
let created = Self.normalizedDateKey(from: createDate)
330+
if !created.isEmpty { return created }
331+
return Self.normalizedDateKey(from: updateDate)
332+
}
333+
334+
func toDiaryFeedModel() -> DiaryFeedModel {
335+
DiaryFeedModel(
336+
diaryId: diaryId,
337+
artist: artist,
338+
musicTitle: musicTitle,
339+
albumImageUrl: albumImageUrl,
340+
content: content,
341+
videoUrl: videoUrl,
342+
scope: scope,
343+
duration: duration,
344+
totalDuration: totalDuration,
345+
start: start,
346+
end: end,
347+
createDate: createDate,
348+
updateDate: updateDate,
349+
isLiked: false,
350+
isStored: false,
351+
likeCount: 0,
352+
userId: 0,
353+
username: nil,
354+
tag: nil,
355+
profileImageUrl: nil
356+
)
357+
}
358+
359+
init(from decoder: Decoder) throws {
360+
let container = try decoder.container(keyedBy: CodingKeys.self)
361+
diaryId = (try? container.decode(Int.self, forKey: .diaryId))
362+
?? Int((try? container.decode(String.self, forKey: .diaryId)) ?? "")
363+
?? 0
364+
artist = (try? container.decode(String.self, forKey: .artist)) ?? ""
365+
musicTitle = (try? container.decode(String.self, forKey: .musicTitle)) ?? ""
366+
albumImageUrl = (try? container.decode(String.self, forKey: .albumImageUrl)) ?? ""
367+
content = (try? container.decode(String.self, forKey: .content)) ?? ""
368+
videoUrl = (try? container.decode(String.self, forKey: .videoUrl)) ?? ""
369+
scope = (try? container.decode(DiaryScope.self, forKey: .scope)) ?? .private
370+
duration = (try? container.decode(String.self, forKey: .duration)) ?? ""
371+
totalDuration = (try? container.decode(String.self, forKey: .totalDuration)) ?? ""
372+
start = (try? container.decode(String.self, forKey: .start)) ?? ""
373+
end = (try? container.decode(String.self, forKey: .end)) ?? ""
374+
createDate = (try? container.decode(String.self, forKey: .createDate)) ?? ""
375+
updateDate = (try? container.decode(String.self, forKey: .updateDate)) ?? ""
376+
}
377+
378+
private static func normalizedDateKey(from rawDate: String) -> String {
379+
let trimmed = rawDate.trimmingCharacters(in: .whitespacesAndNewlines)
380+
guard !trimmed.isEmpty else { return "" }
381+
return trimmed.split(separator: "T").first.map(String.init) ?? trimmed
382+
}
383+
}
384+
385+
extension DiaryFeedModel {
386+
var calendarDateKey: String {
387+
let created = Self.normalizedDateKey(from: createDate)
388+
if !created.isEmpty { return created }
389+
return Self.normalizedDateKey(from: updateDate)
390+
}
391+
392+
private static func normalizedDateKey(from rawDate: String) -> String {
393+
let trimmed = rawDate.trimmingCharacters(in: .whitespacesAndNewlines)
394+
guard !trimmed.isEmpty else { return "" }
395+
return trimmed.split(separator: "T").first.map(String.init) ?? trimmed
396+
}
397+
}

KillingPart/Services/AuthSessionEvents.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import Foundation
33
extension Notification.Name {
44
static let authenticationSessionExpired = Notification.Name("authenticationSessionExpired")
55
static let diaryCreated = Notification.Name("diaryCreated")
6+
static let navigateToPlayKillingPart = Notification.Name("navigateToPlayKillingPart")
67
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Foundation
2+
3+
protocol CalendarServicing {
4+
func fetchMyCalendarDiaries(startDate: String, endDate: String) async throws -> MyCalendarDiariesResponse
5+
}
6+
7+
enum CalendarServiceError: LocalizedError {
8+
case invalidResponse
9+
case serverError(statusCode: Int, message: String?)
10+
case decodingFailed
11+
case sessionExpired
12+
case networkFailure(message: String)
13+
14+
var errorDescription: String? {
15+
switch self {
16+
case .invalidResponse:
17+
return "서버 응답을 확인할 수 없어요."
18+
case .serverError(_, let message):
19+
return message ?? "캘린더 데이터를 불러오지 못했어요."
20+
case .decodingFailed:
21+
return "캘린더 응답 파싱에 실패했어요."
22+
case .sessionExpired:
23+
return "세션이 만료되었어요. 다시 로그인해 주세요."
24+
case .networkFailure(let message):
25+
return message
26+
}
27+
}
28+
}
29+
30+
struct CalendarService: CalendarServicing {
31+
private let apiClient: APIClienting
32+
33+
init(apiClient: APIClienting = APIClient.shared) {
34+
self.apiClient = apiClient
35+
}
36+
37+
func fetchMyCalendarDiaries(startDate: String, endDate: String) async throws -> MyCalendarDiariesResponse {
38+
let trimmedStartDate = startDate.trimmingCharacters(in: .whitespacesAndNewlines)
39+
let trimmedEndDate = endDate.trimmingCharacters(in: .whitespacesAndNewlines)
40+
41+
var queryItems: [URLQueryItem] = []
42+
if !trimmedStartDate.isEmpty {
43+
queryItems.append(URLQueryItem(name: "start", value: trimmedStartDate))
44+
}
45+
if !trimmedEndDate.isEmpty {
46+
queryItems.append(URLQueryItem(name: "end", value: trimmedEndDate))
47+
}
48+
49+
let request = APIRequest(
50+
path: "/diaries/my/calendar",
51+
method: .get,
52+
queryItems: queryItems,
53+
requiresAuthorization: true
54+
)
55+
56+
do {
57+
return try await apiClient.request(request, responseType: MyCalendarDiariesResponse.self)
58+
} catch {
59+
if isRequestCancelled(error) { throw error }
60+
throw mapError(error)
61+
}
62+
}
63+
64+
private func mapError(_ error: Error) -> CalendarServiceError {
65+
if let calendarError = error as? CalendarServiceError {
66+
return calendarError
67+
}
68+
69+
if let apiError = error as? APIClientError {
70+
switch apiError {
71+
case .invalidResponse:
72+
return .invalidResponse
73+
case .missingAccessToken, .missingRefreshToken, .unauthorized:
74+
return .sessionExpired
75+
case .serverError(let statusCode, let message):
76+
return .serverError(statusCode: statusCode, message: message)
77+
case .decodingFailed:
78+
return .decodingFailed
79+
}
80+
}
81+
82+
return .networkFailure(message: "캘린더 요청 중 오류가 발생했어요.")
83+
}
84+
85+
private func isRequestCancelled(_ error: Error) -> Bool {
86+
if error is CancellationError {
87+
return true
88+
}
89+
90+
let nsError = error as NSError
91+
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled
92+
}
93+
}

0 commit comments

Comments
 (0)