Skip to content

Commit dd2b064

Browse files
authored
feat - 킬링파트 재생 페이지 작업 (#37)
* feat(PlayKillingPartView): 킬링파트 재생 페이지 UI 업데이트 * feat(PlayKillingPartView): UI 디테일 수정 * feat(PlayKillingPart): 플레이리스트 뷰 UI 업데이트 * feat(PlayKillingpart): 바텀 플레이어 패널 크기 및 UI 크기 상승 * fix(PlayKillingpart): 바텀플레이어 패널, 하단 네비, body 여백 대응 * feat(PlayKillingPart): 현재 재생 컨테이너 UI 업데이트 * feat(PlayKillingPart): 음악 다이어리 정렬 순서 수정 API 연동 및 드래그 핸들 UI 적용 * fix(PlayKillingPart): 잔상 제거 * fix(PlayKillingPart): 드래그 상태 반투명 적용 * fix(PlayKillingPart): 드래그앤 드롭 UX 개선 * feat(1.0.16): 버전 업데이트
1 parent e465d6a commit dd2b064

10 files changed

Lines changed: 1284 additions & 40 deletions

File tree

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 = 15;
437+
CURRENT_PROJECT_VERSION = 16;
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.15;
462+
MARKETING_VERSION = 1.0.16;
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 = 15;
482+
CURRENT_PROJECT_VERSION = 16;
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.15;
507+
MARKETING_VERSION = 1.0.16;
508508
PRODUCT_BUNDLE_IDENTIFIER = com.killingpoint.killingpart;
509509
PRODUCT_NAME = "$(TARGET_NAME)";
510510
REGISTER_APP_GROUPS = YES;
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.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "killingpart_music_icon 1.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"filename" : "killingpart_music_icon 2.png",
15+
"idiom" : "universal",
16+
"scale" : "3x"
17+
}
18+
],
19+
"info" : {
20+
"author" : "xcode",
21+
"version" : 1
22+
}
23+
}
434 Bytes
Loading
434 Bytes
Loading
434 Bytes
Loading

KillingPart/Models/DiaryModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,7 @@ struct DiaryUpdateRequest: Encodable {
128128
try container.encodeIfPresent(end, forKey: .end)
129129
}
130130
}
131+
132+
struct DiaryOrderUpdateRequest: Encodable {
133+
let diaryIds: [Int]
134+
}

KillingPart/Services/DiaryService.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ protocol DiaryServicing {
55
func createDiary(request: DiaryCreateRequest) async throws -> DiaryCreateResult
66
func updateDiary(diaryId: Int, request: DiaryUpdateRequest) async throws
77
func deleteDiary(diaryId: Int) async throws
8+
func updateMyDiaryOrder(request: DiaryOrderUpdateRequest) async throws
89
}
910

1011
enum DiaryServiceError: LocalizedError {
@@ -136,6 +137,30 @@ struct DiaryService: DiaryServicing {
136137
}
137138
}
138139

140+
func updateMyDiaryOrder(request: DiaryOrderUpdateRequest) async throws {
141+
let requestBody: Data
142+
do {
143+
requestBody = try JSONEncoder().encode(request)
144+
} catch {
145+
throw DiaryServiceError.requestEncodingFailed
146+
}
147+
148+
do {
149+
var apiRequest = APIRequest(
150+
path: "/diaries/order",
151+
method: .patch,
152+
requiresAuthorization: true,
153+
body: requestBody
154+
)
155+
apiRequest.headers["Accept"] = "application/json"
156+
apiRequest.headers["Content-Type"] = "application/json"
157+
try await apiClient.request(apiRequest)
158+
} catch {
159+
if isRequestCancelled(error) { throw error }
160+
throw mapError(error)
161+
}
162+
}
163+
139164
private func mapError(_ error: Error) -> DiaryServiceError {
140165
if let diaryServiceError = error as? DiaryServiceError {
141166
return diaryServiceError
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Foundation
2+
3+
@MainActor
4+
final class PlayKillingPartViewModel: ObservableObject {
5+
@Published private(set) var isEditMode = false
6+
@Published private(set) var isSavingOrder = false
7+
@Published var errorMessage: String?
8+
9+
private let diaryService: DiaryServicing
10+
11+
init(diaryService: DiaryServicing = DiaryService()) {
12+
self.diaryService = diaryService
13+
}
14+
15+
func beginEditing() {
16+
guard !isSavingOrder else { return }
17+
errorMessage = nil
18+
isEditMode = true
19+
}
20+
21+
func completeEditing(with diaryIDs: [Int]) async -> Bool {
22+
guard isEditMode else { return true }
23+
guard !isSavingOrder else { return false }
24+
25+
let hasDuplicateDiaryID = Set(diaryIDs).count != diaryIDs.count
26+
if hasDuplicateDiaryID {
27+
errorMessage = "플레이리스트 순서가 올바르지 않아요."
28+
return false
29+
}
30+
31+
isSavingOrder = true
32+
errorMessage = nil
33+
defer { isSavingOrder = false }
34+
35+
do {
36+
try await diaryService.updateMyDiaryOrder(
37+
request: DiaryOrderUpdateRequest(diaryIds: diaryIDs)
38+
)
39+
isEditMode = false
40+
return true
41+
} catch {
42+
if isRequestCancelled(error) { return false }
43+
errorMessage = resolveErrorMessage(from: error)
44+
return false
45+
}
46+
}
47+
48+
func endEditingWithoutSave() {
49+
guard !isSavingOrder else { return }
50+
errorMessage = nil
51+
isEditMode = false
52+
}
53+
54+
private func resolveErrorMessage(from error: Error) -> String {
55+
if let diaryError = error as? DiaryServiceError {
56+
return diaryError.errorDescription ?? "요청 처리에 실패했어요."
57+
}
58+
59+
if let apiError = error as? APIClientError {
60+
return apiError.errorDescription ?? "요청 처리에 실패했어요."
61+
}
62+
63+
if let localizedError = error as? LocalizedError {
64+
return localizedError.errorDescription ?? "요청 처리에 실패했어요."
65+
}
66+
67+
return "요청 처리에 실패했어요."
68+
}
69+
70+
private func isRequestCancelled(_ error: Error) -> Bool {
71+
if error is CancellationError {
72+
return true
73+
}
74+
75+
let nsError = error as NSError
76+
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled
77+
}
78+
}

KillingPart/Views/Screens/Main/Add/AddSearchDetail/components/YoutubePlayerView.swift

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ struct YoutubePlayerView: UIViewRepresentable {
66
let videoURL: URL?
77
let startSeconds: Double
88
let endSeconds: Double
9+
let isPlaying: Bool
10+
11+
init(
12+
videoURL: URL?,
13+
startSeconds: Double,
14+
endSeconds: Double,
15+
isPlaying: Bool = true
16+
) {
17+
self.videoURL = videoURL
18+
self.startSeconds = startSeconds
19+
self.endSeconds = endSeconds
20+
self.isPlaying = isPlaying
21+
}
922

1023
func makeUIView(context: Context) -> WKWebView {
1124
let configuration = WKWebViewConfiguration()
@@ -36,11 +49,13 @@ struct YoutubePlayerView: UIViewRepresentable {
3649
context.coordinator.loadedVideoID = videoID
3750
context.coordinator.lastSyncedStart = targetStart
3851
context.coordinator.lastSyncedEnd = targetEnd
52+
context.coordinator.lastSyncedIsPlaying = isPlaying
3953
webView.loadHTMLString(
4054
makePlayerHTML(
4155
videoID: videoID,
4256
startSeconds: targetStart,
43-
endSeconds: targetEnd
57+
endSeconds: targetEnd,
58+
shouldAutoplay: isPlaying
4459
),
4560
baseURL: appRefererURL
4661
)
@@ -55,23 +70,49 @@ struct YoutubePlayerView: UIViewRepresentable {
5570
context.coordinator.lastSyncedEnd,
5671
targetEnd
5772
)
58-
guard !(isSameStart && isSameEnd) else { return }
59-
context.coordinator.lastSyncedStart = targetStart
60-
context.coordinator.lastSyncedEnd = targetEnd
73+
let isRangeChanged = !(isSameStart && isSameEnd)
74+
75+
let isSamePlayState = context.coordinator.lastSyncedIsPlaying == isPlaying
76+
let isPlayStateChanged = !isSamePlayState
77+
guard isRangeChanged || isPlayStateChanged else { return }
78+
79+
if isRangeChanged {
80+
context.coordinator.lastSyncedStart = targetStart
81+
context.coordinator.lastSyncedEnd = targetEnd
82+
}
83+
if isPlayStateChanged {
84+
context.coordinator.lastSyncedIsPlaying = isPlaying
85+
}
6186

6287
let targetStartJS = jsNumber(targetStart)
6388
let targetEndJS = jsNumber(targetEnd)
89+
let shouldAutoplayJS = isPlaying ? "true" : "false"
90+
let shouldForceSeekJS = (isRangeChanged || isPlayStateChanged) ? "true" : "false"
91+
let playbackControlJS = isPlaying
92+
? """
93+
if (window.kpApplyDesiredRange) {
94+
window.kpApplyDesiredRange(\(shouldForceSeekJS));
95+
} else {
96+
if (\(shouldForceSeekJS)) {
97+
window.kpPlayer.seekTo(window.kpDesiredStart, true);
98+
}
99+
window.kpPlayer.playVideo();
100+
}
101+
"""
102+
: """
103+
if (\(shouldForceSeekJS)) {
104+
window.kpPlayer.seekTo(window.kpDesiredStart, true);
105+
}
106+
window.kpPlayer.pauseVideo();
107+
"""
108+
64109
webView.evaluateJavaScript(
65110
"""
66111
window.kpDesiredStart = \(targetStartJS);
67112
window.kpDesiredEnd = \(targetEndJS);
113+
window.kpShouldAutoplay = \(shouldAutoplayJS);
68114
if (window.kpPlayerReady && window.kpPlayer) {
69-
if (window.kpApplyDesiredRange) {
70-
window.kpApplyDesiredRange(true);
71-
} else {
72-
window.kpPlayer.seekTo(window.kpDesiredStart, true);
73-
window.kpPlayer.playVideo();
74-
}
115+
\(playbackControlJS)
75116
}
76117
""",
77118
completionHandler: nil
@@ -86,6 +127,7 @@ struct YoutubePlayerView: UIViewRepresentable {
86127
var loadedVideoID: String?
87128
var lastSyncedStart: Double?
88129
var lastSyncedEnd: Double?
130+
var lastSyncedIsPlaying: Bool?
89131
}
90132

91133
private var appRefererURL: URL? {
@@ -95,12 +137,19 @@ struct YoutubePlayerView: UIViewRepresentable {
95137
return URL(string: appRefererURLString)
96138
}
97139

98-
private func makePlayerHTML(videoID: String, startSeconds: Double, endSeconds: Double) -> String {
140+
private func makePlayerHTML(
141+
videoID: String,
142+
startSeconds: Double,
143+
endSeconds: Double,
144+
shouldAutoplay: Bool
145+
) -> String {
99146
let safeVideoID = escapeForJavaScript(videoID)
100147
let safeReferer = escapeForJavaScript(appRefererURLString ?? "")
101148
let initialStart = max(Int(startSeconds.rounded(.down)), 0)
102149
let initialStartJS = jsNumber(startSeconds)
103150
let initialEndJS = jsNumber(endSeconds)
151+
let initialShouldAutoplayJS = shouldAutoplay ? "true" : "false"
152+
let autoplayFlag = shouldAutoplay ? 1 : 0
104153

105154
return """
106155
<!doctype html>
@@ -128,6 +177,7 @@ struct YoutubePlayerView: UIViewRepresentable {
128177
<script>
129178
window.kpDesiredStart = \(initialStartJS);
130179
window.kpDesiredEnd = \(initialEndJS);
180+
window.kpShouldAutoplay = \(initialShouldAutoplayJS);
131181
window.kpPlayer = null;
132182
window.kpPlayerReady = false;
133183
window.kpLoopTimer = null;
@@ -202,7 +252,7 @@ struct YoutubePlayerView: UIViewRepresentable {
202252
height: '100%',
203253
videoId: '\(safeVideoID)',
204254
playerVars: {
205-
autoplay: 1,
255+
autoplay: \(autoplayFlag),
206256
controls: 0,
207257
disablekb: 1,
208258
fs: 0,
@@ -217,11 +267,16 @@ struct YoutubePlayerView: UIViewRepresentable {
217267
events: {
218268
onReady: function() {
219269
window.kpPlayerReady = true;
220-
window.kpApplyDesiredRange(true);
221-
window.kpStartRangeLoop();
270+
if (window.kpShouldAutoplay) {
271+
window.kpApplyDesiredRange(true);
272+
window.kpStartRangeLoop();
273+
} else {
274+
window.kpPlayer.seekTo(window.kpDesiredStart, true);
275+
window.kpPlayer.pauseVideo();
276+
}
222277
},
223278
onStateChange: function(event) {
224-
if (Number(event.data) === 0) {
279+
if (window.kpShouldAutoplay && Number(event.data) === 0) {
225280
window.kpApplyDesiredRange(true);
226281
}
227282
}

0 commit comments

Comments
 (0)