Skip to content

Commit 48f04a7

Browse files
authored
feat - 내 컬렉션 음악 다이어리 조회,삭제,수정 (#29)
* feat(MyCollectionDiary): 마이컬렉션 다이어리 페이지 추가 * feat(MyCollectionDairy): 다이어리 수정,삭제 API 연동 및 기능 추가 * feat(MyCollection): 다이어리 삭제 이후 데이터 refetch * fix(MyCollectionDiary): UI 필요없는 부분 삭제 * feat(MyCollectionDiary): 시작,끝 초 겹침 이슈 개선 * fix(MyCollectionDiary): 워닝 제거 * feat(MyCollectionDiary): 본인 태그 적용 * fix(MyCollectionDiary): UI 배치 수정 * refactor(MyCollectionDiary): 컴포넌트 분리 * fix(MyCollectionDiary): 상하단 safearea 대응 * feat(1.0.13): 버전 업데이트
1 parent 4fb46d6 commit 48f04a7

12 files changed

Lines changed: 776 additions & 9 deletions

KillingPart.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
435435
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
436436
CODE_SIGN_STYLE = Automatic;
437-
CURRENT_PROJECT_VERSION = 12;
437+
CURRENT_PROJECT_VERSION = 13;
438438
DEAD_CODE_STRIPPING = YES;
439439
DEVELOPMENT_TEAM = GQ89YG5G9R;
440440
ENABLE_APP_SANDBOX = YES;
@@ -459,7 +459,7 @@
459459
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
460460
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
461461
MACOSX_DEPLOYMENT_TARGET = 14.0;
462-
MARKETING_VERSION = 1.0.12;
462+
MARKETING_VERSION = 1.0.13;
463463
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
464464
PRODUCT_NAME = "$(TARGET_NAME)";
465465
REGISTER_APP_GROUPS = YES;
@@ -479,7 +479,7 @@
479479
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
480480
CODE_SIGN_ENTITLEMENTS = KillingPart/KillingPart.entitlements;
481481
CODE_SIGN_STYLE = Automatic;
482-
CURRENT_PROJECT_VERSION = 12;
482+
CURRENT_PROJECT_VERSION = 13;
483483
DEAD_CODE_STRIPPING = YES;
484484
DEVELOPMENT_TEAM = GQ89YG5G9R;
485485
ENABLE_APP_SANDBOX = YES;
@@ -504,7 +504,7 @@
504504
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
505505
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
506506
MACOSX_DEPLOYMENT_TARGET = 14.0;
507-
MARKETING_VERSION = 1.0.12;
507+
MARKETING_VERSION = 1.0.13;
508508
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
509509
PRODUCT_NAME = "$(TARGET_NAME)";
510510
REGISTER_APP_GROUPS = YES;

KillingPart/Models/DiaryModel.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,43 @@ struct DiaryCreateResult {
8888
let diaryId: Int?
8989
let location: String?
9090
}
91+
92+
struct DiaryUpdateRequest: Encodable {
93+
var artist: String?
94+
var musicTitle: String?
95+
var albumImageUrl: String?
96+
var videoUrl: String?
97+
var scope: DiaryScope?
98+
var content: String?
99+
var duration: String?
100+
var totalDuration: String?
101+
var start: String?
102+
var end: String?
103+
104+
enum CodingKeys: String, CodingKey {
105+
case artist
106+
case musicTitle
107+
case albumImageUrl
108+
case videoUrl
109+
case scope
110+
case content
111+
case duration
112+
case totalDuration
113+
case start
114+
case end
115+
}
116+
117+
func encode(to encoder: Encoder) throws {
118+
var container = encoder.container(keyedBy: CodingKeys.self)
119+
try container.encodeIfPresent(artist, forKey: .artist)
120+
try container.encodeIfPresent(musicTitle, forKey: .musicTitle)
121+
try container.encodeIfPresent(albumImageUrl, forKey: .albumImageUrl)
122+
try container.encodeIfPresent(videoUrl, forKey: .videoUrl)
123+
try container.encodeIfPresent(scope, forKey: .scope)
124+
try container.encodeIfPresent(content, forKey: .content)
125+
try container.encodeIfPresent(duration, forKey: .duration)
126+
try container.encodeIfPresent(totalDuration, forKey: .totalDuration)
127+
try container.encodeIfPresent(start, forKey: .start)
128+
try container.encodeIfPresent(end, forKey: .end)
129+
}
130+
}

KillingPart/Services/DiaryService.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Foundation
33
protocol DiaryServicing {
44
func fetchMyFeeds(page: Int, size: Int) async throws -> MyDiaryFeedsResponse
55
func createDiary(request: DiaryCreateRequest) async throws -> DiaryCreateResult
6+
func updateDiary(diaryId: Int, request: DiaryUpdateRequest) async throws
7+
func deleteDiary(diaryId: Int) async throws
68
}
79

810
enum DiaryServiceError: LocalizedError {
@@ -96,6 +98,44 @@ struct DiaryService: DiaryServicing {
9698
}
9799
}
98100

101+
func updateDiary(diaryId: Int, request: DiaryUpdateRequest) async throws {
102+
let requestBody: Data
103+
do {
104+
requestBody = try JSONEncoder().encode(request)
105+
} catch {
106+
throw DiaryServiceError.requestEncodingFailed
107+
}
108+
109+
do {
110+
var apiRequest = APIRequest(
111+
path: "/diaries/\(diaryId)",
112+
method: .put,
113+
requiresAuthorization: true,
114+
body: requestBody
115+
)
116+
apiRequest.headers["Accept"] = "application/json"
117+
apiRequest.headers["Content-Type"] = "application/json"
118+
try await apiClient.request(apiRequest)
119+
} catch {
120+
if isRequestCancelled(error) { throw error }
121+
throw mapError(error)
122+
}
123+
}
124+
125+
func deleteDiary(diaryId: Int) async throws {
126+
do {
127+
let request = APIRequest(
128+
path: "/diaries/\(diaryId)",
129+
method: .delete,
130+
requiresAuthorization: true
131+
)
132+
try await apiClient.request(request)
133+
} catch {
134+
if isRequestCancelled(error) { throw error }
135+
throw mapError(error)
136+
}
137+
}
138+
99139
private func mapError(_ error: Error) -> DiaryServiceError {
100140
if let diaryServiceError = error as? DiaryServiceError {
101141
return diaryServiceError

KillingPart/Utils/TimeFormatter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ enum TimeFormatter {
88
let minutes = safeSeconds / 60
99
let remainingSeconds = safeSeconds % 60
1010
let secondText = remainingSeconds < 10 ? "0\(remainingSeconds)" : "\(remainingSeconds)"
11-
return "\(minutes):\(secondText)"
11+
return "\(minutes):\(secondText)"
1212
}
1313

1414
private static func normalizedSeconds(from seconds: Double) -> Int {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import Foundation
2+
3+
@MainActor
4+
final class MyCollectionDiaryViewModel: ObservableObject {
5+
@Published private(set) var diary: DiaryFeedModel
6+
@Published var displayedStart: String
7+
@Published var displayedEnd: String
8+
@Published var displayedContent: String
9+
@Published var editContentDraft: String
10+
@Published var isEditMode = false
11+
@Published private(set) var isProcessing = false
12+
@Published private(set) var isDeleted = false
13+
@Published var errorMessage: String?
14+
15+
private let diaryService: DiaryServicing
16+
17+
init(
18+
diary: DiaryFeedModel,
19+
diaryService: DiaryServicing = DiaryService()
20+
) {
21+
self.diary = diary
22+
self.diaryService = diaryService
23+
self.displayedStart = diary.start
24+
self.displayedEnd = diary.end
25+
self.displayedContent = diary.content
26+
self.editContentDraft = diary.content
27+
}
28+
29+
var startSeconds: Double {
30+
parsedSeconds(from: displayedStart) ?? 0
31+
}
32+
33+
var endSeconds: Double {
34+
let parsedEnd = parsedSeconds(from: displayedEnd) ?? startSeconds
35+
return max(parsedEnd, startSeconds + 0.1)
36+
}
37+
38+
var totalSeconds: Double {
39+
let parsedTotal = parsedSeconds(from: diary.totalDuration) ?? 0
40+
return max(parsedTotal, endSeconds, 1)
41+
}
42+
43+
var startMinuteSecondText: String {
44+
TimeFormatter.minuteSecondText(from: startSeconds)
45+
}
46+
47+
var endMinuteSecondText: String {
48+
TimeFormatter.minuteSecondText(from: endSeconds)
49+
}
50+
51+
var canSubmitEdit: Bool {
52+
!isProcessing
53+
&& !trimmedEditContent.isEmpty
54+
&& trimmedEditContent != trimmedDisplayedContent
55+
}
56+
57+
func beginEdit() {
58+
guard !isProcessing else { return }
59+
editContentDraft = displayedContent
60+
errorMessage = nil
61+
isEditMode = true
62+
}
63+
64+
func cancelEdit() {
65+
guard !isProcessing else { return }
66+
editContentDraft = displayedContent
67+
errorMessage = nil
68+
isEditMode = false
69+
}
70+
71+
func submitEdit() async -> Bool {
72+
guard !isProcessing else { return false }
73+
guard canSubmitEdit else {
74+
errorMessage = "코멘트를 입력해 주세요."
75+
return false
76+
}
77+
78+
let payload = trimmedEditContent
79+
let request = DiaryUpdateRequest(
80+
content: payload
81+
)
82+
83+
isProcessing = true
84+
errorMessage = nil
85+
defer { isProcessing = false }
86+
87+
do {
88+
try await diaryService.updateDiary(diaryId: diary.diaryId, request: request)
89+
displayedContent = payload
90+
isEditMode = false
91+
NotificationCenter.default.post(name: .diaryCreated, object: nil)
92+
return true
93+
} catch {
94+
if isRequestCancelled(error) { return false }
95+
errorMessage = resolveErrorMessage(from: error)
96+
return false
97+
}
98+
}
99+
100+
func deleteDiary() async -> Bool {
101+
guard !isProcessing else { return false }
102+
103+
isProcessing = true
104+
errorMessage = nil
105+
defer { isProcessing = false }
106+
107+
do {
108+
try await diaryService.deleteDiary(diaryId: diary.diaryId)
109+
isDeleted = true
110+
NotificationCenter.default.post(name: .diaryCreated, object: nil)
111+
return true
112+
} catch {
113+
if isRequestCancelled(error) { return false }
114+
errorMessage = resolveErrorMessage(from: error)
115+
return false
116+
}
117+
}
118+
119+
private var trimmedEditContent: String {
120+
editContentDraft.trimmingCharacters(in: .whitespacesAndNewlines)
121+
}
122+
123+
private var trimmedDisplayedContent: String {
124+
displayedContent.trimmingCharacters(in: .whitespacesAndNewlines)
125+
}
126+
127+
private func parsedSeconds(from value: String) -> Double? {
128+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
129+
guard !trimmed.isEmpty else { return nil }
130+
131+
if let raw = Double(trimmed) {
132+
return max(raw, 0)
133+
}
134+
135+
let sanitized = trimmed.replacingOccurrences(of: "", with: "")
136+
if sanitized.contains(":") {
137+
let parts = sanitized.split(separator: ":").map(String.init)
138+
guard
139+
parts.count == 2,
140+
let minutes = Double(parts[0]),
141+
let seconds = Double(parts[1])
142+
else {
143+
return nil
144+
}
145+
return max((minutes * 60) + seconds, 0)
146+
}
147+
148+
if let raw = Double(sanitized) {
149+
return max(raw, 0)
150+
}
151+
152+
return nil
153+
}
154+
155+
private func resolveErrorMessage(from error: Error) -> String {
156+
if let diaryError = error as? DiaryServiceError {
157+
return diaryError.errorDescription ?? "요청 처리에 실패했어요."
158+
}
159+
160+
if let localizedError = error as? LocalizedError {
161+
return localizedError.errorDescription ?? "요청 처리에 실패했어요."
162+
}
163+
164+
return "요청 처리에 실패했어요."
165+
}
166+
167+
private func isRequestCancelled(_ error: Error) -> Bool {
168+
if error is CancellationError {
169+
return true
170+
}
171+
172+
let nsError = error as NSError
173+
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled
174+
}
175+
}

KillingPart/ViewModels/My/MyCollection/MyCollectionViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ final class MyCollectionViewModel: ObservableObject {
135135
return datePart.replacingOccurrences(of: "-", with: ".")
136136
}
137137

138+
func removeMyFeedLocally(diaryId: Int) {
139+
myFeeds.removeAll { $0.diaryId == diaryId }
140+
}
141+
138142
func logout(onSuccess: @escaping () -> Void) {
139143
guard !isProcessing else { return }
140144

0 commit comments

Comments
 (0)