diff --git a/Encrypted/Secrets/GoogleService-Info.plist.encrypted b/Encrypted/Secrets/GoogleService-Info.plist.encrypted index 58ede7d0..dbe4cb73 100644 Binary files a/Encrypted/Secrets/GoogleService-Info.plist.encrypted and b/Encrypted/Secrets/GoogleService-Info.plist.encrypted differ diff --git a/Projects/Domain/Sources/Client/KeymeTestsClient.swift b/Projects/Domain/Sources/Client/KeymeTestsClient.swift index f4be857b..1e508f62 100644 --- a/Projects/Domain/Sources/Client/KeymeTestsClient.swift +++ b/Projects/Domain/Sources/Client/KeymeTestsClient.swift @@ -35,12 +35,12 @@ private func getClient(with network: KeymeAPIManager) -> KeymeTestsClient { let api = KeymeAPI.test(.onboarding) let response = try await network.request(api, object: KeymeTestsDTO.self) - return response.toIconModel() + return response.toKeymeTestsModel() }, fetchDailyTests: { let api = KeymeAPI.test(.daily) let response = try await network.request(api, object: KeymeTestsDTO.self) - return response.toIconModel() + return response.toKeymeTestsModel() }, fetchTestResult: { testResultId in let api = KeymeAPI.test(.result(testResultId)) let response = try await network.request(api, object: TestResultDTO.self) diff --git a/Projects/Domain/Sources/Client/TestClient.swift b/Projects/Domain/Sources/Client/TestClient.swift deleted file mode 100644 index 6e98be09..00000000 --- a/Projects/Domain/Sources/Client/TestClient.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// TestClient.swift -// Domain -// -// Created by 김영인 on 2023/07/29. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import ComposableArchitecture - -import Network - -public struct TestClient { - public var fetchTest: @Sendable () async throws -> TestModel -} - -extension DependencyValues { - public var testClient: TestClient { - get { self[TestClient.self] } - set { self[TestClient.self] = newValue } - } -} - -extension TestClient: DependencyKey { - public static var liveValue = TestClient( - fetchTest: { -// let api = TestAPI.hello -// let response = try await TestAPIManager.shared.request(api) -// let decoded = String(data: response.data, encoding: .utf8)! -// -// return TestDTO(hello: decoded).toModel() - return .init(hello: "") - } - ) -} diff --git a/Projects/Domain/Sources/Model/CirclePack/CharacterScore.swift b/Projects/Domain/Sources/Model/CirclePack/CharacterScore.swift index f00bacd5..589cda57 100644 --- a/Projects/Domain/Sources/Model/CirclePack/CharacterScore.swift +++ b/Projects/Domain/Sources/Model/CirclePack/CharacterScore.swift @@ -6,6 +6,7 @@ // Copyright © 2023 team.humanwave. All rights reserved. // +import Network import Foundation public struct CharacterScore: Identifiable, Equatable { @@ -18,3 +19,11 @@ public struct CharacterScore: Identifiable, Equatable { self.date = date } } + +public extension QuestionResultScoresDTO { + func toCharacterScores() -> [CharacterScore] { + return self.data.results.map { resultItem in + CharacterScore(score: resultItem.score, date: resultItem.createdAt) + } + } +} diff --git a/Projects/Domain/Sources/Model/KeymeTestsModel.swift b/Projects/Domain/Sources/Model/KeymeTestsModel.swift index 78d29139..348baf68 100644 --- a/Projects/Domain/Sources/Model/KeymeTestsModel.swift +++ b/Projects/Domain/Sources/Model/KeymeTestsModel.swift @@ -13,7 +13,6 @@ import Network import Kingfisher public struct KeymeTestsModel: Equatable { - public let nickname: String public let testId: Int public let icons: [IconModel] } @@ -26,14 +25,12 @@ public struct IconModel: Equatable, Hashable { } public extension KeymeTestsDTO { - func toIconModel() -> KeymeTestsModel { - let nickname = data.owner.nickname + func toKeymeTestsModel() -> KeymeTestsModel { let icons = data.questions.map { IconModel(imageURL: $0.category.iconUrl, color: Color.hex($0.category.color)) } - return KeymeTestsModel(nickname: nickname ?? "키미", - testId: data.testId, - icons: icons) + + return KeymeTestsModel(testId: data.testId, icons: icons) } } diff --git a/Projects/Domain/Sources/Model/TestModel.swift b/Projects/Domain/Sources/Model/TestModel.swift deleted file mode 100644 index dd6acd9c..00000000 --- a/Projects/Domain/Sources/Model/TestModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// TestModel.swift -// Domain -// -// Created by 김영인 on 2023/07/29. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import Foundation - -import Network - -public struct TestModel { - public let hello: String -} - -public extension TestDTO { - func toModel() -> TestModel { - return .init(hello: hello) - } -} diff --git a/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift new file mode 100644 index 00000000..2a80651c --- /dev/null +++ b/Projects/Features/Sources/Home/KeymeTestHomeFeature.swift @@ -0,0 +1,61 @@ +// +// KeymeTestHomeFeature.swift +// Features +// +// Created by 이영빈 on 2023/08/30. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture +import Domain +import Network + +struct KeymeTestsHomeFeature: Reducer { + @Dependency(\.keymeAPIManager) private var network + + // 테스트를 아직 풀지 않았거나, 풀었거나 2가지 케이스만 존재 + struct State: Equatable { + @PresentationState var testStartViewState: KeymeTestsStartFeature.State? + var view: View + + struct View: Equatable { + let nickname: String + var dailyTestId: Int? + } + + init(nickname: String) { + self.view = View(nickname: nickname) + } + } + + enum Action { + case fetchDailyTests + case showTestStartView(testData: KeymeTestsModel) + case startTest(PresentationAction) + + enum View {} + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .fetchDailyTests: + return .run { send in + let fetchedTest = try await network.request(.test(.daily), object: KeymeTestsDTO.self) + let testData = fetchedTest.toKeymeTestsModel() + + await send(.showTestStartView(testData: testData)) + } + + case .showTestStartView(let testData): + state.view.dailyTestId = testData.testId + state.testStartViewState = .init(nickname: state.view.nickname, testData: testData) + + default: + break + } + + return .none + } + } +} diff --git a/Projects/Features/Sources/Home/KeymeTestHomeView.swift b/Projects/Features/Sources/Home/KeymeTestHomeView.swift new file mode 100644 index 00000000..a4bae6ab --- /dev/null +++ b/Projects/Features/Sources/Home/KeymeTestHomeView.swift @@ -0,0 +1,71 @@ +// +// KeymeTestHomeView.swift +// Features +// +// Created by 이영빈 on 2023/08/30. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import ComposableArchitecture +import DSKit +import SwiftUI + +struct KeymeTestsHomeView: View { + var store: StoreOf + + init(store: StoreOf) { + self.store = store + store.send(.fetchDailyTests) + } + + var body: some View { + WithViewStore(store, observe: { $0.view }) { viewStore in + ZStack(alignment: .center) { + DSKitAsset.Color.keymeBlack.swiftUIColor.ignoresSafeArea() + + VStack(alignment: .leading) { + // Filler + Spacer().frame(height: 75) + + welcomeText(nickname: viewStore.nickname) + .foregroundColor(DSKitAsset.Color.keymeWhite.swiftUIColor) + + Spacer() + } + .fullFrame() + .padding(.horizontal, 16) + + // 테스트 뷰 + testView + + // 결과 화면 표시도 생각 + + } + } + } +} + +extension KeymeTestsHomeView { + var testView: some View { + let startTestStore = store.scope( + state: \.$testStartViewState, + action: KeymeTestsHomeFeature.Action.startTest) + + return IfLetStore(startTestStore) { store in + KeymeTestsStartView(store: store) + } else: { + Circle() + .strokeBorder(.white.opacity(0.3), lineWidth: 1) + .background(Circle().foregroundColor(.white.opacity(0.3))) + .frame(width: 280, height: 280) + } + } + + func welcomeText(nickname: String) -> some View { + Text.keyme( + "환영해요 \(nickname)님!", +// "환영해요 \(viewStore.nickname)님!\n이제 문제를 풀어볼까요?", + font: .heading1) + } + +} diff --git a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift index 98daceff..3f74eed0 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartFeature.swift @@ -13,18 +13,21 @@ import Domain public struct KeymeTestsStartFeature: Reducer { public struct State: Equatable { + public let nickname: String + public let testData: KeymeTestsModel + + public var icon: IconModel = .EMPTY public var keymeTests: KeymeTestsFeature.State? public var isAnimating: Bool = false - public var nickname: String? - public var testId: Int = 18 // TODO: change - public var icon: IconModel = .EMPTY - public init() { } + public init(nickname: String, testData: KeymeTestsModel) { + self.nickname = nickname + self.testData = testData + } } public enum Action { case viewWillAppear - case fetchDailyTests(TaskResult) case startAnimation([IconModel]) case setIcon(IconModel) case startButtonDidTap @@ -40,24 +43,8 @@ public struct KeymeTestsStartFeature: Reducer { Reduce { state, action in switch action { case .viewWillAppear: - return .run { send in - await send(.fetchDailyTests( - TaskResult { - try await self.keymeTestsClient.fetchDailyTests() - } - )) - } - - case let .fetchDailyTests(.success(tests)): - state.nickname = tests.nickname - state.testId = tests.testId state.isAnimating = true - - return .send(.startAnimation(tests.icons)) - - case .fetchDailyTests(.failure): - state.nickname = "키미" // TODO: 변경 - return .send(.startAnimation([IconModel.EMPTY])) + return .send(.startAnimation(state.testData.icons)) case .startAnimation(let icons): return .run { send in @@ -74,7 +61,7 @@ public struct KeymeTestsStartFeature: Reducer { state.isAnimating = true case .startButtonDidTap: - let url = "https://keyme-frontend.vercel.app/test/\(state.testId)" + let url = "https://keyme-frontend.vercel.app/test/\(state.testData.testId)" state.keymeTests = KeymeTestsFeature.State(url: url) case .keymeTests: diff --git a/Projects/Features/Sources/Home/KeymeTestsStartView.swift b/Projects/Features/Sources/Home/KeymeTestsStartView.swift index 56b07c8d..f69d9742 100644 --- a/Projects/Features/Sources/Home/KeymeTestsStartView.swift +++ b/Projects/Features/Sources/Home/KeymeTestsStartView.swift @@ -24,48 +24,26 @@ public struct KeymeTestsStartView: View { public var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - VStack { - IfLetStore( - self.store.scope( - state: \.keymeTests, - action: KeymeTestsStartFeature.Action.keymeTests - ), - then: { store in - KeymeTestsView(store: store) - .ignoresSafeArea(.all) - .transition(.scale.animation(.easeIn)) - }, - else: { - Spacer() - .frame(height: 75) - - welcomeText(viewStore) - - Spacer() - - startTestsButton(viewStore) - .onTapGesture { - viewStore.send(.startButtonDidTap) - } - - Spacer() - } - ) - } - .frame(maxWidth: .infinity) - .background(DSKitAsset.Color.keymeBlack.swiftUIColor) + IfLetStore( + self.store.scope( + state: \.keymeTests, + action: KeymeTestsStartFeature.Action.keymeTests + ), + then: { store in + KeymeTestsView(store: store) + .ignoresSafeArea(.all) + .transition(.scale.animation(.easeIn)) + }, + else: { + startTestsButton(viewStore) + .onTapGesture { + viewStore.send(.startButtonDidTap) + } + } + ) } } - func welcomeText(_ viewStore: ViewStore) -> some View { - Text.keyme( - "환영해요 \(viewStore.nickname ?? "키미")님!\n이제 문제를 풀어볼까요?", - font: .heading1) // TODO: 닉변 - .foregroundColor(DSKitAsset.Color.keymeWhite.swiftUIColor) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(Padding.insets(leading: 16)) - } - func startTestsButton(_ viewStore: ViewStore) -> some View { ZStack { diff --git a/Projects/Features/Sources/MainPage/MainPageFeature.swift b/Projects/Features/Sources/MainPage/MainPageFeature.swift index 3d401171..80fd4e0b 100644 --- a/Projects/Features/Sources/MainPage/MainPageFeature.swift +++ b/Projects/Features/Sources/MainPage/MainPageFeature.swift @@ -11,13 +11,22 @@ import ComposableArchitecture public struct MainPageFeature: Reducer { public struct State: Equatable { - public init() { } + let userId: Int + let nickname: String + + public init(userId: Int, nickname: String) { + self.userId = userId + self.nickname = nickname + } } - public enum Action { } + public enum Action { + case logout + case changeNickname(String) + } public var body: some Reducer { - Reduce { _, action in + Reduce { _, _ in return .none } } diff --git a/Projects/Features/Sources/MainPage/MainPageView.swift b/Projects/Features/Sources/MainPage/MainPageView.swift index 1218bbcf..e8d2b9c8 100644 --- a/Projects/Features/Sources/MainPage/MainPageView.swift +++ b/Projects/Features/Sources/MainPage/MainPageView.swift @@ -20,37 +20,35 @@ struct KeymeMainView: View { self.store = store } - private var myPageStore = Store(initialState: MyPageFeature.State()) { - MyPageFeature() - } - enum Tab: Int { case home, myPage } var body: some View { - WithViewStore(store, observe: { $0 }) { _ in + WithViewStore(store, observe: { $0 }) { viewStore in TabView(selection: $selectedTab) { - KeymeTestsStartView(store: Store( - initialState: KeymeTestsStartFeature.State()) { - KeymeTestsStartFeature() - }) + KeymeTestsHomeView(store: Store( + initialState: KeymeTestsHomeFeature.State( + nickname: viewStore.nickname) + ) { + KeymeTestsHomeFeature() + }) .tabItem { homeTabImage - .resizable() - .frame(width: 24, height: 24) - .aspectRatio(contentMode: .fit) } .tag(Tab.home) - MyPageView(store: myPageStore) - .tabItem { - myPageTabImage - .resizable() - .frame(width: 24, height: 24) - .aspectRatio(contentMode: .fit) - } - .tag(Tab.myPage) + MyPageView(store: Store( + initialState: MyPageFeature.State( + userId: viewStore.state.userId, + nickname: viewStore.state.nickname) + ) { + MyPageFeature() + }) + .tabItem { + myPageTabImage + } + .tag(Tab.myPage) } .introspect(.tabView, on: .iOS(.v16, .v17)) { tabViewController in let tabBar = tabViewController.tabBar diff --git a/Projects/Features/Sources/MyPage/MyPageFeature.swift b/Projects/Features/Sources/MyPage/MyPageFeature.swift index 988a075b..23a3b48c 100644 --- a/Projects/Features/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Features/Sources/MyPage/MyPageFeature.swift @@ -6,6 +6,7 @@ // Copyright © 2023 team.humanwave. All rights reserved. // +import Core import ComposableArchitecture import Domain import DSKit @@ -22,49 +23,64 @@ struct Coordinate { struct MyPageFeature: Reducer { @Dependency(\.keymeAPIManager) private var network - @Dependency(\.userStorage) private var userStorage struct State: Equatable { - var selectedSegment: MyPageSegment = .similar - var shownFirstTime = true var similarCircleDataList: [CircleData] = [] var differentCircleDataList: [CircleData] = [] - var shownCircleDatalist: [CircleData] = [] - var circleShown = false + var view: View + var scoreListState: ScoreListFeature.State = .init() + + struct View: Equatable { + let userId: Int + let nickname: String + + var circleShown = false + var selectedSegment: MyPageSegment = .similar + var shownFirstTime = true + var shownCircleDatalist: [CircleData] = [] + } + + init(userId: Int, nickname: String) { + self.view = View(userId: userId, nickname: nickname) + } } + enum Action: Equatable { - case selectSegement(MyPageSegment) - case requestCircle(MatchRate) case saveCircle([CircleData], MatchRate) - case markViewAsShown - case circleTapped - case circleDismissed + case showCircle(MyPageSegment) + case requestCircle(MatchRate) + case view(View) + case scoreListAction(ScoreListFeature.Action) + + enum View: Equatable { + case markViewAsShown + case circleTapped + case circleDismissed + case selectSegement(MyPageSegment) + } } + // 마이페이지를 사용할 수 없는 케이스 + // 1. 원 그래프가 아직 집계되지 않음 -> 빈 화면 페이지 + // 2. 네트워크가 연결되지 않음 -> 네트워크 미연결 안내 public var body: some ReducerOf { - Reduce { state, action in + Scope(state: \.scoreListState, action: /Action.scoreListAction) { + ScoreListFeature() + } + + Reduce { state, action in switch action { - case .selectSegement(let segment): - state.selectedSegment = segment - switch segment { - case .different : - state.shownCircleDatalist = state.differentCircleDataList - case .similar : - state.shownCircleDatalist = state.similarCircleDataList - } - - return .none - + case .view(.selectSegement(let segment)): + state.view.selectedSegment = segment + return .send(.showCircle(state.view.selectedSegment)) + // 서버 부하가 있으므로 웬만하면 한 번만 콜 할 것 case .requestCircle(let rate): + let userId = state.view.userId + switch rate { case .top5: - guard let userId = userStorage.userId else { - // TODO: Throw - return .none - } - - return .run { send in + return .run(priority: .userInitiated) { send in let response = try await network.request( .myPage(.statistics(userId, .similar)), object: CircleData.NetworkResult.self) @@ -73,12 +89,7 @@ struct MyPageFeature: Reducer { } case .low5: - guard let userId = userStorage.userId else { - // TODO: Throw - return .none - } - - return .run { send in + return .run(priority: .userInitiated) { send in let response = try await network.request( .myPage(.statistics(userId, .different)), object: CircleData.NetworkResult.self) @@ -95,18 +106,32 @@ struct MyPageFeature: Reducer { state.differentCircleDataList = data } + return .send(.showCircle(state.view.selectedSegment)) + + case .showCircle(let segment): + switch segment { + case .similar: + state.view.shownCircleDatalist = state.similarCircleDataList + case .different: + state.view.shownCircleDatalist = state.differentCircleDataList + } + return .none + + case .view(.markViewAsShown): + state.view.shownFirstTime = false return .none - case .markViewAsShown: - state.shownFirstTime = false + case .view(.circleTapped): + HapticManager.shared.tok() + state.view.circleShown = true return .none - case .circleTapped: - state.circleShown = true + case .view(.circleDismissed): + state.view.circleShown = false return .none - case .circleDismissed: - state.circleShown = false + case .scoreListAction: + print("score") return .none } } diff --git a/Projects/Features/Sources/MyPage/MyPageView.swift b/Projects/Features/Sources/MyPage/MyPageView.swift index 4b0b8be8..c630bc45 100644 --- a/Projects/Features/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/Sources/MyPage/MyPageView.swift @@ -7,7 +7,6 @@ // import ComposableArchitecture -import Core import Domain import DSKit import SwiftUI @@ -16,31 +15,31 @@ struct MyPageView: View { @Namespace private var namespace private let store: StoreOf - private let scoreListStore: StoreOf init(store: StoreOf) { self.store = store - self.scoreListStore = Store(initialState: ScoreListFeature.State(), reducer: { - ScoreListFeature() - }) - + store.send(.requestCircle(.top5)) store.send(.requestCircle(.low5)) - store.send(.selectSegement(.similar)) - - scoreListStore.send(.loadScores) + store.send(.view(.selectSegement(.similar))) } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(store, observe: \.view, send: MyPageFeature.Action.view) { viewStore in ZStack(alignment: .topLeading) { CirclePackView( namespace: namespace, data: viewStore.shownCircleDatalist, detailViewBuilder: { data in + let scoreListStore = store.scope( + state: \.scoreListState, + action: MyPageFeature.Action.scoreListAction) + ScoreListView( - nickname: "키미", + ownerId: viewStore.userId, + questionId: data.metadata.questionId, + nickname: viewStore.nickname, keyword: data.metadata.keyword, store: scoreListStore) }) @@ -48,7 +47,6 @@ struct MyPageView: View { .activateCircleBlink(viewStore.state.shownFirstTime) .onCircleTapped { _ in viewStore.send(.circleTapped) - HapticManager.shared.tok() } .onCircleDismissed { _ in withAnimation(Animation.customInteractiveSpring()) { @@ -84,7 +82,7 @@ struct MyPageView: View { .padding(.horizontal, 17) .padding(.top, 25) - Text.keyme("친구들이 생각하는\n키미님의 성격은?", font: .heading1) // TODO: Change nickname + Text.keyme("친구들이 생각하는\n\(viewStore.nickname)님의 성격은?", font: .heading1) .padding(17) .transition(.opacity) } diff --git a/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift b/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift index ec1b1f39..ff3c7dd6 100644 --- a/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift +++ b/Projects/Features/Sources/MyPage/ScoreList/ScoreListFeature.swift @@ -7,3 +7,54 @@ // import Foundation +import ComposableArchitecture +import Domain +import Network + +struct ScoreListFeature: Reducer { + @Dependency(\.keymeAPIManager) private var network + + public struct State: Equatable { + var canFetch = true + var totalCount: Int? + var scores: [CharacterScore] + + public init(totalCount: Int? = nil, scores: [CharacterScore] = []) { + self.scores = [] + } + } + + public enum Action: Equatable { + case loadScores(ownerId: Int, questionId: Int, limit: Int) + case saveScores(totalCount: Int, scores: [CharacterScore]) + } + + public var body: some ReducerOf { + Reduce { state, action in + switch action { + case let .loadScores(ownerId, questionId, limit): + state.canFetch = false + + return .run { send in + let questionScores = try await network.request( + .question( + .scores(ownerId: ownerId, questionId: questionId, limit: limit) + ), + object: QuestionResultScoresDTO.self + ).toCharacterScores() + + await send(.saveScores( + totalCount: questionScores.count, + scores: questionScores)) + } + + case let .saveScores(totalCount, data): + state.totalCount = totalCount + state.scores.append(contentsOf: data) + + state.canFetch = true + } + return .none + } + } +} diff --git a/Projects/Features/Sources/MyPage/ScoreList/ScoreListView.swift b/Projects/Features/Sources/MyPage/ScoreList/ScoreListView.swift index cf034cc4..5cf6d385 100644 --- a/Projects/Features/Sources/MyPage/ScoreList/ScoreListView.swift +++ b/Projects/Features/Sources/MyPage/ScoreList/ScoreListView.swift @@ -13,56 +13,30 @@ import ComposableArchitecture import Domain import DSKit -struct ScoreListFeature: Reducer { - public struct State: Equatable { - var totalCount: Int? - var scores: [CharacterScore] - - public init(totalCount: Int? = nil, scores: [CharacterScore] = []) { - self.scores = [] - } - } - - public enum Action: Equatable { - case loadScores - case saveScores(totalCount: Int, scores: [CharacterScore]) - } - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .loadScores: - return .run { send in - try await Task.sleep(until: .now + .seconds(0.5), clock: .continuous) - await send(.saveScores( - totalCount: 42, - scores: (0..<42).map { i in - let randomInterval = TimeInterval(-2 * i) - return CharacterScore(score: Int.random(in: 1...5), date: Date().addingTimeInterval(randomInterval + Double(Int.random(in: 0...1)))) - } - )) - } - - case .saveScores(let totalCount, let data): - state.totalCount = totalCount - state.scores = data - } - return .none - } - } -} - struct ScoreListView: View { + private let scoreFetchLimit = 20 + private let formatter: RelativeDateTimeFormatter + + private let ownerId: Int + private let questionId: Int private let nickname: String private let keyword: String private let store: StoreOf - init(nickname: String, keyword: String, store: StoreOf) { + init( + ownerId: Int, + questionId: Int, + nickname: String, + keyword: String, + store: StoreOf + ) { self.formatter = RelativeDateTimeFormatter() formatter.locale = Locale(identifier: "ko_KR") formatter.dateTimeStyle = .named + self.ownerId = ownerId + self.questionId = questionId self.nickname = nickname self.keyword = keyword self.store = store @@ -104,16 +78,29 @@ struct ScoreListView: View { .cornerRadius(16) .onAppear { if - let thirdToLast = viewStore.state.scores.dropLast(2).last, - thirdToLast == scoreData + let thirdToLastItem = viewStore.state.scores.dropLast(2).last, + thirdToLastItem == scoreData { -// viewStore.send(.loadScores) + guard viewStore.canFetch else { return } + viewStore.send( + .loadScores( + ownerId: self.ownerId, + questionId: self.questionId, + limit: scoreFetchLimit)) } } } } .padding(.horizontal, 17) } + .onAppear { + guard viewStore.canFetch else { return } + viewStore.send( + .loadScores( + ownerId: self.ownerId, + questionId: self.questionId, + limit: scoreFetchLimit)) + } } } diff --git a/Projects/Features/Sources/Root/RootFeature.swift b/Projects/Features/Sources/Root/RootFeature.swift index 999380e3..ea4e001d 100644 --- a/Projects/Features/Sources/Root/RootFeature.swift +++ b/Projects/Features/Sources/Root/RootFeature.swift @@ -24,6 +24,7 @@ public struct RootFeature: Reducer { @PresentationState public var logInStatus: SignInFeature.State? @PresentationState public var registrationState: RegistrationFeature.State? @PresentationState public var onboardingStatus: OnboardingFeature.State? + @PresentationState public var mainPageState: MainPageFeature.State? public init( isLoggedIn: Bool? = nil, @@ -57,10 +58,9 @@ public struct RootFeature: Reducer { case login(PresentationAction) case registration(PresentationAction) case onboarding(PresentationAction) + case mainPage(PresentationAction) case onboardingChecked(TaskResult) - case mainPage(MainPageFeature.Action) - case checkUserStatus case checkLoginStatus @@ -68,6 +68,7 @@ public struct RootFeature: Reducer { case checkOnboardingStatus case updateMemberInformation + case startMainPage(userId: Int, nickname: String) } public var body: some ReducerOf { @@ -173,7 +174,7 @@ public struct RootFeature: Reducer { return .none case .updateMemberInformation: - return .run(priority: .userInitiated) { _ in + return .run(priority: .userInitiated) { send in let memberInformation = try await network.request( .member(.fetch), object: MemberUpdateDTO.self).data @@ -202,8 +203,16 @@ public struct RootFeature: Reducer { _ = try await network.request(.registerPushToken(.register(token))) } + + await send(.startMainPage( + userId: memberInformation.id, + nickname: memberInformation.nickname)) } + case .startMainPage(let userId, let nickname): + state.mainPageState = MainPageFeature.State(userId: userId, nickname: nickname) + return .none + default: return .none } @@ -217,5 +226,8 @@ public struct RootFeature: Reducer { .ifLet(\.$onboardingStatus, action: /Action.onboarding) { OnboardingFeature() } + .ifLet(\.$mainPageState, action: /Action.mainPage) { + MainPageFeature() + } } } diff --git a/Projects/Features/Sources/Root/RootView.swift b/Projects/Features/Sources/Root/RootView.swift index a19c81af..56d26c43 100644 --- a/Projects/Features/Sources/Root/RootView.swift +++ b/Projects/Features/Sources/Root/RootView.swift @@ -42,7 +42,7 @@ public struct RootView: View { let registrationStore = store.scope( state: \.$registrationState, action: RootFeature.Action.registration) - + IfLetStore(registrationStore) { store in RegistrationView(store: store) } @@ -60,11 +60,14 @@ public struct RootView: View { } } else { // 가입했고 온보딩을 진행한 유저 - KeymeMainView(store: Store( - initialState: MainPageFeature.State()) { - MainPageFeature() - }) - .transition(.opacity) + let mainPageStore = store.scope(state: \.$mainPageState, action: RootFeature.Action.mainPage) + + IfLetStore(mainPageStore) { store in + KeymeMainView(store: store) + .transition(.opacity) + } else: { + Text("에러") + } } } } diff --git a/Projects/Features/Sources/Test/TestStore.swift b/Projects/Features/Sources/Test/TestStore.swift deleted file mode 100644 index 9fa659e2..00000000 --- a/Projects/Features/Sources/Test/TestStore.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// TestStore.swift -// Features -// -// Created by 김영인 on 2023/07/29. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import ComposableArchitecture - -import Domain - -public struct TestStore: ReducerProtocol { - public struct State: Equatable { - var text: String? - - public init(text: String? = nil) { - self.text = text - } - } - - public enum Action { - case testResponse(TaskResult) - case buttonDidTap - } - - @Dependency(\.testClient) var testClient - - public init() { } - - public var body: some ReducerProtocol { - Reduce { state, action in - switch action { - case let .testResponse(.success(textModel)): - state.text = textModel.hello - case .testResponse(.failure): - state.text = nil - case .buttonDidTap: - return .run { send in - await send(.testResponse( - TaskResult { try await self.testClient.fetchTest() } - )) - } - } - return .none - } - } -} diff --git a/Projects/Features/Sources/Test/TestView.swift b/Projects/Features/Sources/Test/TestView.swift deleted file mode 100644 index 4f489cdb..00000000 --- a/Projects/Features/Sources/Test/TestView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// TestView.swift -// Features -// -// Created by 김영인 on 2023/07/29. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import SwiftUI - -import ComposableArchitecture - -import DSKit - -public struct TestView: View { - public let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - WithViewStore(self.store, observe: { $0 }) { viewStore in - VStack(spacing: 50) { - Text(viewStore.text ?? "") - .font(Font.Keyme.body2) - .foregroundColor(DSKitAsset.Color.keymeBlack.swiftUIColor) - - DSKitAsset.Image.camera.swiftUIImage - .frame(width: 40, height: 40) - - Button("테스트 서버 호출하기") { - viewStore.send(.buttonDidTap) - } - } - } - } -} diff --git a/Projects/Network/Sources/DTO/KeymeTestsDTO.swift b/Projects/Network/Sources/DTO/KeymeTestsDTO.swift index 49c52f48..0aca7b0b 100644 --- a/Projects/Network/Sources/DTO/KeymeTestsDTO.swift +++ b/Projects/Network/Sources/DTO/KeymeTestsDTO.swift @@ -9,32 +9,34 @@ import Foundation public struct KeymeTestsDTO: Codable { - public let data: DataDTO - let message: String let code: Int -} - -public struct DataDTO: Codable { - public let testResultId: Int? - public let owner: PresenterProfileDTO - public let questions: [QuestionDTO] - let solvedCount: Int - public let testId: Int - let title: String -} + let message: String + public let data: TestData + + public struct TestData: Codable { + public let owner: Owner + public let questions: [Question] + public let testId: Int + let testResultId: Int? + public let title: String + } -public struct PresenterProfileDTO: Codable { - let id: Int - public let nickname: String? - let profileThumbnail: String -} + public struct Owner: Codable { + let id: Int + public let nickname: String + let profileThumbnail: String + } -public struct QuestionDTO: Codable { - public let category: CategoryDTO - let title, keyword: String - let questionId: Int -} + public struct Question: Codable { + public let category: Category + let keyword: String + let questionId: Int + let title: String + } -public struct CategoryDTO: Codable { - public let color, iconUrl, name: String + public struct Category: Codable { + public let color: String + public let iconUrl: String + let name: String + } } diff --git a/Projects/Network/Sources/DTO/QuestionResultScoresDTO.swift b/Projects/Network/Sources/DTO/QuestionResultScoresDTO.swift new file mode 100644 index 00000000..64ef5cab --- /dev/null +++ b/Projects/Network/Sources/DTO/QuestionResultScoresDTO.swift @@ -0,0 +1,27 @@ +// +// QuestionResultScoresDTO.swift +// Network +// +// Created by 이영빈 on 2023/08/29. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Foundation + +public struct QuestionResultScoresDTO: Decodable { + let code: Int + public let data: DataResponse + let message: String + + public struct DataResponse: Decodable { + public let hasNext: Bool + public let results: [ResultItem] + public let totalCount: Int + } + + public struct ResultItem: Decodable { + public let createdAt: Date + public let id: Int + public let score: Int + } +} diff --git a/Projects/Network/Sources/DTO/TestDTO.swift b/Projects/Network/Sources/DTO/TestDTO.swift deleted file mode 100644 index 19f99121..00000000 --- a/Projects/Network/Sources/DTO/TestDTO.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TestDTO.swift -// Network -// -// Created by 김영인 on 2023/07/29. -// Copyright © 2023 team.humanwave. All rights reserved. -// - -import Foundation - -public struct TestDTO { - public let hello: String - - public init(hello: String) { - self.hello = hello - } -} diff --git a/Projects/Network/Sources/Network/API/KeymeAPI.swift b/Projects/Network/Sources/Network/API/KeymeAPI.swift index d69fd5a0..9a59e201 100644 --- a/Projects/Network/Sources/Network/API/KeymeAPI.swift +++ b/Projects/Network/Sources/Network/API/KeymeAPI.swift @@ -16,6 +16,7 @@ public enum KeymeAPI { case registration(RegistrationAPI) case member(MemberAPI) case test(KeymeTestsAPI) + case question(QuestionAPI) } extension KeymeAPI: BaseAPI { @@ -33,6 +34,8 @@ extension KeymeAPI: BaseAPI { return api.path case .test(let api): return api.path + case .question(let api): + return api.path } } @@ -50,6 +53,8 @@ extension KeymeAPI: BaseAPI { return api.method case .test(let api): return api.method + case .question(let api): + return api.method } } @@ -67,6 +72,8 @@ extension KeymeAPI: BaseAPI { return api.task case .test(let api): return api.task + case .question(let api): + return api.task } } diff --git a/Projects/Network/Sources/Network/API/QuestionAPI.swift b/Projects/Network/Sources/Network/API/QuestionAPI.swift new file mode 100644 index 00000000..1dde1ac9 --- /dev/null +++ b/Projects/Network/Sources/Network/API/QuestionAPI.swift @@ -0,0 +1,65 @@ +// +// QuestionAPI.swift +// Network +// +// Created by 이영빈 on 2023/08/29. +// Copyright © 2023 team.humanwave. All rights reserved. +// + +import Moya +import Foundation + +public enum QuestionAPI { + case scores(ownerId: Int, questionId: Int, limit: Int) +} + +extension QuestionAPI: BaseAPI { + public var path: String { + switch self { + case .scores(_, let questionId, _): + return "questions/\(questionId)/result/scores" + } + } + + public var method: Moya.Method { + switch self { + case .scores: + return .get + } + } + + public var sampleData: Data { + switch self { + case .scores: + return """ + { + "code": 200, + "data": { + "hasNext": true, + "results": [ + { + "createdAt": "2023-08-29T04:30:18.366Z", + "id": 0, + "score": 0 + } + ], + "totalCount": 0 + }, + "message": "SUCCESS" + } + """.data(using: .utf8)! + } + } + + public var task: Task { + switch self { + case let .scores(ownerId, _, limit): + return .requestParameters( + parameters: [ + "limit": limit, + "ownerId": ownerId + ], + encoding: URLEncoding.queryString) + } + } +} diff --git a/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift b/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift index 9cf8f4c5..8ebfbe38 100644 --- a/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift +++ b/Projects/Network/Sources/Network/Manager/KeymeAPIManager.swift @@ -27,6 +27,7 @@ public class KeymeAPIManager { init(core: CoreNetworkService) { self.core = core + decoder.dateDecodingStrategy = .iso8601 } }