Skip to content
1 change: 1 addition & 0 deletions Qapple/Qapple/Resource/Constant/Constant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ enum Constant {

extension Constant {
static let isSignIn = "isSignIn"
static let recentQuestionID = "recentQuestionID"
static let userRandomID = "userRandomID"
}
2 changes: 1 addition & 1 deletion Qapple/Qapple/SourceCode/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension AppDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
#if DEBUG
RepositoryService.shared.configureServer(to: .production)
RepositoryService.shared.configureServer(to: .test)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디버그에서도 실제 서버로 들어가지길레 수정했습니다!

#else
RepositoryService.shared.configureServer(to: .production)
#endif
Expand Down
23 changes: 23 additions & 0 deletions Qapple/Qapple/SourceCode/Entity/AnswerComment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// AnswerComment.swift
// Qapple
//
// Created by 문인범 on 4/14/25.
//

import Foundation

// MARK: 임시 Entity
struct AnswerComment: Identifiable, Equatable {
let id: Int
let writeId: Int
let writerGeneration: String
let content: String
var heartCount: Int
var isLiked: Bool
let isMine: Bool
var isReport: Bool
let createdAt: Date

var anonymityId: Int
}
Comment on lines +11 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

임시로 만들어놓은 답변에 대한 댓글 엔티티 입니다.
추후 수정 예정

1 change: 1 addition & 0 deletions Qapple/Qapple/SourceCode/Entity/DataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ enum DataType: Equatable {
case answer(Answer)
case bulletinBoard(BulletinBoard)
case comment(BoardComment)
case answerComment(AnswerComment)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변-댓글의 신고 대응

}
6 changes: 3 additions & 3 deletions Qapple/Qapple/SourceCode/Entity/QuestionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ enum QuestionState: Codable {
/// 질문 및 답변 여부에 따라 변화하는 버튼 문자열
func buttonTitle(isAnswerd: Bool) -> String {
switch self {
case .creating: isAnswerd ? "다른 답변 둘러보기" : "이전 질문 답변하기"
case .ready: "질문에 답변하기"
case .complete: "다른 답변 둘러보기"
case .creating: isAnswerd ? "둘러보기" : "답변하기"
case .ready: "답변하기"
case .complete: "둘러보기"
}
}
}
17 changes: 17 additions & 0 deletions Qapple/Qapple/SourceCode/Feature/1.MainFlow/MainFlowFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ struct MainFlowFeature {
state.path.append(.report(.init(dataType: dataType)))
return .none

case let .questionTab(.todayQuestion((.answerCommentButtonTapped(answer)))):
state.path.append(.answerCommentList(.init(answer: answer)))
return .none

case let .bulletinBoardTab(.boardCellTapped(board)):
state.path.append(.comment(.init(board: board)))
return .none
Expand Down Expand Up @@ -137,6 +141,18 @@ struct MainFlowFeature {
state.path.append(.report(.init(dataType: dataType)))
return .none

case let .element(id: _, action: .answerList(.answerCommentButtonTapped(answer))):
state.path.append(.answerCommentList(.init(answer: answer)))
return .none

case let .element(id: _, action: .answerCommentList(.sheet(.presented(.seeMore(.reportButtonTapped(dataType)))))):
state.path.append(.report(.init(dataType: dataType)))
return .none

case let .element(id: _, action: .answerCommentList(.reportButtonTapped(answerComment))):
state.path.append(.report(.init(dataType: .answerComment(answerComment))))
return .none

case let .element(id: _, action: .bulletinBoardSearch(.sheet(.presented(.seeMore(.reportButtonTapped(dataType)))))):
state.path.append(.report(.init(dataType: dataType)))
return .none
Expand Down Expand Up @@ -207,6 +223,7 @@ extension MainFlowFeature {
case writeAnswer(WriteAnswerFeature)
case completeAnswer(CompleteAnswerFeature)
case answerList(AnswerListFeature)
case answerCommentList(AnswerCommentFeature)
case bulletinBoard(BulletinBoardFeature)
case bulletinBoardSearch(BulletinBoardSearchFeature)
case bulletinBoardPost(BulletinBoardPostFeature)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct MainFlowView: View {
case let .writeAnswer(store): WriteAnswerView(store: store)
case let .completeAnswer(store): CompleteAnswerView(store: store)
case let .answerList(store): AnswerListView(store: store)
case let .answerCommentList(store): AnswerCommentView(store: store)
case let .bulletinBoard(store): BulletinBoardView(store: store)
case let .bulletinBoardSearch(store): BulletinBoardSearchView(store: store)
case let .bulletinBoardPost(store): BulletinBoardPostView(store: store)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ struct TodayQuestionFeature {
var timeRemainingForQuestion: TimeInterval = 0
var isLoading = true
var isFirstLaunch = true
var isNewQuestion = false
@Presents var sheet: Sheet.State?
@Presents var alert: AlertState<Action.Alert>?
@Shared(.appStorage(Constant.recentQuestionID)) var recentQuestionID = 0
}

enum Action {
Expand All @@ -34,6 +36,7 @@ struct TodayQuestionFeature {
case questionButtonTapped(Question)
case seeAllAnswerButtonTapped(Question)
case seeMoreAnswerButtonTapped(Answer)
case answerCommentButtonTapped(Answer)
case questionTimerTick
case cancelQuestionTimer
case toggleLoading(Bool)
Expand Down Expand Up @@ -79,6 +82,14 @@ struct TodayQuestionFeature {
case let .mainQuestionResponse(mainQuestion):
state.isFirstLaunch = false
state.todayQuestion = mainQuestion

if mainQuestion.id != state.recentQuestionID {
state.isNewQuestion = true
state.$recentQuestionID.withLock { $0 = mainQuestion.id }
} else {
state.isNewQuestion = false
}

if isQuestionLiveTime {
state.questionState = mainQuestion.isAnswered ? .complete : .ready
return .none
Expand Down Expand Up @@ -131,6 +142,9 @@ struct TodayQuestionFeature {
case .cancelQuestionTimer:
return .cancel(id: CancelID.questionTimer)

case .answerCommentButtonTapped:
return .none

case let .sheet(.presented(.seeMore(.alert(.presented(.confirmDeletion(sheetData)))))):
guard case let .answer(answer) = sheetData else { return .none }
return .run { send in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ struct TodayQuestionView: View {
@Bindable var store: StoreOf<TodayQuestionFeature>

var body: some View {
ZStack {
Color.second.ignoresSafeArea()

ScrollView {
VStack(spacing: 0) {
HeaderView(store: store)
QuestionButton(store: store)
AnswerPreviewList(store: store)
ScrollView {
VStack(spacing: 0) {
if store.isNewQuestion {
LargeHeaderView(store: store)
LargeQuestionButton(store: store)
} else {
SmallHeaderView(store: store)
}
AnswerPreviewList(store: store)
}
.scrollIndicators(.hidden)
}
.background(.second)
.scrollIndicators(.hidden)
.onAppear {
store.send(.onAppear)
}
Expand All @@ -36,20 +37,98 @@ struct TodayQuestionView: View {
}
.loadingIndicator(isLoading: store.isLoading)
.alert($store.scope(state: \.alert, action: \.alert))
.sheet(item: $store.scope(
state: \.sheet,
action: \.sheet)
) { store in
.sheet(item: $store.scope(state: \.sheet, action: \.sheet)) { store in
switch store.case {
case let .seeMore(store): SeeMoreSheet(store: store)
}
}
}
}

// MARK: - HeaderView
// MARK: - SmallHeaderView

private struct SmallHeaderView: View {

let store: StoreOf<TodayQuestionFeature>

var body: some View {
ZStack {
Color.first.ignoresSafeArea()

HStack(spacing: 8) {
Image(store.questionState.graphicImage)
.resizable()
.scaledToFill()
.frame(width: 40, height: 40)

Text(store.questionState.mainTitle)
.font(.pretendard(.semiBold, size: 18))
.foregroundStyle(.wh)
.tracking(-1)

Spacer()

if store.questionState == .creating {
QuestionTimer()
} else {
AnsweringButton()
}
}
.padding(.horizontal, 24)
.frame(maxWidth: .infinity)
.frame(height: 90)
.background(.second)
.cornerRadius(32, corners: [.bottomLeft, .bottomRight])
}
}

/// 질문 타이머
private func QuestionTimer() -> some View {
Text(store.timeRemainingForQuestion.timerFormat)
.font(.pretendard(.bold, size: 22))
.foregroundStyle(LinearGradient.timer)
.monospacedDigit()
.kerning(-2)
}

/// 답변하기 버튼
private func AnsweringButton() -> some View {
Button {
store.send(.questionButtonTapped(store.todayQuestion))
} label: {
Text(title)
.font(.pretendard(.semiBold, size: 15))
.frame(width: 84)
.padding(.vertical, 10)
.foregroundStyle(.main)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
.opacity(store.isLoading ? 0 : 1)
.buttonStyle(ScalableButtonStyle())
}

/// 버튼 제목
private var title: String {
store.questionState.buttonTitle(
isAnswerd: store.todayQuestion.isAnswered
)
}

/// 버튼 배경 색상
private var backgroundColor: Color {
switch store.questionState {
case .creating:
store.todayQuestion.isAnswered ? .secondaryButton : .button
case .ready: .button
case .complete: .secondaryButton
}
}
}

// MARK: - LargeHeaderView

private struct HeaderView: View {
private struct LargeHeaderView: View {

@State private var offsetY: CGFloat = 0

Expand Down Expand Up @@ -93,9 +172,9 @@ private struct HeaderView: View {
}
}

// MARK: - QuestionButton
// MARK: - LargeQuestionButton

private struct QuestionButton: View {
private struct LargeQuestionButton: View {

let store: StoreOf<TodayQuestionFeature>

Expand Down Expand Up @@ -141,7 +220,6 @@ private struct QuestionButton: View {
}
}
}

// MARK: - AnswerPreviewList

private struct AnswerPreviewList: View {
Expand All @@ -165,7 +243,7 @@ private struct AnswerPreviewList: View {
if store.answerPreviewList.isEmpty {
Spacer()

Text("아직 답변이 달리지않았어요\n첫 답변을 달아보세요!")
Text(" 답변을 달아보세요!")
.font(.pretendard(.semiBold, size: 14))
.foregroundStyle(.sub4)
.multilineTextAlignment(.center)
Expand Down Expand Up @@ -221,6 +299,12 @@ private struct AnswerPreviewList: View {
state: .normal,
seeMoreAction: {
store.send(.seeMoreAnswerButtonTapped(answer))
},
likeAction: {

},
commentAction: {
store.send(.answerCommentButtonTapped(answer))
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct AnswerListFeature {
case networkingFailed(Error)
case seeMoreAction(Answer)
case backButtonTapped
case likeAnswerButtonTapped
case answerCommentButtonTapped(Answer)
case toggleLoading(Bool)
case sheet(PresentationAction<Sheet.Action>)
case alert(PresentationAction<Alert>)
Expand Down Expand Up @@ -113,6 +115,13 @@ struct AnswerListFeature {
state.answerList = state.answerList.reversed().filter(UserDefaults.filterAnswerBlockedUser)
return .none

case .likeAnswerButtonTapped:
// TODO: 좋아요 기능 구현 필요
return .none

case .answerCommentButtonTapped:
return .none

Comment on lines +122 to +124
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변-댓글 뷰로 이동하는 액션

case let .networkingFailed(error):
HapticService.notification(type: .error)
state.alert = .failedNetworking(with: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ private struct AnswerList: View {
state: .normal,
seeMoreAction: {
store.send(.seeMoreAction(answer))
},
likeAction: {
store.send(.likeAnswerButtonTapped)
},
commentAction: {
store.send(.answerCommentButtonTapped(answer))
}
)
.configurePagination(
Expand Down
Loading