Skip to content

Commit e465d6a

Browse files
authored
fix - QA 반영 (#34)
* fix(diary): videoUrl 필드 유튜브 id로 변경 * fix: 백그라운드 상태 복귀 시 유튜브 영상 재생 이슈해결
1 parent 0cfbbef commit e465d6a

6 files changed

Lines changed: 164 additions & 10 deletions

File tree

KillingPart/ViewModels/Add/AddSearchDetailViewModel.swift

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,87 @@ final class AddSearchDetailViewModel: ObservableObject {
287287
}
288288

289289
private var videoURLForSave: String? {
290-
if let embedURL = selectedVideo?.embedURL?.absoluteString {
291-
return embedURL
290+
guard let selectedVideo else { return nil }
291+
292+
if let normalizedVideoID = normalizedYouTubeVideoID(from: selectedVideo.id) {
293+
return normalizedVideoID
292294
}
293295

294-
guard let selectedVideo else { return nil }
295-
let videoID = selectedVideo.id.trimmingCharacters(in: .whitespacesAndNewlines)
296-
guard !videoID.isEmpty else { return nil }
297-
return "https://www.youtube.com/watch?v=\(videoID)"
296+
if
297+
let embedURLString = selectedVideo.embedURL?.absoluteString,
298+
let normalizedVideoID = normalizedYouTubeVideoID(from: embedURLString)
299+
{
300+
return normalizedVideoID
301+
}
302+
303+
return nil
304+
}
305+
306+
private func normalizedYouTubeVideoID(from value: String) -> String? {
307+
let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
308+
guard !trimmedValue.isEmpty else { return nil }
309+
310+
if let extractedVideoID = extractYouTubeVideoID(from: trimmedValue) {
311+
return extractedVideoID
312+
}
313+
314+
if
315+
!trimmedValue.contains("/"),
316+
!trimmedValue.contains("?"),
317+
!trimmedValue.contains("&"),
318+
!trimmedValue.contains("="),
319+
!trimmedValue.contains(".")
320+
{
321+
return trimmedValue
322+
}
323+
324+
return nil
325+
}
326+
327+
private func extractYouTubeVideoID(from value: String) -> String? {
328+
guard let components = URLComponents(string: value) else {
329+
return nil
330+
}
331+
332+
let pathComponents = components.path.split(separator: "/").map(String.init)
333+
if let embedIndex = pathComponents.firstIndex(of: "embed"),
334+
pathComponents.indices.contains(embedIndex + 1) {
335+
let candidate = pathComponents[embedIndex + 1]
336+
if !candidate.isEmpty {
337+
return candidate
338+
}
339+
}
340+
341+
if let shortsIndex = pathComponents.firstIndex(of: "shorts"),
342+
pathComponents.indices.contains(shortsIndex + 1) {
343+
let candidate = pathComponents[shortsIndex + 1]
344+
if !candidate.isEmpty {
345+
return candidate
346+
}
347+
}
348+
349+
if let liveIndex = pathComponents.firstIndex(of: "live"),
350+
pathComponents.indices.contains(liveIndex + 1) {
351+
let candidate = pathComponents[liveIndex + 1]
352+
if !candidate.isEmpty {
353+
return candidate
354+
}
355+
}
356+
357+
if
358+
let host = components.host?.lowercased(),
359+
host.contains("youtu.be"),
360+
let firstPath = pathComponents.first,
361+
!firstPath.isEmpty
362+
{
363+
return firstPath
364+
}
365+
366+
if let watchVideoID = components.queryItems?.first(where: { $0.name == "v" })?.value,
367+
!watchVideoID.isEmpty {
368+
return watchVideoID
369+
}
370+
371+
return nil
298372
}
299373
}

KillingPart/ViewModels/My/MyCollection/MyCollectionViewModel.swift

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,12 @@ final class MyCollectionViewModel: ObservableObject {
238238

239239
do {
240240
let response = try await diaryService.fetchMyFeeds(page: page, size: size)
241+
let normalizedFeeds = normalizeFeedVideoURLs(in: response.content)
241242
if mode == .initial {
242-
myFeeds = response.content
243+
myFeeds = normalizedFeeds
243244
} else {
244245
let existingFeedIDs = Set(myFeeds.map(\.id))
245-
let newFeeds = response.content.filter { !existingFeedIDs.contains($0.id) }
246+
let newFeeds = normalizedFeeds.filter { !existingFeedIDs.contains($0.id) }
246247
myFeeds.append(contentsOf: newFeeds)
247248
if newFeeds.isEmpty {
248249
hasLoadedMyFeeds = true
@@ -264,6 +265,38 @@ final class MyCollectionViewModel: ObservableObject {
264265
}
265266
}
266267

268+
private func normalizeFeedVideoURLs(in feeds: [DiaryFeedModel]) -> [DiaryFeedModel] {
269+
feeds.map { feed in
270+
let normalizedVideoURL = resolvedVideoURLForPlayback(from: feed.videoUrl)
271+
guard normalizedVideoURL != feed.videoUrl else { return feed }
272+
return feed.replacingVideoURL(normalizedVideoURL)
273+
}
274+
}
275+
276+
private func resolvedVideoURLForPlayback(from rawVideoURL: String) -> String {
277+
let trimmedVideoURL = rawVideoURL.trimmingCharacters(in: .whitespacesAndNewlines)
278+
guard !trimmedVideoURL.isEmpty else { return rawVideoURL }
279+
guard isLikelyYouTubeVideoID(trimmedVideoURL) else { return trimmedVideoURL }
280+
return "https://www.youtube.com/embed/\(trimmedVideoURL)?playsinline=1"
281+
}
282+
283+
private func isLikelyYouTubeVideoID(_ value: String) -> Bool {
284+
if value.hasPrefix("//") {
285+
return false
286+
}
287+
288+
if let components = URLComponents(string: value),
289+
components.scheme != nil || components.host != nil {
290+
return false
291+
}
292+
293+
return !value.contains("/")
294+
&& !value.contains("?")
295+
&& !value.contains("&")
296+
&& !value.contains("=")
297+
&& !value.contains(".")
298+
}
299+
267300
private enum FeedLoadMode {
268301
case initial
269302
case pagination
@@ -327,3 +360,30 @@ final class MyCollectionViewModel: ObservableObject {
327360
}
328361
}
329362
}
363+
364+
private extension DiaryFeedModel {
365+
func replacingVideoURL(_ newVideoURL: String) -> DiaryFeedModel {
366+
DiaryFeedModel(
367+
diaryId: diaryId,
368+
artist: artist,
369+
musicTitle: musicTitle,
370+
albumImageUrl: albumImageUrl,
371+
content: content,
372+
videoUrl: newVideoURL,
373+
scope: scope,
374+
duration: duration,
375+
totalDuration: totalDuration,
376+
start: start,
377+
end: end,
378+
createDate: createDate,
379+
updateDate: updateDate,
380+
isLiked: isLiked,
381+
isStored: isStored,
382+
likeCount: likeCount,
383+
userId: userId,
384+
username: username,
385+
tag: tag,
386+
profileImageUrl: profileImageUrl
387+
)
388+
}
389+
}

KillingPart/Views/Screens/Main/Add/AddSearchDetail/AddSearchDetailView.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import SwiftUI
22

33
struct AddSearchDetailView: View {
44
@Environment(\.dismiss) private var dismiss
5+
@Environment(\.scenePhase) private var scenePhase
56
@StateObject private var viewModel: AddSearchDetailViewModel
67
@State private var isForwardStepTransition = true
8+
@State private var playerReloadToken = UUID()
79
private let onSaved: (() -> Void)?
810

911
init(
@@ -20,7 +22,10 @@ struct AddSearchDetailView: View {
2022

2123
ScrollView {
2224
VStack(alignment: .leading, spacing: AppSpacing.m) {
23-
AddSearchDetailVideoSection(viewModel: viewModel)
25+
AddSearchDetailVideoSection(
26+
viewModel: viewModel,
27+
playerReloadToken: playerReloadToken
28+
)
2429
AddSearchDetailTrackInfoSection(track: viewModel.track)
2530
detailInputSection
2631
.clipped()
@@ -41,6 +46,10 @@ struct AddSearchDetailView: View {
4146
.task {
4247
await viewModel.loadIfNeeded()
4348
}
49+
.onChange(of: scenePhase) { phase in
50+
guard phase == .active else { return }
51+
playerReloadToken = UUID()
52+
}
4453
.navigationTitle("")
4554
.navigationBarTitleDisplayMode(.inline)
4655
.toolbar(.visible, for: .navigationBar)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import SwiftUI
22

33
struct AddSearchDetailVideoSection: View {
44
@ObservedObject var viewModel: AddSearchDetailViewModel
5+
let playerReloadToken: UUID
56
private let videoAspectRatio: CGFloat = 16 / 9
67
private let videoCornerRadius: CGFloat = 16
78

@@ -18,6 +19,7 @@ struct AddSearchDetailVideoSection: View {
1819
startSeconds: viewModel.startSeconds,
1920
endSeconds: viewModel.endSeconds
2021
)
22+
.id(playerReloadToken)
2123
.frame(maxWidth: .infinity)
2224
.aspectRatio(videoAspectRatio, contentMode: .fit)
2325
.allowsHitTesting(false)

KillingPart/Views/Screens/Main/My/MyCollection/[diaryId]/MyCollectionDiary.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import UIKit
33

44
struct MyCollectionDiary: View {
55
@Environment(\.dismiss) private var dismiss
6+
@Environment(\.scenePhase) private var scenePhase
67
@FocusState private var isCommentEditorFocused: Bool
78

89
let diaryId: Int
@@ -12,6 +13,7 @@ struct MyCollectionDiary: View {
1213
@StateObject private var viewModel: MyCollectionDiaryViewModel
1314
@State private var isDeleteDialogPresented = false
1415
@State private var keyboardHeight: CGFloat = 0
16+
@State private var playerReloadToken = UUID()
1517

1618
private let commentFocusAnchorID = "my-collection-diary-comment-focus-anchor"
1719

@@ -59,7 +61,8 @@ struct MyCollectionDiary: View {
5961
MyCollectionDiaryVideoSection(
6062
videoURL: videoURL,
6163
startSeconds: viewModel.startSeconds,
62-
endSeconds: viewModel.endSeconds
64+
endSeconds: viewModel.endSeconds,
65+
playerReloadToken: playerReloadToken
6366
)
6467

6568
MyCollectionDiaryTrackSection(
@@ -133,6 +136,10 @@ struct MyCollectionDiary: View {
133136
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { notification in
134137
updateKeyboardHeight(from: notification)
135138
}
139+
.onChange(of: scenePhase) { phase in
140+
guard phase == .active else { return }
141+
playerReloadToken = UUID()
142+
}
136143
.navigationTitle("")
137144
.navigationBarTitleDisplayMode(.inline)
138145
.toolbar(.visible, for: .navigationBar)

KillingPart/Views/Screens/Main/My/MyCollection/[diaryId]/components/MyCollectionDiaryVideoSection.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct MyCollectionDiaryVideoSection: View {
44
let videoURL: URL?
55
let startSeconds: Double
66
let endSeconds: Double
7+
let playerReloadToken: UUID
78

89
private let videoAspectRatio: CGFloat = 16 / 9
910
private let videoCornerRadius: CGFloat = 16
@@ -14,6 +15,7 @@ struct MyCollectionDiaryVideoSection: View {
1415
startSeconds: startSeconds,
1516
endSeconds: endSeconds
1617
)
18+
.id(playerReloadToken)
1719
.frame(maxWidth: .infinity)
1820
.aspectRatio(videoAspectRatio, contentMode: .fill)
1921
.allowsHitTesting(false)

0 commit comments

Comments
 (0)