From 28d0997beab5b19418b7ee3813ec1ff225c277a0 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 10:20:34 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[#368]=20=EC=9D=B8=EA=B8=B0=20=EB=8B=B5?= =?UTF-8?q?=EB=B3=80=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GradientStroke/Contents.json | 6 ++ .../popularEnd.colorset/Contents.json | 20 +++++ .../popularStart.colorset/Contents.json | 20 +++++ .../Data/Repository/AnswerRepository.swift | 3 +- .../UIComponent/QPPopularAnswerCell.swift | 82 +++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/Contents.json create mode 100644 Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularEnd.colorset/Contents.json create mode 100644 Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularStart.colorset/Contents.json create mode 100644 Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift diff --git a/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/Contents.json b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularEnd.colorset/Contents.json b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularEnd.colorset/Contents.json new file mode 100644 index 00000000..671d55fa --- /dev/null +++ b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularEnd.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x61", + "green" : "0x42", + "red" : "0xEC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularStart.colorset/Contents.json b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularStart.colorset/Contents.json new file mode 100644 index 00000000..9de79167 --- /dev/null +++ b/Qapple/Qapple/Resource/Color/Colors.xcassets/GradientStroke/popularStart.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC2", + "green" : "0xB5", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index 7381a6ee..620de63f 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -82,7 +82,8 @@ extension AnswerRepository: DependencyKey { let response = try await RepositoryService.shared.request { server, accessToken in try await AnswerAPI.fetchListOfQuestion( questionId: Int(questionId), - threshold: threshold, + // TODO: 5/20 수정 필요 + threshold: "", pageSize: 30, server: server, accessToken: accessToken diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift new file mode 100644 index 00000000..be5ad01b --- /dev/null +++ b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift @@ -0,0 +1,82 @@ +// +// QPPopularAnswerCell.swift +// Qapple +// +// Created by 문인범 on 5/19/25. +// + +import SwiftUI + + +enum PopularAnswerCellStatus { + case todayQuestion + case popularAnswer +} + +struct QPPopularAnswerCell: View { + let status: PopularAnswerCellStatus + + var body: some View { + HStack(alignment: .top, spacing: 0) { + Image(status == .todayQuestion ? .questionReady : .questionComplete) + .resizable() + .frame(width: 22, height: 21) + .padding(.vertical, 13) + .padding(.leading, 18) + + switch status { + case .todayQuestion: + Text("오늘의 질문이 도착했어요!") + .font(.pretendard(.semiBold, size: 15)) + .foregroundStyle(.main) + .padding(.top, 15) + .padding(.leading, 6) + case .popularAnswer: + VStack(alignment: .leading, spacing: 5) { + Text("오늘의 인기 답변") + .font(.pretendard(.light, size: 12)) + .foregroundStyle(.main.opacity(0.5)) + + Text("프라이데이는 여자친구가 가지고 싶어요") + .font(.pretendard(.regular, size: 15)) + .foregroundStyle(.main) + .lineLimit(1) + } + .padding(.vertical, 13) + .padding(.leading, 7.5) + } + + Spacer() + } + .background { + ZStack { + RoundedRectangle(cornerRadius: 12) + .foregroundStyle(.questionNoti) + + + RoundedRectangle(cornerRadius: 12) + .stroke(lineWidth: 1) + .foregroundStyle( +// LinearGradient( +// colors: [.popularStart, .popularEnd], +// startPoint: .leading, +// endPoint: .trailing +// ) +// .opacity(0.17) + RadialGradient(colors: [.popularStart, .popularEnd], center: .center, startRadius: 0, endRadius: 100) + .opacity(0.17) + + ) + } + } + } +} + + +#Preview { + VStack { + QPPopularAnswerCell(status: .todayQuestion) + QPPopularAnswerCell(status: .popularAnswer) + } + .padding(.horizontal, 10) +} From b0294f39224506231dd9aaa7d8272a168d399560 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 10:40:49 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[#368]=20Answer=20DTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=EC=A2=8B=EC=95=84=EC=9A=94,=20=EB=8C=93=EA=B8=80=20c?= =?UTF-8?q?ount=20=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/AnswerRepository.swift | 28 +++++++++++++++---- Qapple/Qapple/SourceCode/Entity/Answer.swift | 14 +++++++++- .../2-2.TodayQuestion/TodayQuestionView.swift | 15 ++++++++-- .../AnswerCommentView.swift | 5 +++- .../SourceCode/UIComponent/QPAnswerCell.swift | 20 ++++++++++--- 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index 620de63f..f6344e4a 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -45,7 +45,11 @@ extension AnswerRepository: DependencyKey { publishedDate: $0.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: false, isMine: true, - isResignMember: false + isLiked: $0.isLiked, + isResignMember: false, + // TODO: 5/20 논의 필요 + commentCount: 0, + heartCount: $0.heartCount ) } let paginationInfo = QappleAPI.PaginationInfo( @@ -74,7 +78,10 @@ extension AnswerRepository: DependencyKey { publishedDate: $0.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: $0.isReported, isMine: $0.isMine, - isResignMember: $0.nickname == "알 수 없음" + isLiked: $0.isLiked ?? false, + isResignMember: $0.nickname == "알 수 없음", + commentCount: $0.commentCount, + heartCount: $0.heartCount ) } }, @@ -83,7 +90,7 @@ extension AnswerRepository: DependencyKey { try await AnswerAPI.fetchListOfQuestion( questionId: Int(questionId), // TODO: 5/20 수정 필요 - threshold: "", + threshold: threshold, pageSize: 30, server: server, accessToken: accessToken @@ -99,7 +106,10 @@ extension AnswerRepository: DependencyKey { publishedDate: $0.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: $0.isReported, isMine: $0.isMine, - isResignMember: $0.nickname == "알 수 없음" + isLiked: $0.isLiked ?? false, + isResignMember: $0.nickname == "알 수 없음", + commentCount: $0.commentCount, + heartCount: $0.heartCount ) } let paginationInfo = QappleAPI.PaginationInfo( @@ -141,7 +151,10 @@ extension AnswerRepository: DependencyKey { publishedDate: .init(timeIntervalSinceNow: TimeInterval(i * -5000)), isReported: false, isMine: true, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 0, + heartCount: 0 ) } return (stubProfiles, .init(threshold: "10", hasNext: false)) @@ -183,7 +196,10 @@ extension AnswerRepository { publishedDate: .init(timeIntervalSinceNow: TimeInterval(i*(-10000))), isReported: i == 2, isMine: i == 1, - isResignMember: i == 3 + isLiked: i == 4, + isResignMember: i == 3, + commentCount: i, + heartCount: i ) ) } diff --git a/Qapple/Qapple/SourceCode/Entity/Answer.swift b/Qapple/Qapple/SourceCode/Entity/Answer.swift index 01a8e72d..e83a422c 100644 --- a/Qapple/Qapple/SourceCode/Entity/Answer.swift +++ b/Qapple/Qapple/SourceCode/Entity/Answer.swift @@ -33,9 +33,18 @@ struct Answer: Identifiable, Equatable { /// 현재 사용자가 작성한 답변인지 여부 let isMine: Bool + /// 내가 좋아요를 눌렀는지 여부 + let isLiked: Bool + /// 탈퇴한 사용자의 답변인지 여부 let isResignMember: Bool + /// 답변에 대한 댓글 갯수 + let commentCount: Int + + /// 답변의 좋아요 갯수 + let heartCount: Int + /// 초기화용 답변 엔티티 static var initialState: Answer { Answer( @@ -47,7 +56,10 @@ struct Answer: Identifiable, Equatable { publishedDate: .now, isReported: false, isMine: true, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 0, + heartCount: 0 ) } } diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift index 411f7727..2bcaede3 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift @@ -365,7 +365,10 @@ private struct SeeAllButton: View { publishedDate: .now, isReported: false, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ), Answer( id: 1, @@ -376,7 +379,10 @@ private struct SeeAllButton: View { publishedDate: .now, isReported: false, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ), Answer( id: 2, @@ -387,7 +393,10 @@ private struct SeeAllButton: View { publishedDate: .now, isReported: false, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ) ] ) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift index 1b3385a5..2b3470eb 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift @@ -217,7 +217,10 @@ private struct AddCommentView: View { publishedDate: .init(), isReported: false, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ) ) ){ diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift index 53281cda..8cc513ce 100644 --- a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift +++ b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift @@ -315,7 +315,10 @@ private struct ReportedCell: View { publishedDate: .now, isReported: false, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ), Answer( id: 1, @@ -326,7 +329,10 @@ private struct ReportedCell: View { publishedDate: .now, isReported: false, isMine: true, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ), Answer( id: 2, @@ -337,7 +343,10 @@ private struct ReportedCell: View { publishedDate: .now, isReported: false, isMine: false, - isResignMember: true + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ), Answer( id: 3, @@ -348,7 +357,10 @@ private struct ReportedCell: View { publishedDate: .now, isReported: true, isMine: false, - isResignMember: false + isLiked: false, + isResignMember: false, + commentCount: 1, + heartCount: 1 ) ] ZStack { From e385aa0b8ab48bde35cf54be8471a4eec8f7a0b1 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 10:51:16 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[#368]=20AnswerCell=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceCode/UIComponent/QPAnswerCell.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift index 8cc513ce..849ec762 100644 --- a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift +++ b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift @@ -84,12 +84,11 @@ private struct NormalCell: View { Content() .padding(.top, 8) .padding(.horizontal, 16) - .padding(.bottom, 20) + .padding(.bottom, 5) -// Footer() -// .padding(.top, 6) -// .padding(.bottom, 20) -// .padding(.horizontal, 16) + Footer() + .padding(.bottom, 20) + .padding(.horizontal, 16) } .background(.first) } @@ -156,11 +155,11 @@ private struct NormalCell: View { likeAction() } label: { HStack(spacing: 4) { - Image(true ? .heartActive : .heart) + Image(answer.isLiked ? .heartActive : .heart) .resizable() .frame(width: 18, height: 18) - Text("32") + Text("\(answer.heartCount)") .pretendard(.regular, 13) .foregroundStyle(.sub3) } @@ -175,7 +174,7 @@ private struct NormalCell: View { .resizable() .frame(width: 15, height: 14) - Text("32") + Text("\(answer.commentCount)") .pretendard(.regular, 13) } .foregroundStyle(.sub3) @@ -343,7 +342,7 @@ private struct ReportedCell: View { publishedDate: .now, isReported: false, isMine: false, - isLiked: false, + isLiked: true, isResignMember: false, commentCount: 1, heartCount: 1 @@ -357,7 +356,7 @@ private struct ReportedCell: View { publishedDate: .now, isReported: true, isMine: false, - isLiked: false, + isLiked: true, isResignMember: false, commentCount: 1, heartCount: 1 From 3f653f96b68ad5424361b902c05e9fd034bd929a Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 11:49:09 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[#368]=20=EB=8B=B5=EB=B3=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/AnswerRepository.swift | 13 ++++++++- Qapple/Qapple/SourceCode/Entity/Answer.swift | 4 +-- .../TodayQuestionFeature.swift | 26 +++++++++++++++++ .../2-2.TodayQuestion/TodayQuestionView.swift | 2 +- .../2-6.AnswerList/AnswerListFeature.swift | 29 ++++++++++++++++--- .../2-6.AnswerList/AnswerListView.swift | 2 +- .../AnswerCommentFeature.swift | 21 ++++++++++---- .../5-3.MyAnswerList/MyAnswerListView.swift | 4 +-- 8 files changed, 84 insertions(+), 17 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index f6344e4a..b4639a73 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -19,6 +19,7 @@ struct AnswerRepository { ) var postAnswer: (_ questionId: Int, _ answer: String) async throws -> Void var deleteAnswer: (_ answerId: Int) async throws -> Void + var likeAnswer: (_ questionId: Int) async throws -> Void } // MARK: - DependencyKey @@ -136,6 +137,15 @@ extension AnswerRepository: DependencyKey { accessToken: accessToken ) } + }, + likeAnswer: { answerId in + let response = try await RepositoryService.shared.request { server, accessToken in + try await AnswerAPI.like( + answerId: answerId, + server: server, + accessToken: accessToken + ) + } } ) @@ -166,7 +176,8 @@ extension AnswerRepository: DependencyKey { (stubAnswerList, 25, .init(threshold: "", hasNext: false)) }, postAnswer: { _, _ in }, - deleteAnswer: { _ in } + deleteAnswer: { _ in }, + likeAnswer: { _ in } ) } diff --git a/Qapple/Qapple/SourceCode/Entity/Answer.swift b/Qapple/Qapple/SourceCode/Entity/Answer.swift index e83a422c..0cf6fc97 100644 --- a/Qapple/Qapple/SourceCode/Entity/Answer.swift +++ b/Qapple/Qapple/SourceCode/Entity/Answer.swift @@ -34,7 +34,7 @@ struct Answer: Identifiable, Equatable { let isMine: Bool /// 내가 좋아요를 눌렀는지 여부 - let isLiked: Bool + var isLiked: Bool /// 탈퇴한 사용자의 답변인지 여부 let isResignMember: Bool @@ -43,7 +43,7 @@ struct Answer: Identifiable, Equatable { let commentCount: Int /// 답변의 좋아요 갯수 - let heartCount: Int + var heartCount: Int /// 초기화용 답변 엔티티 static var initialState: Answer { diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionFeature.swift index 0480c398..f802c58a 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionFeature.swift @@ -38,6 +38,8 @@ struct TodayQuestionFeature { case seeAllAnswerButtonTapped(Question) case seeMoreAnswerButtonTapped(Answer) case answerCommentButtonTapped(Answer) + case likeAnswerButtonTapped(Answer) + case likeAnswer(Answer) case questionTimerTick case cancelQuestionTimer case toggleLoading(Bool) @@ -146,6 +148,30 @@ struct TodayQuestionFeature { case .answerCommentButtonTapped: return .none + case let .likeAnswerButtonTapped(answer): + return .run { send in + await send(.toggleLoading(true), animation: .bouncy) + do { + try await answerRepository.likeAnswer(answer.id) + await send(.likeAnswer(answer)) + } catch { + await send(.networkingFailed(error)) + } + await send(.toggleLoading(false), animation: .bouncy) + } + + case let .likeAnswer(answer): + guard let currentAnswerIdx = state.answerPreviewList.firstIndex(where: { $0.id == answer.id }) + else { return .none } + + if state.answerPreviewList[currentAnswerIdx].isLiked { + state.answerPreviewList[currentAnswerIdx].heartCount -= 1 + } else { + state.answerPreviewList[currentAnswerIdx].heartCount += 1 + } + state.answerPreviewList[currentAnswerIdx].isLiked.toggle() + return .none + case let .sheet(.presented(.seeMore(.alert(.presented(.confirmDeletion(sheetData)))))): guard case let .answer(answer) = sheetData else { return .none } return .run { send in diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift index 2bcaede3..e1e0662e 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-2.TodayQuestion/TodayQuestionView.swift @@ -308,7 +308,7 @@ private struct AnswerPreviewList: View { store.send(.seeMoreAnswerButtonTapped(answer)) }, likeAction: { - + store.send(.likeAnswerButtonTapped(answer)) }, commentAction: { store.send(.answerCommentButtonTapped(answer)) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift index 3cb009cf..3e01ea39 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift @@ -33,8 +33,9 @@ struct AnswerListFeature { case networkingFailed(Error) case seeMoreAction(Answer) case backButtonTapped - case likeAnswerButtonTapped + case likeAnswerButtonTapped(Answer) case answerCommentButtonTapped(Answer) + case likeAnswer(Answer) case toggleLoading(Bool) case sheet(PresentationAction) case alert(PresentationAction) @@ -115,13 +116,33 @@ struct AnswerListFeature { state.answerList = state.answerList.reversed().filter(UserDefaults.filterAnswerBlockedUser) return .none - case .likeAnswerButtonTapped: - // TODO: 좋아요 기능 구현 필요 - return .none + case let .likeAnswerButtonTapped(answer): + return .run { send in + await send(.toggleLoading(true), animation: .bouncy) + do { + try await answerRepository.likeAnswer(answer.id) + await send(.likeAnswer(answer)) + } catch { + await send(.networkingFailed(error)) + } + await send(.toggleLoading(false), animation: .bouncy) + } case .answerCommentButtonTapped: return .none + case let .likeAnswer(answer): + guard let currentAnswerIdx = state.answerList.firstIndex(where: { $0.id == answer.id }) + else { return .none } + + if state.answerList[currentAnswerIdx].isLiked { + state.answerList[currentAnswerIdx].heartCount -= 1 + } else { + state.answerList[currentAnswerIdx].heartCount += 1 + } + state.answerList[currentAnswerIdx].isLiked.toggle() + return .none + case let .networkingFailed(error): HapticService.notification(type: .error) state.alert = .failedNetworking(with: error) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift index 0373fd3a..0152330f 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift @@ -141,7 +141,7 @@ private struct AnswerList: View { store.send(.seeMoreAction(answer)) }, likeAction: { - store.send(.likeAnswerButtonTapped) + store.send(.likeAnswerButtonTapped(answer)) }, commentAction: { store.send(.answerCommentButtonTapped(answer)) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift index b4b5b4f4..cd192b0f 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift @@ -57,6 +57,7 @@ struct AnswerCommentFeature { } @Dependency(\.dismiss) var dismiss + @Dependency(\.answerRepository) var answerRepository var body: some ReducerOf { BindingReducer() @@ -174,6 +175,13 @@ struct AnswerCommentFeature { HapticService.impact(style: .light) return .run { [answer = state.answer] send in await send(.toggleLoading(true), animation: .bouncy) + do { + try await answerRepository.likeAnswer(answer.id) + await send(.likeAnswer) + // TODO: 5/20 GA 업데이트 + } catch { + await send(.networkingFailed(error)) + } // do { // try await bulletinBoardRepository.likeBoard(answer.id) // await send(.likeBoard) @@ -185,12 +193,13 @@ struct AnswerCommentFeature { } case .likeAnswer: -// if state.answer.isLiked { -// state.answer.heartCount -= 1 -// } else { -// state.answer.heartCount += 1 -// } -// state.answer.isLiked.toggle() + if state.answer.isLiked { + state.answer.heartCount -= 1 + } else { + state.answer.heartCount += 1 + } + + state.answer.isLiked.toggle() return .none case .seeMoreAction: diff --git a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift index 53a8bc86..482d5229 100644 --- a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift +++ b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift @@ -72,10 +72,10 @@ private struct MyAnswerList: View { store.send(.seeMoreAction(answer)) }, likeAction: { - + // TODO: 5/20 좋아요 기능 연결? }, commentAction: { - + // TODO: 5/20 댓글 기능 연결? } ) .configurePagination( From 594f418305e4f9cc6be36663fff8c5438f173134 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 19:38:07 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[#368]=20AnswerCommentRepository=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/AnswerCommentRepository.swift | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 Qapple/Qapple/SourceCode/Data/Repository/AnswerCommentRepository.swift diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerCommentRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerCommentRepository.swift new file mode 100644 index 00000000..32380987 --- /dev/null +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerCommentRepository.swift @@ -0,0 +1,141 @@ +// +// AnswerCommentRepository.swift +// Qapple +// +// Created by 문인범 on 5/20/25. +// + +import ComposableArchitecture +import QappleRepository +import Foundation + + +/** + AnswerComment API 의존성 + */ +struct AnswerCommentRepository { + var fetchAnswerComments: (_ answerId: Int) async throws -> [AnswerComment] + var createAnswerComment: (_ answerId: Int, _ content: String) async throws -> Void + var likeAnswerComment: (_ answerCommentId: Int) async throws -> Void + var deleteAnswerComment: (_ answerCommentId: Int) async throws -> Void +} + + +// MARK: - DependencyKey +extension AnswerCommentRepository: DependencyKey { + static let liveValue: AnswerCommentRepository = Self( + fetchAnswerComments: { answerId in + let response = try await RepositoryService.shared.request { server, accessToken in + try await AnswerCommentAPI.fetchAnswerComments( + answerId: answerId, + server: server, + accessToken: accessToken + ) + } + + let list = response.answerCommentInfos.map { + AnswerComment( + id: $0.answerCommentId, + writeId: $0.writerId, + // TODO: 5/20 문의 필요(writer generation, isLiked, isMine, isReport이 있는지 여부) + writerGeneration: "", + content: $0.content, + heartCount: $0.heartCount, + isLiked: false, + isMine: false, + isReport: false, + createdAt: $0.createdAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), + anonymityId: -2 + ) + } + + return list + }, + createAnswerComment: { answerId, content in + let _ = try await RepositoryService.shared.request { server, accessToken in + try await AnswerCommentAPI.createAnswerComment( + answerId: answerId, + content: content, + server: server, + accessToken: accessToken + ) + } + }, + likeAnswerComment: { answerCommentId in + let _ = try await RepositoryService.shared.request { server, accessToken in + try await AnswerCommentAPI.likeAnswerComment( + answerCommentId: answerCommentId, + server: server, + accessToken: accessToken + ) + } + }, + deleteAnswerComment: { answerCommentId in + let _ = try await RepositoryService.shared.request { server, accessToken in + try await AnswerCommentAPI.deleteAnswerComment( + answerCommentId: answerCommentId, + server: server, + accessToken: accessToken + ) + } + } + ) + + static let previewValue: AnswerCommentRepository = .init( + fetchAnswerComments: { _ in + sampleComments + }, + createAnswerComment: { answerId, content in + print("\(answerId) 게시글에 \"\(content)\" 댓글을 작성했습니다.") + }, + likeAnswerComment: { answerCommentId in + print("\(answerCommentId) 댓글에 좋아요를 눌렀습니다.") + }, + deleteAnswerComment: { answerCommentId in + print("\(answerCommentId) 댓글을 삭제했습니다.") + } + ) + + static let testValue: AnswerCommentRepository = .init( + fetchAnswerComments: { _ in + sampleComments + }, + createAnswerComment: { _, _ in }, + likeAnswerComment: { _ in }, + deleteAnswerComment: { _ in } + ) +} + + +// MARK: DependencyValues +extension DependencyValues { + var answerCommentRepository: AnswerCommentRepository { + get { self[AnswerCommentRepository.self] } + set { self[AnswerCommentRepository.self] = newValue } + } +} + + +// MARK: Test Values +extension AnswerCommentRepository { + private static let sampleComments: [AnswerComment] = { + var result = [AnswerComment]() + for i in 1...10 { + result.append( + AnswerComment( + id: i, + writeId: i, + writerGeneration: "4기", + content: "테스트 댓글\(i)", + heartCount: i, + isLiked: i == 1, + isMine: i == 2, + isReport: i == 3, + createdAt: .now, + anonymityId: -2 + ) + ) + } + return result + }() +} From f69e7f22392d75d738eb4aa4f59d2c082d6c7c84 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 21:56:26 +0900 Subject: [PATCH 06/13] [#368] CommentRepository -> BoardCommentRepository renaming --- ...ory.swift => BoardCommentRepository.swift} | 22 +++++++++---------- .../BoardCommentFeature.swift | 12 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) rename Qapple/Qapple/SourceCode/Data/Repository/{CommentRepository.swift => BoardCommentRepository.swift} (89%) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/CommentRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/BoardCommentRepository.swift similarity index 89% rename from Qapple/Qapple/SourceCode/Data/Repository/CommentRepository.swift rename to Qapple/Qapple/SourceCode/Data/Repository/BoardCommentRepository.swift index a080aa4a..6dc35b80 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/CommentRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/BoardCommentRepository.swift @@ -1,5 +1,5 @@ // -// CommentRepository.swift +// BoardCommentRepository.swift // Qapple // // Created by 문인범 on 1/23/25. @@ -13,7 +13,7 @@ import ComposableArchitecture /** Comment API 의존성 */ -struct CommentRepository { +struct BoardCommentRepository { var fetchBoardCommentList: (_ boardId: Int, _ threshold: Int?) async throws -> ([BoardComment], QappleAPI.PaginationInfo) var deleteBoardComment: (_ boardCommentId: Int) async throws -> Void var postBoardComment: (_ boardId: Int, _ content: String) async throws -> Void @@ -22,9 +22,9 @@ struct CommentRepository { // MARK: - DependencyKey -extension CommentRepository: DependencyKey { +extension BoardCommentRepository: DependencyKey { - static let liveValue: CommentRepository = Self( + static let liveValue: BoardCommentRepository = Self( fetchBoardCommentList: { boardId, threshold in let response = try await RepositoryService.shared.request { server, accessToken in try await BoardCommentAPI.fetchList( @@ -85,7 +85,7 @@ extension CommentRepository: DependencyKey { } ) - static let previewValue: CommentRepository = Self( + static let previewValue: BoardCommentRepository = Self( fetchBoardCommentList: { _, _ in (sampleCommentList, .init(threshold: "", hasNext: false)) }, @@ -100,9 +100,9 @@ extension CommentRepository: DependencyKey { } ) - static let testValue: CommentRepository = Self( + static let testValue: BoardCommentRepository = Self( fetchBoardCommentList: { _, _ in - (CommentRepository.sampleCommentList, CommentRepository.samplePaginationInfo) + (BoardCommentRepository.sampleCommentList, BoardCommentRepository.samplePaginationInfo) }, deleteBoardComment: { _ in }, postBoardComment: { _, _ in }, @@ -113,15 +113,15 @@ extension CommentRepository: DependencyKey { // MARK: - DependencyValues extension DependencyValues { - var commentRepository: CommentRepository { - get { self[CommentRepository.self] } - set { self[CommentRepository.self] = newValue } + var boardCommentRepository: BoardCommentRepository { + get { self[BoardCommentRepository.self] } + set { self[BoardCommentRepository.self] = newValue } } } // MARK: - TestValues -extension CommentRepository { +extension BoardCommentRepository { private static let sampleCommentList: [BoardComment] = [ .init( id: 1, diff --git a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift index b2c3dce1..9aa2c93a 100644 --- a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift @@ -55,7 +55,7 @@ struct BoardCommentFeature { } } - @Dependency(\.commentRepository) var commentRepository + @Dependency(\.boardCommentRepository) var boardCommentRepository @Dependency(\.bulletinBoardRepository) var bulletinBoardRepository @Dependency(\.dismiss) var dismiss @@ -67,7 +67,7 @@ struct BoardCommentFeature { return .run { [boardId = state.board.id] send in await send(.toggleLoading(true), animation: .bouncy) do { - let commentResponse = try await commentRepository.fetchBoardCommentList(boardId, nil) + let commentResponse = try await boardCommentRepository.fetchBoardCommentList(boardId, nil) let boardResponse = try await bulletinBoardRepository.fetchSingleBoard(boardId) await send(.commentListResponse(commentResponse.0, commentResponse.1)) await send(.boardResponse(boardResponse)) @@ -87,7 +87,7 @@ struct BoardCommentFeature { ] send in await send(.toggleLoading(true), animation: .bouncy) do { - let response = try await commentRepository.fetchBoardCommentList(boardId, threshold) + let response = try await boardCommentRepository.fetchBoardCommentList(boardId, threshold) await send(.paginationResponse(response.0, response.1)) } catch { await send(.networkingFailed(error)) @@ -119,7 +119,7 @@ struct BoardCommentFeature { return .run { [board = state.board] send in await send(.toggleLoading(true), animation: .bouncy) do { - try await commentRepository.likeBoardComment(boardComment.id) + try await boardCommentRepository.likeBoardComment(boardComment.id) await send(.likeComment(boardComment.id)) GAService.log(.likeBoardComment(board: board, boardComment: boardComment)) } catch { @@ -142,7 +142,7 @@ struct BoardCommentFeature { ] send in await send(.toggleLoading(true), animation: .bouncy) do { - try await commentRepository.postBoardComment(board.id, text) + try await boardCommentRepository.postBoardComment(board.id, text) HapticService.notification(type: .success) GAService.log(.postBoardComment(board: board, comment: text)) await send(.refresh) @@ -253,7 +253,7 @@ struct BoardCommentFeature { return .run { send in await send(.toggleLoading(true), animation: .bouncy) do { - try await commentRepository.deleteBoardComment(boardCommentId) + try await boardCommentRepository.deleteBoardComment(boardCommentId) await send(.refresh) await send(.successDeletion) } catch { From 3614b22f7ff135b76091bbf6d30f21f6b8692a78 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 22:49:49 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[#368]=20AnswerCommentFeature=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnswerCommentFeature.swift | 133 +++++++----------- .../BoardCommentFeature.swift | 6 +- 2 files changed, 50 insertions(+), 89 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift index cd192b0f..bc342ecc 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift @@ -8,7 +8,7 @@ import Foundation import ComposableArchitecture -// TODO: 4/14 - Reducer 업데이트 필요 + @Reducer struct AnswerCommentFeature { @ObservableState @@ -16,7 +16,6 @@ struct AnswerCommentFeature { var answer: Answer var commentText: String = "" var commentList: [AnswerComment] = [] - var paginationInfo = QappleAPI.PaginationInfo(threshold: "", hasNext: false) var isLoading: Bool = false @Presents var sheet: Sheet.State? @Presents var alert: AlertState? @@ -26,10 +25,7 @@ struct AnswerCommentFeature { case onAppear case onDisappear case refresh - case pagination - case commentListResponse([AnswerComment], QappleAPI.PaginationInfo) - case paginationResponse([AnswerComment], QappleAPI.PaginationInfo) - case answerResponse(Answer) + case commentListResponse([AnswerComment]) case backButtonTapped case likeCommentButtonTapped(AnswerComment) @@ -58,6 +54,7 @@ struct AnswerCommentFeature { @Dependency(\.dismiss) var dismiss @Dependency(\.answerRepository) var answerRepository + @Dependency(\.answerCommentRepository) var answerCommentRepository var body: some ReducerOf { BindingReducer() @@ -66,47 +63,21 @@ struct AnswerCommentFeature { case .onAppear, .refresh: return .run { [answerId = state.answer.id] send in await send(.toggleLoading(true), animation: .bouncy) -// do { -// let commentResponse = try await commentRepository.fetchBoardCommentList(boardId, nil) -// let boardResponse = try await bulletinBoardRepository.fetchSingleBoard(boardId) -// await send(.commentListResponse(commentResponse.0, commentResponse.1)) -// await send(.boardResponse(boardResponse)) -// } catch { -// await send(.networkingFailed(error)) -// } + do { + let commentResponse = try await answerCommentRepository.fetchAnswerComments(answerId) + // TODO: 5/20 단일 답변 패치 필요? + await send(.commentListResponse(commentResponse)) + } catch { + await send(.networkingFailed(error)) + } await send(.toggleLoading(false), animation: .bouncy) } case .onDisappear: return .none - case .pagination: - return .run { [ - answerId = state.answer.id, - threshold = Int(state.paginationInfo.threshold) - ] send in - await send(.toggleLoading(true), animation: .bouncy) -// do { -// let response = try await commentRepository.fetchBoardCommentList(boardId, threshold) -// await send(.paginationResponse(response.0, response.1)) -// } catch { -// await send(.networkingFailed(error)) -// } - await send(.toggleLoading(false), animation: .bouncy) - } - - case let .commentListResponse(commentList, paginationInfo): -// state.commentList = anonymizeCommentList(state.answer.writerId, commentList) -// state.paginationInfo = paginationInfo - return .none - - case let .paginationResponse(commentList, paginationInfo): -// state.commentList.append(contentsOf: anonymizeCommentList(state.answer.writerId, commentList)) -// state.paginationInfo = paginationInfo - return .none - - case let .answerResponse(answer): - state.answer = answer + case let .commentListResponse(commentList): + state.commentList = anonymizeCommentList(state.answer.writerId, commentList) return .none case .backButtonTapped: @@ -118,21 +89,21 @@ struct AnswerCommentFeature { HapticService.impact(style: .light) return .run { [answer = state.answer] send in await send(.toggleLoading(true), animation: .bouncy) -// do { -// try await commentRepository.likeBoardComment(boardComment.id) -// await send(.likeComment(boardComment.id)) -// GAService.log(.likeBoardComment(board: board, boardComment: boardComment)) -// } catch { -// await send(.networkingFailed(error)) -// } + do { + try await answerCommentRepository.likeAnswerComment(answerComment.id) + await send(.likeComment(answerComment.id)) + // TODO: 5/20 Google Analytics 추가 + } catch { + await send(.networkingFailed(error)) + } await send(.toggleLoading(false), animation: .bouncy) } case let .likeComment(answerCommentId): -// if let index = state.commentList.firstIndex(where: { $0.id == boardCommentId }) { -// state.commentList[index].isLiked.toggle() -// state.commentList[index].heartCount += state.commentList[index].isLiked ? 1 : -1 -// } + if let index = state.commentList.firstIndex(where: { $0.id == answerCommentId }) { + state.commentList[index].isLiked.toggle() + state.commentList[index].heartCount += state.commentList[index].isLiked ? 1 : -1 + } return .none case .uploadCommentButtonTapped: @@ -141,15 +112,15 @@ struct AnswerCommentFeature { answer = state.answer ] send in await send(.toggleLoading(true), animation: .bouncy) -// do { -// try await commentRepository.postBoardComment(board.id, text) -// HapticService.notification(type: .success) -// GAService.log(.postBoardComment(board: board, comment: text)) -// await send(.refresh) -// await send(.commentTextReset) -// } catch { -// await send(.networkingFailed(error)) -// } + do { + try await answerCommentRepository.createAnswerComment(answer.id, text) + HapticService.notification(type: .success) + // TODO: 5/20 Google Analytics 추가 + await send(.refresh) + await send(.commentTextReset) + } catch { + await send(.networkingFailed(error)) + } await send(.toggleLoading(false), animation: .bouncy) } @@ -161,9 +132,9 @@ struct AnswerCommentFeature { NotificationCenter.default.post(name: .updateCommentCellToggle, object: nil) return .none - case let .deleteCommentButtonTapped(boardComment): + case let .deleteCommentButtonTapped(answerComment): HapticService.notification(type: .error) - state.alert = .confirmDeletion(boardComment.id) + state.alert = .confirmDeletion(answerComment.id) return .none case .successDeletion: @@ -182,13 +153,6 @@ struct AnswerCommentFeature { } catch { await send(.networkingFailed(error)) } -// do { -// try await bulletinBoardRepository.likeBoard(answer.id) -// await send(.likeBoard) -// if !answer.isLiked { GAService.log(.likeBoardFromDetail(board: answer)) } -// } catch { -// await send(.networkingFailed(error)) -// } await send(.toggleLoading(false), animation: .bouncy) } @@ -198,7 +162,6 @@ struct AnswerCommentFeature { } else { state.answer.heartCount += 1 } - state.answer.isLiked.toggle() return .none @@ -227,12 +190,12 @@ struct AnswerCommentFeature { guard case let .answer(answer) = sheetData else { return .none } return .run { send in await send(.toggleLoading(true), animation: .bouncy) -// do { -// try await bulletinBoardRepository.deleteBoard(board.id) -// await send(.sheet(.presented(.seeMore(.completionDeletion)))) -// } catch { -// await send(.networkingFailed(error)) -// } + do { + try await answerRepository.deleteAnswer(answer.id) + await send(.sheet(.presented(.seeMore(.completionDeletion)))) + } catch { + await send(.networkingFailed(error)) + } await send(.toggleLoading(false), animation: .bouncy) } @@ -257,16 +220,14 @@ struct AnswerCommentFeature { state.sheet = nil return .send(.onDisappear) - case let .alert(.presented(.confirmDeletion(answerId))): + case let .alert(.presented(.confirmDeletion(answerCommentId))): return .run { send in await send(.toggleLoading(true), animation: .bouncy) -// do { -// try await commentRepository.deleteBoardComment(boardCommentId) + do { + try await answerCommentRepository.deleteAnswerComment(answerCommentId) await send(.refresh) await send(.successDeletion) -// } catch { -// await send(.networkingFailed(error)) -// } + } await send(.toggleLoading(false), animation: .bouncy) } @@ -315,7 +276,7 @@ extension AlertState where Action == AnswerCommentFeature.Action.Alert { extension AnswerCommentFeature { // 이름을 익명화 해주는 method - private func anonymizeCommentList(_ BoardWriterId: Int, _ commentList: [AnswerComment]) -> [AnswerComment] { + private func anonymizeCommentList(_ answerWriterId: Int, _ commentList: [AnswerComment]) -> [AnswerComment] { var anonymousArray: [Int: Int] = [:] var anonymousIndex: Int = 0 @@ -325,7 +286,7 @@ extension AnswerCommentFeature { if !isContainName { anonymousIndex += 1 - let anonymityId = (comment.writeId == BoardWriterId) ? -1 : anonymousIndex + let anonymityId = (comment.writeId == answerWriterId) ? -1 : anonymousIndex anonymousArray.updateValue(comment.writeId, forKey: anonymityId) @@ -339,7 +300,7 @@ extension AnswerCommentFeature { isMine: comment.isMine, isReport: comment.isReport, createdAt: comment.createdAt, - anonymityId: (comment.writeId == BoardWriterId) ? -1 : anonymousIndex + anonymityId: (comment.writeId == answerWriterId) ? -1 : anonymousIndex ) } else { let currentIndex = anonymousArray.first(where: { $0.value == comment.writeId })?.key ?? 0 diff --git a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift index 9aa2c93a..a238993e 100644 --- a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-4.BoardComment/BoardCommentFeature.swift @@ -307,7 +307,7 @@ extension AlertState where Action == BoardCommentFeature.Action.Alert { extension BoardCommentFeature { // 이름을 익명화 해주는 method - private func anonymizeCommentList(_ BoardWriterId: Int, _ commentList: [BoardComment]) -> [BoardComment] { + private func anonymizeCommentList(_ boardWriterId: Int, _ commentList: [BoardComment]) -> [BoardComment] { var anonymousArray: [Int: Int] = [:] var anonymousIndex: Int = 0 @@ -317,7 +317,7 @@ extension BoardCommentFeature { if !isContainName { anonymousIndex += 1 - let anonymityId = (comment.writeId == BoardWriterId) ? -1 : anonymousIndex + let anonymityId = (comment.writeId == boardWriterId) ? -1 : anonymousIndex anonymousArray.updateValue(comment.writeId, forKey: anonymityId) @@ -331,7 +331,7 @@ extension BoardCommentFeature { isMine: comment.isMine, isReport: comment.isReport, createdAt: comment.createdAt, - anonymityId: (comment.writeId == BoardWriterId) ? -1 : anonymousIndex + anonymityId: (comment.writeId == boardWriterId) ? -1 : anonymousIndex ) } else { let currentIndex = anonymousArray.first(where: { $0.value == comment.writeId })?.key ?? 0 From 4354e2b42d69bae797f31a1c5b934f4bb568a74a Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Tue, 20 May 2025 23:08:00 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[#368]=20GAService=20=EC=B6=94=EA=B0=80(?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EC=A2=8B=EC=95=84=EC=9A=94,=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EA=B4=80=EB=A0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceCode/Data/Service/GAService.swift | 47 +++++++++++++++++++ .../2-6.AnswerList/AnswerListFeature.swift | 1 + .../AnswerCommentFeature.swift | 6 +-- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Service/GAService.swift b/Qapple/Qapple/SourceCode/Data/Service/GAService.swift index 80d1de27..cda8038a 100644 --- a/Qapple/Qapple/SourceCode/Data/Service/GAService.swift +++ b/Qapple/Qapple/SourceCode/Data/Service/GAService.swift @@ -26,6 +26,12 @@ enum GAService { /// 게시글 상세 페이지에서 좋아요 case likeBoardFromDetail(board: BulletinBoard) + /// 답변 리스트에서 좋아요 + case likeAnswerFromList(answer: Answer) + + /// 답변 상세 페이지에서 좋아요 + case likeAnswerFromDetail(answer: Answer) + /// 아카데미 일정 확인 case checkAcademySchedule(event: AcademyEventFor4th) @@ -35,6 +41,13 @@ enum GAService { /// 게시글 댓글 좋아요 case likeBoardComment(board: BulletinBoard, boardComment: BoardComment) + /// 답변 댓글 작성 + case postAnswerComment(answer: Answer, comment: String) + + /// 답변 댓글 좋아요 + case likeAnswerComment(answer: Answer, answerComment: AnswerComment) + + /// Push 알림을 눌러 질문 탭으로 이동 case navigateToQuestionTabFromPush(title: String, body: String, questionId: Int) @@ -47,9 +60,13 @@ enum GAService { case .postBoard: "post_board" case .likeBoardFromList: "like_board_from_list" case .likeBoardFromDetail: "like_board_from_detail" + case .likeAnswerFromList: "like_answer_from_list" + case .likeAnswerFromDetail: "like_answer_from_detail" case .checkAcademySchedule: "check_academy_schedule" case .postBoardComment: "post_board_comment" case .likeBoardComment: "like_board_comment" + case .postAnswerComment: "post_answer_comment" + case .likeAnswerComment: "like_answer_comment" case .navigateToQuestionTabFromPush: "navigate_to_question_tab_from_push_notification" case .navigateToBoardCommentFromPush: "navigate_to_board_comment_from_push_notification" } @@ -90,6 +107,20 @@ enum GAService { "heart_count": board.heartCount + 1 ] + case let .likeAnswerFromList(answer): + parameters = [ + "answer_id": answer.id, + "content": answer.content, + "heart_count": answer.heartCount + 1 + ] + + case let.likeAnswerFromDetail(answer): + parameters = [ + "answer_id": answer.id, + "content": answer.content, + "heart_count": answer.heartCount + 1 + ] + case let .checkAcademySchedule(event): parameters = [ "event_title": event.title @@ -111,6 +142,22 @@ enum GAService { "comment_heart_count": comment.heartCount + 1 ] + case let .postAnswerComment(answer, comment): + parameters = [ + "answer_id": answer.id, + "answer_content": answer.content, + "comment_content": comment + ] + + case let .likeAnswerComment(answer, comment): + parameters = [ + "answer_id": answer.id, + "answer_content": answer.content, + "answer_heart_count": answer.heartCount, + "comment_content": comment.content, + "comment_heart_count": comment.heartCount + 1 + ] + case let .navigateToQuestionTabFromPush(title, body, questionId): parameters = [ "question_id": questionId, diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift index 3e01ea39..ad91fd50 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift @@ -121,6 +121,7 @@ struct AnswerListFeature { await send(.toggleLoading(true), animation: .bouncy) do { try await answerRepository.likeAnswer(answer.id) + GAService.log(.likeAnswerFromList(answer: answer)) await send(.likeAnswer(answer)) } catch { await send(.networkingFailed(error)) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift index bc342ecc..6ea3777a 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentFeature.swift @@ -92,7 +92,7 @@ struct AnswerCommentFeature { do { try await answerCommentRepository.likeAnswerComment(answerComment.id) await send(.likeComment(answerComment.id)) - // TODO: 5/20 Google Analytics 추가 + GAService.log(.likeAnswerComment(answer: answer, answerComment: answerComment)) } catch { await send(.networkingFailed(error)) } @@ -115,7 +115,7 @@ struct AnswerCommentFeature { do { try await answerCommentRepository.createAnswerComment(answer.id, text) HapticService.notification(type: .success) - // TODO: 5/20 Google Analytics 추가 + GAService.log(.postAnswerComment(answer: answer, comment: text)) await send(.refresh) await send(.commentTextReset) } catch { @@ -149,7 +149,7 @@ struct AnswerCommentFeature { do { try await answerRepository.likeAnswer(answer.id) await send(.likeAnswer) - // TODO: 5/20 GA 업데이트 + if !answer.isLiked { GAService.log(.likeAnswerFromDetail(answer: answer)) } } catch { await send(.networkingFailed(error)) } From 7b85cc7f50365bac2a1c489417f86c5f6ac5f92d Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Wed, 21 May 2025 14:07:13 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[#368]=20MyPage=EC=9D=98=20=EB=82=B4=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94,=20comment=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SourceCode/Entity/AnswerComment.swift | 2 +- .../Feature/1.MainFlow/MainFlowFeature.swift | 4 +++ .../MyAnswerListFeature.swift | 30 +++++++++++++++++++ .../5-3.MyAnswerList/MyAnswerListView.swift | 4 +-- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Entity/AnswerComment.swift b/Qapple/Qapple/SourceCode/Entity/AnswerComment.swift index 4a6ad8c1..42aa9877 100644 --- a/Qapple/Qapple/SourceCode/Entity/AnswerComment.swift +++ b/Qapple/Qapple/SourceCode/Entity/AnswerComment.swift @@ -7,7 +7,7 @@ import Foundation -// MARK: 임시 Entity + struct AnswerComment: Identifiable, Equatable { let id: Int let writeId: Int diff --git a/Qapple/Qapple/SourceCode/Feature/1.MainFlow/MainFlowFeature.swift b/Qapple/Qapple/SourceCode/Feature/1.MainFlow/MainFlowFeature.swift index 22314066..614f217b 100644 --- a/Qapple/Qapple/SourceCode/Feature/1.MainFlow/MainFlowFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/1.MainFlow/MainFlowFeature.swift @@ -202,6 +202,10 @@ struct MainFlowFeature { state.path.append(.comment(.init(board: board))) return .none + case let .element(id: _, action: .myAnswerList(.commentButtonTapped(answer))): + state.path.append(.answerCommentList(.init(answer: answer))) + return .none + case .element(id: _, action: .answerList(.backButtonTapped)): state.path.removeAll() return .none diff --git a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListFeature.swift b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListFeature.swift index 092a8e72..220fbc05 100644 --- a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListFeature.swift @@ -25,6 +25,9 @@ struct MyAnswerListFeature { case onAppear case refresh case pagination + case likeAnswerButtonTapped(Answer) + case commentButtonTapped(Answer) + case likeAnswer(Answer) case answerListResponse( [Answer], QappleAPI.PaginationInfo @@ -71,6 +74,33 @@ struct MyAnswerListFeature { await send(.toggleLoading(false), animation: .bouncy) } + case let .likeAnswerButtonTapped(answer): + return .run { send in + await send(.toggleLoading(true), animation: .bouncy) + do { + try await answerRepository.likeAnswer(answer.id) + await send(.likeAnswer(answer)) + } catch { + await send(.networkingFailed(error)) + } + await send(.toggleLoading(false), animation: .bouncy) + } + + case let .likeAnswer(answer): + guard let currentAnswerIdx = state.myAnswerList.firstIndex(where: { $0.id == answer.id }) + else { return .none } + + if state.myAnswerList[currentAnswerIdx].isLiked { + state.myAnswerList[currentAnswerIdx].heartCount -= 1 + } else { + state.myAnswerList[currentAnswerIdx].heartCount += 1 + } + state.myAnswerList[currentAnswerIdx].isLiked.toggle() + return .none + + case .commentButtonTapped: + return .none + case let .answerListResponse(answerList, paginationInfo): state.myAnswerList = answerList state.paginationInfo = paginationInfo diff --git a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift index 482d5229..ee498522 100644 --- a/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift +++ b/Qapple/Qapple/SourceCode/Feature/5.Profile/5-3.MyAnswerList/MyAnswerListView.swift @@ -72,10 +72,10 @@ private struct MyAnswerList: View { store.send(.seeMoreAction(answer)) }, likeAction: { - // TODO: 5/20 좋아요 기능 연결? + store.send(.likeAnswerButtonTapped(answer)) }, commentAction: { - // TODO: 5/20 댓글 기능 연결? + store.send(.commentButtonTapped(answer)) } ) .configurePagination( From b8580b6aaed40e3736cbf6603d23fd027deac1ec Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Wed, 21 May 2025 17:40:29 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[#368]=20=EC=9D=B8=EA=B8=B0=20=EB=8B=B5?= =?UTF-8?q?=EB=B3=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/AnswerRepository.swift | 119 +++++++++++++++++- .../BulletinBoardFeature.swift | 15 +++ .../3-1.BulletinBoard/BulletinBoardView.swift | 95 +++----------- .../UIComponent/QPPopularAnswerCell.swift | 13 +- 4 files changed, 159 insertions(+), 83 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index b4639a73..e4ab2ebd 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -17,6 +17,7 @@ struct AnswerRepository { QappleAPI.TotalCount, QappleAPI.PaginationInfo ) + var fetchPopularAnswer: () async throws -> (Answer?, Question?, Bool) var postAnswer: (_ questionId: Int, _ answer: String) async throws -> Void var deleteAnswer: (_ answerId: Int) async throws -> Void var likeAnswer: (_ questionId: Int) async throws -> Void @@ -90,7 +91,6 @@ extension AnswerRepository: DependencyKey { let response = try await RepositoryService.shared.request { server, accessToken in try await AnswerAPI.fetchListOfQuestion( questionId: Int(questionId), - // TODO: 5/20 수정 필요 threshold: threshold, pageSize: 30, server: server, @@ -119,6 +119,114 @@ extension AnswerRepository: DependencyKey { ) return (answerList, response.total, paginationInfo) }, + fetchPopularAnswer: { + let currentHour = Calendar.current.component(.hour, from: .now) + if currentHour > 12 && currentHour < 19 { + return (nil, nil, false) + } + + let response = try await RepositoryService.shared.request { server, accessToken in + try await QuestionAPI.fetchQuestionList( + threshold: nil, + pageSize: 1, + server: server, + accessToken: accessToken + ) + } + + guard let question = response.content.first else { return (nil, nil, true) } + + let currentQuestion = Question( + id: question.questionId, + content: question.content, + publishedDate: question.livedAt?.ISO8601ToDate(.yearMonthDateTime) ?? .now, + isAnswered: question.isAnswered, + isLived: question.questionStatus == ("LIVE") + ) + + var popularAnswer: Answer = .init( + id: -1, + writerId: -1, + content: "", + authorNickname: "", + authorGeneration: "", + publishedDate: .init(timeIntervalSince1970: 0), + isReported: false, + isMine: false, + isLiked: false, + isResignMember: false, + commentCount: 0, + heartCount: 0 + ) + + var hasNext = true + var threshold: Int? + + while hasNext { + let answersOfQuestion = try await RepositoryService.shared.request { server, accessToken in + try await AnswerAPI.fetchListOfQuestion( + questionId: Int(question.questionId), + threshold: threshold, + pageSize: 30, + server: server, + accessToken: accessToken + ) + } + + if answersOfQuestion.content.isEmpty { + return (nil, nil, true) + } + + for answer in answersOfQuestion.content { + if answer.isReported { continue } + + let sum = answer.commentCount + answer.heartCount + let popularSum = popularAnswer.heartCount + popularAnswer.commentCount + + if sum > popularSum { + popularAnswer = .init( + id: answer.answerId, + writerId: answer.writerId, + content: answer.content, + authorNickname: answer.nickname, + authorGeneration: answer.writerGeneration, + publishedDate: answer.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), + isReported: false, + isMine: answer.isMine, + isLiked: answer.isLiked ?? false, + isResignMember: answer.nickname == "알 수 없음", + commentCount: answer.commentCount, + heartCount: answer.heartCount + ) + } else if sum == popularSum { + let popularAnswerDate = popularAnswer.publishedDate + let answerDate = answer.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds) + + if answerDate > popularAnswerDate { + popularAnswer = .init( + id: answer.answerId, + writerId: answer.writerId, + content: answer.content, + authorNickname: answer.nickname, + authorGeneration: answer.writerGeneration, + publishedDate: answer.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), + isReported: false, + isMine: answer.isMine, + isLiked: answer.isLiked ?? false, + isResignMember: answer.nickname == "알 수 없음", + commentCount: answer.commentCount, + heartCount: answer.heartCount + ) + } + } + } + + hasNext = answersOfQuestion.hasNext + threshold = Int(answersOfQuestion.threshold) + } + + return (popularAnswer, currentQuestion, false) + }, postAnswer: { questionId, answer in let response = try await RepositoryService.shared.request { server, accessToken in try await AnswerAPI.create( @@ -174,6 +282,15 @@ extension AnswerRepository: DependencyKey { }, fetchAnswerListOfQuestion: { _, _ in (stubAnswerList, 25, .init(threshold: "", hasNext: false)) + }, fetchPopularAnswer: { + let currentHour = Calendar.current.component(.hour, from: .now) + if currentHour > 12 && currentHour < 19 { + return (nil, nil, false) + } + let question = Question(id: 0, content: "", publishedDate: .now, isAnswered: false, isLived: true) + + + return (AnswerRepository.stubAnswerList.first!, question, false) }, postAnswer: { _, _ in }, deleteAnswer: { _ in }, diff --git a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift index 333b9916..7c10a194 100644 --- a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift @@ -17,6 +17,7 @@ struct BulletinBoardFeature { var bulletinBoardList: [BulletinBoard] = [] var todayQuestion: Question = .initialState var event: AcademyEventFor4th = .fourthStart + var popularAnswerStatus: PopularAnswerCellStatus = .none var paginationInfo = QappleAPI.PaginationInfo(threshold: "", hasNext: false) var isLoading: Bool = false var isFirstLaunch = true @@ -43,8 +44,10 @@ struct BulletinBoardFeature { case networkingFailed(Error) case toggleLoading(Bool) case filterBlockedUser + case questionNotiTapped(Question) case popularAnswerTapped(Question) + case fetchPopularAnswer((Answer?, Question?, Bool)) case sheet(PresentationAction) case alert(PresentationAction) @@ -61,6 +64,7 @@ struct BulletinBoardFeature { @Dependency(\.questionRepository.fetchMainQuestion) var fetchMainQuestion @Dependency(\.bulletinBoardRepository) var bulletinBoardRepository + @Dependency(\.answerRepository.fetchPopularAnswer) var fetchPopularAnswer var body: some ReducerOf { Reduce { state, action in @@ -74,6 +78,8 @@ struct BulletinBoardFeature { do { let mainQuestion = try await fetchMainQuestion() let response = try await bulletinBoardRepository.fetchBulletinBoardList(nil) + let popularAnswer = try await fetchPopularAnswer() + await send(.fetchPopularAnswer(popularAnswer)) await send(.bulletinBoardListResponse(mainQuestion, response.0, response.1)) } catch { await send(.networkingFailed(error)) @@ -146,6 +152,15 @@ struct BulletinBoardFeature { } return .none + case let .fetchPopularAnswer(result): + if let answer = result.0, let question = result.1 { + state.popularAnswerStatus = .popularAnswer(answer, question) + } else { + state.popularAnswerStatus = result.2 ? .none : .todayQuestion + } + + return .none + case .searchButtonTapped: return .none diff --git a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardView.swift b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardView.swift index 48939ec2..7b7f100f 100644 --- a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardView.swift +++ b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardView.swift @@ -88,79 +88,6 @@ private struct BulletinBoardContentView: View { } } -// MARK: - QuestionNotificationView - -private struct QuestionNotificationView: View { - - let store: StoreOf - - var body: some View { - if !store.todayQuestion.isAnswered { - Button { - store.send(.questionNotiTapped(store.todayQuestion)) - } label: { - HStack(spacing: 0) { - Image("questionReady") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing, 6) - .padding(.leading, 18) - - Text("오늘의 질문이 도착했어요!") - .font(.pretendard(.semiBold, size: 15)) - .foregroundStyle(.white) - - Spacer() - } - .frame(width: 361, height: 47) - .background(RoundedRectangle(cornerRadius: 12) - .fill(.questionNoti) - .stroke(.button.opacity(0.17), lineWidth: 0.6)) // TODO: 그라데이션 - } - } else { - Button { - // store.send(.popularAnswerTapped(store.todayQuestion)) - } label: { - HStack(spacing: 0) { - VStack(spacing: 0) { - Image("questionComplete") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing, 8) - .padding(.leading, 18) - - Spacer() - } - - - VStack(alignment: .leading, spacing: 0) { -// Text("오늘의 인기 답변") -// .font(.pretendard(.regular, size: 12)) -// .foregroundStyle(TextLabel.sub4) -// -// Spacer() - - Text("이전에 개발자의 실수로 이상한(?) 정보가 표시됐었답니다.") // TODO: 인기 답변으로 - .lineLimit(2) - .multilineTextAlignment(.leading) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(.white) - } - - Spacer() - } - .padding(.vertical, 12) - .frame(width: 361, height: 67) - .background(RoundedRectangle(cornerRadius: 12) - .fill(.questionNoti) - .stroke(.button.opacity(0.17), lineWidth: 0.6)) // TODO: 그라데이션 - } - } - } -} - // MARK: - BulletionBoardListView private struct BulletionBoardListView: View { @@ -169,10 +96,24 @@ private struct BulletionBoardListView: View { var body: some View { ScrollView { - - QuestionNotificationView(store: store) - .padding(.horizontal) - .padding(.top, 2) + Group { + switch store.state.popularAnswerStatus { + case .todayQuestion: + QPPopularAnswerCell(status: .todayQuestion) + .onTapGesture { + store.send(.questionNotiTapped(store.todayQuestion)) + } + case let .popularAnswer(answer, question): + QPPopularAnswerCell(status: .popularAnswer(answer, question)) + .onTapGesture { + store.send(.popularAnswerTapped(question)) + } + case .none: + EmptyView() + } + } + .padding(.horizontal) + .padding(.top, 2) LazyVStack(spacing: 0) { ForEach(enumerated(store.bulletinBoardList), id: \.offset) { index, board in diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift index be5ad01b..b157f27d 100644 --- a/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift +++ b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift @@ -8,9 +8,10 @@ import SwiftUI -enum PopularAnswerCellStatus { +enum PopularAnswerCellStatus: Equatable { + case none case todayQuestion - case popularAnswer + case popularAnswer(Answer, Question) } struct QPPopularAnswerCell: View { @@ -31,19 +32,22 @@ struct QPPopularAnswerCell: View { .foregroundStyle(.main) .padding(.top, 15) .padding(.leading, 6) - case .popularAnswer: + case let .popularAnswer(answer, _): VStack(alignment: .leading, spacing: 5) { Text("오늘의 인기 답변") .font(.pretendard(.light, size: 12)) .foregroundStyle(.main.opacity(0.5)) - Text("프라이데이는 여자친구가 가지고 싶어요") + Text(answer.content) .font(.pretendard(.regular, size: 15)) .foregroundStyle(.main) .lineLimit(1) } .padding(.vertical, 13) .padding(.leading, 7.5) + + case .none: + EmptyView() } Spacer() @@ -76,7 +80,6 @@ struct QPPopularAnswerCell: View { #Preview { VStack { QPPopularAnswerCell(status: .todayQuestion) - QPPopularAnswerCell(status: .popularAnswer) } .padding(.horizontal, 10) } From f84d98faca5da95d3253d6e75da419d340c226aa Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Wed, 21 May 2025 17:48:47 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[#368]=20AnswerCommentView=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnswerCommentView.swift | 58 +++++-------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift index 2b3470eb..8e22a174 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-7.AnswerCommentList/AnswerCommentView.swift @@ -85,7 +85,7 @@ private struct CommentListView: View { var body: some View { ZStack { - VStack { + VStack(spacing: 0) { seperator HStack { @@ -94,39 +94,12 @@ private struct CommentListView: View { .foregroundStyle(.sub3) Spacer() } - .padding(.top, 12) + .padding(.vertical, 12) .padding(.horizontal, 20) ScrollView { LazyVStack(spacing: 0) { - // TODO: 4/14 데이터 연결 필요 - // ForEach(Array(self.store.commentList.enumerated()), id: \.offset) { index, comment in - // AnswerCommentCell( - // comment: comment, - // like: { - // store.send(.likeCommentButtonTapped(comment)) - // }, - // delete: { - // store.send(.deleteCommentButtonTapped(comment)) - // }, - // report: { - // store.send(.reportButtonTapped(comment)) - // } - // ) - // .configurePagination( - // store.commentList, - // currentIndex: index, - // hasNext: store.paginationInfo.hasNext, - // pagination: { - // store.send(.pagination) - // } - // ) - // .disabled(store.isLoading) - // - // seperator - // } - - ForEach(AnswerCommentFeature.sampleComment) { comment in + ForEach(Array(self.store.commentList.enumerated()), id: \.offset) { index, comment in AnswerCommentCell( comment: comment, like: { @@ -139,7 +112,8 @@ private struct CommentListView: View { store.send(.reportButtonTapped(comment)) } ) - + .disabled(store.isLoading) + seperator } } @@ -148,17 +122,17 @@ private struct CommentListView: View { .scrollDismissesKeyboard(.immediately) .background(Color.bk) -// if store.commentList.isEmpty && !store.isLoading { -// VStack { -// Text("아직 작성된 댓글이 없습니다") -// .font(.pretendard(.medium, size: 14)) -// .foregroundStyle(.sub5) -// .multilineTextAlignment(.center) -// .padding(.top, 24) -// -// Spacer() -// } -// } + if store.commentList.isEmpty && !store.isLoading { + VStack { + Text("아직 작성된 댓글이 없습니다") + .font(.pretendard(.medium, size: 14)) + .foregroundStyle(.sub5) + .multilineTextAlignment(.center) + .padding(.top, 24) + + Spacer() + } + } } } From cf02f34a1384d8a9512140e5e09f249f44d92e56 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Thu, 22 May 2025 12:35:39 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[#368]=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/AnswerRepository.swift | 14 +++++++------- .../Feature/8.Report/ReportFeature.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index e4ab2ebd..150e97a9 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -49,8 +49,7 @@ extension AnswerRepository: DependencyKey { isMine: true, isLiked: $0.isLiked, isResignMember: false, - // TODO: 5/20 논의 필요 - commentCount: 0, + commentCount: $0.commentCount, heartCount: $0.heartCount ) } @@ -80,7 +79,7 @@ extension AnswerRepository: DependencyKey { publishedDate: $0.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: $0.isReported, isMine: $0.isMine, - isLiked: $0.isLiked ?? false, + isLiked: $0.isLiked, isResignMember: $0.nickname == "알 수 없음", commentCount: $0.commentCount, heartCount: $0.heartCount @@ -107,7 +106,7 @@ extension AnswerRepository: DependencyKey { publishedDate: $0.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: $0.isReported, isMine: $0.isMine, - isLiked: $0.isLiked ?? false, + isLiked: $0.isLiked, isResignMember: $0.nickname == "알 수 없음", commentCount: $0.commentCount, heartCount: $0.heartCount @@ -193,7 +192,7 @@ extension AnswerRepository: DependencyKey { publishedDate: answer.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: false, isMine: answer.isMine, - isLiked: answer.isLiked ?? false, + isLiked: answer.isLiked, isResignMember: answer.nickname == "알 수 없음", commentCount: answer.commentCount, heartCount: answer.heartCount @@ -212,7 +211,7 @@ extension AnswerRepository: DependencyKey { publishedDate: answer.writeAt.ISO8601ToDate(.yearMonthDateTimeMilliseconds), isReported: false, isMine: answer.isMine, - isLiked: answer.isLiked ?? false, + isLiked: answer.isLiked, isResignMember: answer.nickname == "알 수 없음", commentCount: answer.commentCount, heartCount: answer.heartCount @@ -282,7 +281,8 @@ extension AnswerRepository: DependencyKey { }, fetchAnswerListOfQuestion: { _, _ in (stubAnswerList, 25, .init(threshold: "", hasNext: false)) - }, fetchPopularAnswer: { + }, + fetchPopularAnswer: { let currentHour = Calendar.current.component(.hour, from: .now) if currentHour > 12 && currentHour < 19 { return (nil, nil, false) diff --git a/Qapple/Qapple/SourceCode/Feature/8.Report/ReportFeature.swift b/Qapple/Qapple/SourceCode/Feature/8.Report/ReportFeature.swift index b8e786e9..a8496161 100644 --- a/Qapple/Qapple/SourceCode/Feature/8.Report/ReportFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/8.Report/ReportFeature.swift @@ -60,7 +60,7 @@ struct ReportFeature { case let .comment(comment): try await reportRepository.reportComment(comment.id, reportType) case let .answerComment(comment): - // TODO: 답면 댓글 신고 구현 + // TODO: 답변 댓글 신고 구현 break } From 2c3725819f5e100587c48aacad389e89377b58f4 Mon Sep 17 00:00:00 2001 From: mooninbeom Date: Thu, 22 May 2025 13:48:48 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[#368]=20=EC=9D=B8=EA=B8=B0=EB=8B=B5?= =?UTF-8?q?=EB=B3=80=20=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=EB=AA=A8=EB=93=A0=20=EC=A7=88=EB=AC=B8=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Repository/AnswerRepository.swift | 27 ++++++++----------- .../2-6.AnswerList/AnswerListFeature.swift | 22 +++++++++++++++ .../2-6.AnswerList/AnswerListView.swift | 5 ++++ .../BulletinBoardFeature.swift | 2 +- .../SourceCode/UIComponent/QPAnswerCell.swift | 2 +- .../UIComponent/QPPopularAnswerCell.swift | 10 ++----- 6 files changed, 42 insertions(+), 26 deletions(-) diff --git a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift index 150e97a9..1ae9b409 100644 --- a/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift +++ b/Qapple/Qapple/SourceCode/Data/Repository/AnswerRepository.swift @@ -17,7 +17,7 @@ struct AnswerRepository { QappleAPI.TotalCount, QappleAPI.PaginationInfo ) - var fetchPopularAnswer: () async throws -> (Answer?, Question?, Bool) + var fetchPopularAnswer: (_ question: Question?) async throws -> (Answer?, Question?, Bool) var postAnswer: (_ questionId: Int, _ answer: String) async throws -> Void var deleteAnswer: (_ answerId: Int) async throws -> Void var likeAnswer: (_ questionId: Int) async throws -> Void @@ -118,7 +118,7 @@ extension AnswerRepository: DependencyKey { ) return (answerList, response.total, paginationInfo) }, - fetchPopularAnswer: { + fetchPopularAnswer: { question in let currentHour = Calendar.current.component(.hour, from: .now) if currentHour > 12 && currentHour < 19 { return (nil, nil, false) @@ -133,14 +133,14 @@ extension AnswerRepository: DependencyKey { ) } - guard let question = response.content.first else { return (nil, nil, true) } + guard let questionContent = response.content.first else { return (nil, nil, true) } - let currentQuestion = Question( - id: question.questionId, - content: question.content, - publishedDate: question.livedAt?.ISO8601ToDate(.yearMonthDateTime) ?? .now, - isAnswered: question.isAnswered, - isLived: question.questionStatus == ("LIVE") + let currentQuestion = question ?? Question( + id: questionContent.questionId, + content: questionContent.content, + publishedDate: questionContent.livedAt?.ISO8601ToDate(.yearMonthDateTime) ?? .now, + isAnswered: questionContent.isAnswered, + isLived: questionContent.questionStatus == ("LIVE") ) var popularAnswer: Answer = .init( @@ -164,7 +164,7 @@ extension AnswerRepository: DependencyKey { while hasNext { let answersOfQuestion = try await RepositoryService.shared.request { server, accessToken in try await AnswerAPI.fetchListOfQuestion( - questionId: Int(question.questionId), + questionId: Int(currentQuestion.id), threshold: threshold, pageSize: 30, server: server, @@ -282,14 +282,9 @@ extension AnswerRepository: DependencyKey { fetchAnswerListOfQuestion: { _, _ in (stubAnswerList, 25, .init(threshold: "", hasNext: false)) }, - fetchPopularAnswer: { - let currentHour = Calendar.current.component(.hour, from: .now) - if currentHour > 12 && currentHour < 19 { - return (nil, nil, false) - } + fetchPopularAnswer: { _ in let question = Question(id: 0, content: "", publishedDate: .now, isAnswered: false, isLived: true) - return (AnswerRepository.stubAnswerList.first!, question, false) }, postAnswer: { _, _ in }, diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift index ad91fd50..0f809c60 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListFeature.swift @@ -17,6 +17,7 @@ struct AnswerListFeature { var answerList: [Answer] = [] var totalCount: QappleAPI.TotalCount = 0 var paginationInfo = QappleAPI.PaginationInfo(threshold: "", hasNext: false) + var popularAnswerStatus: PopularAnswerCellStatus = .none var isLoading = false @Presents var sheet: Sheet.State? @Presents var alert: AlertState? @@ -35,6 +36,7 @@ struct AnswerListFeature { case backButtonTapped case likeAnswerButtonTapped(Answer) case answerCommentButtonTapped(Answer) + case fetchPopularAnswer(Answer) case likeAnswer(Answer) case toggleLoading(Bool) case sheet(PresentationAction) @@ -58,6 +60,22 @@ struct AnswerListFeature { let response = try await answerRepository.fetchAnswerListOfQuestion( question.id, nil ) + let currentHour = Calendar.current.component(.hour, from: .now) + + if question.isLived, !(currentHour > 12 && currentHour < 19){ + if !(currentHour > 12 && currentHour < 19) { + let response = try await answerRepository.fetchPopularAnswer(question) + if let answer = response.0 { + await send(.fetchPopularAnswer(answer)) + } + } + } else { + let response = try await answerRepository.fetchPopularAnswer(question) + if let answer = response.0 { + await send(.fetchPopularAnswer(answer)) + } + } + await send( .answerListResponse( response.0, @@ -132,6 +150,10 @@ struct AnswerListFeature { case .answerCommentButtonTapped: return .none + case let .fetchPopularAnswer(answer): + state.popularAnswerStatus = .popularAnswer(answer, state.question) + return .none + case let .likeAnswer(answer): guard let currentAnswerIdx = state.answerList.firstIndex(where: { $0.id == answer.id }) else { return .none } diff --git a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift index 0152330f..84040b00 100644 --- a/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift +++ b/Qapple/Qapple/SourceCode/Feature/2.QuestionTab/2-6.AnswerList/AnswerListView.swift @@ -131,6 +131,11 @@ private struct AnswerList: View { var body: some View { ScrollView { LazyVStack { + if case let .popularAnswer(answer, question) = store.state.popularAnswerStatus { + QPPopularAnswerCell(status: .popularAnswer(answer, question)) + .padding(.horizontal, 16) + } + ForEach(enumerated(store.answerList), id: \.element.id) { index, answer in QPAnswerCell( diff --git a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift index 7c10a194..80d4f179 100644 --- a/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift +++ b/Qapple/Qapple/SourceCode/Feature/3.BulletinBoard/3-1.BulletinBoard/BulletinBoardFeature.swift @@ -78,7 +78,7 @@ struct BulletinBoardFeature { do { let mainQuestion = try await fetchMainQuestion() let response = try await bulletinBoardRepository.fetchBulletinBoardList(nil) - let popularAnswer = try await fetchPopularAnswer() + let popularAnswer = try await fetchPopularAnswer(nil) await send(.fetchPopularAnswer(popularAnswer)) await send(.bulletinBoardListResponse(mainQuestion, response.0, response.1)) } catch { diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift index 849ec762..0e875155 100644 --- a/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift +++ b/Qapple/Qapple/SourceCode/UIComponent/QPAnswerCell.swift @@ -87,7 +87,7 @@ private struct NormalCell: View { .padding(.bottom, 5) Footer() - .padding(.bottom, 20) + .padding(.bottom, 14) .padding(.horizontal, 16) } .background(.first) diff --git a/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift index b157f27d..09f7dd5b 100644 --- a/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift +++ b/Qapple/Qapple/SourceCode/UIComponent/QPPopularAnswerCell.swift @@ -32,9 +32,9 @@ struct QPPopularAnswerCell: View { .foregroundStyle(.main) .padding(.top, 15) .padding(.leading, 6) - case let .popularAnswer(answer, _): + case let .popularAnswer(answer, question): VStack(alignment: .leading, spacing: 5) { - Text("오늘의 인기 답변") + Text(question.isLived ? "오늘의 인기 답변" : "인기 답변") .font(.pretendard(.light, size: 12)) .foregroundStyle(.main.opacity(0.5)) @@ -61,12 +61,6 @@ struct QPPopularAnswerCell: View { RoundedRectangle(cornerRadius: 12) .stroke(lineWidth: 1) .foregroundStyle( -// LinearGradient( -// colors: [.popularStart, .popularEnd], -// startPoint: .leading, -// endPoint: .trailing -// ) -// .opacity(0.17) RadialGradient(colors: [.popularStart, .popularEnd], center: .center, startRadius: 0, endRadius: 100) .opacity(0.17)