diff --git a/Projects/Core/DesignSystem/Sources/View/Keyword/MemeCategoryView.swift b/Projects/Core/DesignSystem/Sources/View/Keyword/MemeCategoryView.swift index a96cf96..1e7f468 100644 --- a/Projects/Core/DesignSystem/Sources/View/Keyword/MemeCategoryView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Keyword/MemeCategoryView.swift @@ -24,7 +24,7 @@ public struct MemeCategoryView: View { } public var body: some View { - VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { HStack { Text(category) .font(Font.Body.Small.semiBold) diff --git a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift index e41731e..6c01d39 100644 --- a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift +++ b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift @@ -54,7 +54,7 @@ public enum MemeEndpoint: Requestable { return "/meme/recommend-memes" case .getSearchKeywordMemeList(_,_,let keyword): return "/meme/search/\(keyword)" - case .getSearchByTextMemeList(_,_,let text): + case .getSearchByTextMemeList(_,_,_): return "/meme/search" case .meme(let memeId): return "/meme/\(memeId)" diff --git a/Projects/Features/MemeDetail/Sources/MemeDetailView.swift b/Projects/Features/MemeDetail/Sources/MemeDetailView.swift index 1f19ba0..899f05f 100644 --- a/Projects/Features/MemeDetail/Sources/MemeDetailView.swift +++ b/Projects/Features/MemeDetail/Sources/MemeDetailView.swift @@ -27,7 +27,10 @@ public struct MemeDetailView: View { @State private var totalHeight: CGFloat = 0 @State private var memeCardHeight: CGFloat = 0 @State private var tabBarHeight: CGFloat = 0 - + @State private var isSheetPresented: Bool = false + @State private var isWebViewPresented: Bool = false + @State private var showContactUsAlert: Bool = false + private var isShortCard: Bool { memeCardHeight + tabBarHeight > totalHeight - 30 } @@ -43,8 +46,14 @@ public struct MemeDetailView: View { public var body: some View { ZStack { memeDetailCardView - if viewModel.state.isSheetPresented { + .sheet(isPresented: $isSheetPresented) { + bottomSheetView + .presentationDetents([.height(66+40)]) + } + + if isSheetPresented { Color.black.opacity(0.4) + .ignoresSafeArea([.container]) } } .onAppear { @@ -52,10 +61,27 @@ public struct MemeDetailView: View { } .plainNavigationBar( backHandler: { viewModel.dispatch(type: .naviBackButtonTapped) }, - rightActionHandler: { viewModel.dispatch(type: .naviMoreButtonTapped) }, + rightActionHandler: { isSheetPresented = true }, hasConfigureButton: true, title: viewModel.state.meme.title ) + .sheet(isPresented: $isWebViewPresented, onDismiss: { isWebViewPresented = false }) { + WebView(url: viewModel.state.reportProblemUrl) + .presentationDetents([.large]) + } + .basicModal( + isPresented: $showContactUsAlert, + opacity: 0.5, + content: { + FarmemeAlertView( + title: "문의하기", + description: "farmemebusiness@gmail.com", + dismiss: { + showContactUsAlert = false + } + ) + } + ) .popup( isActive: $viewModel.state.isCopied, image: ResourceKitAsset.Icon.copyFilled.swiftUIImage, @@ -66,16 +92,6 @@ public struct MemeDetailView: View { image: viewModel.state.meme.isFarmemed ? ResourceKitAsset.Icon.copyFilled.swiftUIImage : nil, text: viewModel.state.meme.isFarmemed ? "파밈 완료!" : "파밈을 취소했어요" ) - .sheet(isPresented: $viewModel.state.isSheetPresented) { - ZStack(alignment: .bottom) { - bottomSheetView - .presentationDetents([.height(66)]) - } - } - .sheet(isPresented: $viewModel.state.isWebViewPresented) { - WebView(url: viewModel.state.reportProblemUrl) - .presentationDetents([.large]) - } } private var memeDetailCardView: some View { @@ -153,11 +169,18 @@ public struct MemeDetailView: View { .frame(height: 16) .foregroundStyle(Color.Background.white) reportProblembutton + .onTapGesture { + isSheetPresented = false + isWebViewPresented = true + } + contactUsButton + .onTapGesture { + isSheetPresented = false + isWebViewPresented = false // 신고하기 후에 누르면, 신고하기가 떠서 강제로 막음 + showContactUsAlert = true + } } .padding(.bottom, 10) - .onTapGesture { - viewModel.dispatch(type: .reportProblemButtonTapped) - } } private var reportProblembutton: some View { @@ -167,6 +190,13 @@ public struct MemeDetailView: View { .padding(.vertical, 16) } + private var contactUsButton: some View { + Text("문의하기") + .font(Font.Body.Xlarge.medium) + .foregroundStyle(Color.Text.primary) + .padding(.vertical, 16) + } + @MainActor private func tabBarTap(_ type: MemeDetailTab) { switch type { @@ -178,7 +208,6 @@ public struct MemeDetailView: View { viewModel.dispatch(type: .shreButtonTapped) } } - } #Preview { diff --git a/Projects/Features/MemeDetail/Sources/MemeDetailViewModel.swift b/Projects/Features/MemeDetail/Sources/MemeDetailViewModel.swift index 619e58e..a3572e5 100644 --- a/Projects/Features/MemeDetail/Sources/MemeDetailViewModel.swift +++ b/Projects/Features/MemeDetail/Sources/MemeDetailViewModel.swift @@ -29,17 +29,12 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { case shreButtonTapped case farmemeButtonTapped case naviBackButtonTapped - case naviMoreButtonTapped - case reportProblemButtonTapped } public struct State { var meme: MemeDetail var isCopied: Bool = false var isFarmemeChanged: Bool = false - var isSheetPresented: Bool = false - var isWebViewPresented: Bool = false - let reportProblemUrl: URL? = URL(string: "https://forms.gle/a5QkMnLD8AANtYCo7") } @@ -57,8 +52,6 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { private var reactionTask: Task? private let trotller = Throttler(seconds: 3) - - // MARK: - Initializers @@ -70,7 +63,7 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { watchMemeUseCase: WatchMemeUseCase, reactToMemeUseCase: ReactToMemeUseCase ) { - print("memeviewmodel init") + debugPrint("memeviewmodel init") self.router = router self.state = State(meme: meme) self.bookmarkMemeUseCase = bookmarkMemeUseCase @@ -80,18 +73,18 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { } deinit { - print("memeviewmodel deinit") - reactionTask?.cancel() + debugPrint("memeviewmodel deinit") + reactionTask?.cancel() } // MARK: - Methods public func dispatch(type: Action) { Task { @MainActor in - print("type: \(type)") + debugPrint("type: \(type)") switch type { case .likeButtonTapped: - await postReaction() + postReaction() case .copyButtonTapped: await copyImage() case .shreButtonTapped: @@ -103,13 +96,8 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { await postSavedFarmeme() } case .naviBackButtonTapped: + await sendReactions() router?.popView() - case .naviMoreButtonTapped: - state.isSheetPresented = true - case .reportProblemButtonTapped: - state.isSheetPresented = false - state.isWebViewPresented = true - print("reportProblemButtonTapped") } } } @@ -130,37 +118,36 @@ public final class MemeDetailViewModel: ViewModelType, ObservableObject { } private extension MemeDetailViewModel { - + @MainActor func postReaction() { - reactionCount += 1 - self.state.meme.reaction += 1 - self.state.meme.isReaction = true - self.logMemeDetail(event: .reaction) + reactionCount += 1 + self.state.meme.reaction += 1 + self.state.meme.isReaction = true + self.logMemeDetail(event: .reaction) - trotller.throttle { - await self.sendReactions() - } + trotller.throttle { + await self.sendReactions() + } } - - + @MainActor func sendReactions() async { - let count = reactionCount - guard count > 0 else { - // 전송할 리액션이 없음 - return - } - reactionCount = 0 - do { - let count = try await reactToMemeUseCase.execute(memeId: state.meme.id, count: count) - print("currentMeme count: \(self.state.meme.reaction)") - print("new count: \(count)") - self.state.meme.reaction = count - print("Reactions sent successfully with count: \(count)") - } catch { - print("Failed to send reactions: \(error)") - } + let count = reactionCount + guard count > 0 else { + // 전송할 리액션이 없음 + return + } + reactionCount = 0 + do { + let count = try await reactToMemeUseCase.execute(memeId: state.meme.id, count: count) + debugPrint("currentMeme count: \(self.state.meme.reaction)") + debugPrint("new count: \(count)") + self.state.meme.reaction = count + debugPrint("Reactions sent successfully with count: \(count)") + } catch { + debugPrint("Failed to send reactions: \(error)") + } } @MainActor @@ -177,7 +164,7 @@ private extension MemeDetailViewModel { state.isCopied = true self.logMemeDetail(event: .copy) } catch { - print("Failed to load image data: \(error)") + debugPrint("Failed to load image data: \(error)") } } @@ -192,7 +179,7 @@ private extension MemeDetailViewModel { self.logMemeDetail(event: .save) } catch { // TODO: - 에러처리 - print(error) + debugPrint(error) } } @@ -207,7 +194,7 @@ private extension MemeDetailViewModel { self.logMemeDetail(event: .saveCancel) } catch { // TODO: - 에러처리 - print(error) + debugPrint(error) } } @@ -220,7 +207,7 @@ private extension MemeDetailViewModel { self.logMemeDetail(event: .share) } catch { // TODO: - 에러처리 - print(error) + debugPrint(error) } } } diff --git a/Projects/Features/MemeEditor/Sources/MemeEditorView.swift b/Projects/Features/MemeEditor/Sources/MemeEditorView.swift index 2f5f01f..02c292a 100644 --- a/Projects/Features/MemeEditor/Sources/MemeEditorView.swift +++ b/Projects/Features/MemeEditor/Sources/MemeEditorView.swift @@ -144,6 +144,7 @@ struct MemeEditorView: View { keywordTags: keywordTags ) { keyword in viewModel.dispatch(type: .memeKeywordTapped(keyword: keyword)) + endTextEditing() } } .id(viewModel.state.memeCategories.count) diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift index d4d67e4..eff5181 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendView.swift @@ -18,8 +18,6 @@ import PPACNetwork import DesignSystem - - public struct RecommendView: View { @ObservedObject private var viewModel: RecommendViewModel @@ -71,6 +69,11 @@ public struct RecommendView: View { .onReadSize { size in memeImageHeight = size.height } + .onTapGesture { + if let currentMeme { + viewModel.router?.showMemeDetailView(meme: currentMeme) + } + } Spacer() } @@ -126,6 +129,9 @@ public struct RecommendView: View { ) ) .edgesIgnoringSafeArea(.bottom) + .onAppear { + viewModel.dispatch(type: .viewInitialized) + } .onChange(of: viewModel.state.isSuccessFetch) { withAnimation(.spring()) { currentOffsetY = viewModel.state.isSuccessFetch ? .zero : 20 diff --git a/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift b/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift index d6d613b..59b0bed 100644 --- a/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift +++ b/Projects/Features/Recommend/Sources/Presentation/RecommendViewModel.swift @@ -143,7 +143,7 @@ private extension RecommendViewModel { @MainActor func getRecommendAndUser() async { do { - let recommendMemeSize = 5 + let recommendMemeSize = 20 let recommendMemes = try await getRecommendMemesUseCase.execute(size: recommendMemeSize) let user = try await getUserInfoUseCase.execute() print("👍memeids: \(recommendMemes.map { $0.id })") diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift b/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift index 17b10c5..e2fe137 100644 --- a/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift +++ b/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift @@ -46,7 +46,8 @@ public final class SearchResultRouter: Router, SearchResultRouting { text: text, router: self, searchKeywordUseCase: SearchKeywordUseCaseImpl(repository: repository), - searchByTextUseCase: SearchByTextUseCaseImpl(repository: repository), + searchByTextUseCase: SearchByTextUseCaseImpl(repository: repository), + getMemeDetailUseCase: GetMemeDetailUseCaseImpl(repository: repository), copyImageUseCase: CopyImageUseCaseImpl(), watchMemeUseCase: WatchMemeUseCaseImpl(repository: repository) )) diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultView.swift b/Projects/Features/Search/Sources/View/Result/SearchResultView.swift index 4202306..43e4ba1 100644 --- a/Projects/Features/Search/Sources/View/Result/SearchResultView.swift +++ b/Projects/Features/Search/Sources/View/Result/SearchResultView.swift @@ -53,6 +53,9 @@ public struct SearchResultView: View { } } } + .refreshable { + viewModel.dispatch(type: .refresh) + } } .onAppear { viewModel.dispatch(type: .viewWillAppear) diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift b/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift index 8c098e5..b70af83 100644 --- a/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift +++ b/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift @@ -27,6 +27,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { public enum Action { case viewWillAppear + case refresh case search(text: String) case memeDetailTapped(meme: MemeDetail) case memeCopyTapped(meme: MemeDetail) @@ -39,6 +40,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { var text: String var memeList: [MemeDetail] var memePagination: MemeListWithPagination.Pagination + var currentMeme: MemeDetail? var isActiveCopyPopup: Bool = false var isLoading: Bool = true } @@ -50,6 +52,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { private let searchKeywordUseCase: SearchKeywordUseCase private let searchByTextUseCase: SearchByTextUseCase + private let getMemeDetailUseCase: GetMemeDetailUseCase private let copyImageUseCase: CopyImageUseCase private let watchMemeUseCase: WatchMemeUseCase @@ -61,6 +64,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { router: SearchResultRouting?, searchKeywordUseCase: SearchKeywordUseCase, searchByTextUseCase: SearchByTextUseCase, + getMemeDetailUseCase: GetMemeDetailUseCase, copyImageUseCase: CopyImageUseCase, watchMemeUseCase: WatchMemeUseCase ) { @@ -73,6 +77,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { ) self.searchKeywordUseCase = searchKeywordUseCase self.searchByTextUseCase = searchByTextUseCase + self.getMemeDetailUseCase = getMemeDetailUseCase self.copyImageUseCase = copyImageUseCase self.watchMemeUseCase = watchMemeUseCase } @@ -85,12 +90,20 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { switch type { case .viewWillAppear: await fetchData() + await refreshCurrentMeme() + case .refresh: + if state.text.isEmpty == false { + await fetchData(with: state.text) + } else if state.keyword.isEmpty == false { + await fetchData(with: state.keyword) + } case .search(text: let text): await fetchData(with: text) case .memeDetailTapped(let meme): router?.showMemeDetail(memeDetail: meme) logSearch(event: .meme, keyword: state.keyword) await postShownMeme(memeId: meme.id) + state.currentMeme = meme case .memeCopyTapped(let meme): await copyImage(meme: meme) break @@ -154,6 +167,22 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject { } } + @MainActor + private func refreshCurrentMeme() async { + guard let currentMeme = state.currentMeme else { return } + + do { + let meme = try await getMemeDetailUseCase.execute(memeId: currentMeme.id) + if let index = state.memeList.firstIndex(where: { $0.id == meme.id }) { + state.memeList[index] = meme + } + } catch(let error) { + debugPrint("error = \(error)") + } + + state.currentMeme = nil + } + @MainActor private func copyImage(meme: MemeDetail) async { do { diff --git a/Projects/Features/Search/Sources/View/SearchView.swift b/Projects/Features/Search/Sources/View/SearchView.swift index d691f4f..117f183 100644 --- a/Projects/Features/Search/Sources/View/SearchView.swift +++ b/Projects/Features/Search/Sources/View/SearchView.swift @@ -51,6 +51,9 @@ public struct SearchView: View { .frame(height: 64 + 50) } .scrollIndicators(.hidden) + .onTapGesture { + endTextEditing() + } } .onAppear { viewModel.dispatch(type: .viewWillAppear)