From 1c12965fd55d3c156f6c864a0ea589d6fd77e129 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 10:18:37 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=ED=99=94=EB=A9=B4=EC=9D=84=20=ED=94=BC=EA=B7=B8?= =?UTF-8?q?=EB=A7=88=20=EC=BD=94=EB=93=9C=20=EC=9E=85=EB=A0=A5=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6칸 참여 코드 입력과 중앙 제목을 적용하고 시안에 없는 안내 영역과 클래스 생성 버튼을 제거합니다. --- .../ClassJoin/Reducer/ClassJoinFeature.swift | 5 +- .../ClassJoin/View/ClassJoinView.swift | 105 ++++++++---------- 2 files changed, 46 insertions(+), 64 deletions(-) diff --git a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift index 91e92f76..43fc7c60 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift @@ -69,14 +69,13 @@ public struct ClassJoinFeature { public var navigationCenterTitle: String? { switch mode { - case .code: return nil - case .nickname: return "클래스 참여" + case .code, .nickname: return "클래스 참여" case .ownerName: return "이름 입력" } } public var navigationLeadingTitle: String? { - mode == .code ? "클래스 참여" : nil + nil } public var nicknamePlaceholder: String { diff --git a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift index 9cb78e2a..37c874e0 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift @@ -31,7 +31,7 @@ public struct ClassJoinView: View { .foregroundStyle(.gray500) ScrollView { - VStack(alignment: .leading, spacing: store.mode == .code ? 26 : 24) { + VStack(alignment: .leading, spacing: store.mode == .code ? 20 : 24) { titleSection() switch store.mode { case .code: @@ -41,13 +41,12 @@ public struct ClassJoinView: View { } } .padding(16) - .padding(.top, store.mode == .code ? 6 : 0) .frame(maxWidth: .infinity, alignment: .leading) } .scrollIndicators(.hidden) .scrollBounceBehavior(.basedOnSize) - bottomActions() + bottomAction .padding(.horizontal, 16) .padding(.vertical, 16) } @@ -61,14 +60,14 @@ public struct ClassJoinView: View { private extension ClassJoinView { @ViewBuilder func titleSection() -> some View { - VStack(alignment: .leading, spacing: store.mode == .code ? 14 : 8) { + VStack(alignment: .leading, spacing: store.mode == .code ? 6 : 8) { Text(store.mode == .code ? "공유받은 코드로\n클래스에 참여하세요" : "이름을 입력해주세요") .pretendardFont(.semiBold24) .lineSpacing(store.mode == .code ? 7 : 2.4) .foregroundStyle(.gray800) Text(store.mode == .code - ? "선생님이나 모임장이 전달한\n6자리 코드를 입력해 주세요." + ? "전달받은 6자리 코드를 입력해 주세요." : "이 클래스에서만 보이는 이름이에요.\n입장 후에도 수정할 수 있어요.") .pretendardFont(.bodyMedium) .lineSpacing(4) @@ -78,58 +77,53 @@ private extension ClassJoinView { @ViewBuilder func codeSection() -> some View { - VStack(alignment: .leading, spacing: 29) { - VStack(alignment: .leading, spacing: 12) { - Text("참여 코드") - .pretendardFont(.headingMedium) - .foregroundStyle(.gray800) - - TextField( - "", - text: $store.joinCode, - prompt: Text("6자리 코드를 입력해 주세요") - .font(.pretendardFontFamily(family: .Regular, size: 14)) - .tracking(0) - .foregroundStyle(.gray300) - ) + VStack(alignment: .leading, spacing: 8) { + ZStack { + HStack(spacing: 14) { + ForEach(0..<6, id: \.self) { index in + Text(codeCharacter(at: index)) + .pretendardFont(.headingMedium) + .foregroundStyle(.gray800) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(.bgSubtle) + .overlay { + RoundedRectangle(cornerRadius: 2) + .strokeBorder(codeBorderColor(at: index), lineWidth: 1) + } + } + } + + TextField("", text: $store.joinCode) .textInputAutocapitalization(.characters) .autocorrectionDisabled() .keyboardType(.asciiCapable) .textContentType(.oneTimeCode) .focused($isCodeFocused) .accessibilityLabel("참여 코드, 6자리") - .tracking(4) - .pickeTextField( - height: 52, - font: .headingLarge - ) - .overlay { - if store.errorMessage != nil { - RoundedRectangle(cornerRadius: 2) - .strokeBorder(.borderErrorDefault, lineWidth: 1) - } - } - - if let message = store.errorMessage { - errorLabel(message) - } + .foregroundStyle(.clear) + .tint(.clear) + .frame(maxWidth: .infinity, maxHeight: .infinity) } + .frame(height: 50) - VStack(alignment: .leading, spacing: 8) { - Text("코드를 찾을 수 없나요?") - .pretendardFont(.headingSmall) - .foregroundStyle(.gray800) - - Text("클래스를 만든 선생님이나 모임장에게\n참여 코드를 확인해 주세요.") - .pretendardFont(.bodySmall) - .foregroundStyle(.gray400) + if let message = store.errorMessage { + errorLabel(message) } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(16) - .background(.beige200) } } + func codeCharacter(at index: Int) -> String { + let characters = Array(store.joinCode) + return characters.indices.contains(index) ? String(characters[index]) : "-" + } + + func codeBorderColor(at index: Int) -> Color { + if store.errorMessage != nil { return .borderErrorDefault } + if isCodeFocused && index == min(store.joinCode.count, 5) { return .secondary500 } + return .gray50 + } + @ViewBuilder func nicknameSection() -> some View { VStack(alignment: .leading, spacing: 8) { @@ -173,22 +167,11 @@ private extension ClassJoinView { .fixedSize(horizontal: false, vertical: true) } - @ViewBuilder - func bottomActions() -> some View { - VStack(spacing: 8) { - Button { send(store.mode == .code ? .findTapped : .joinTapped) } label: { - Text(store.primaryButtonTitle) - } - .ctaButtonStyle(.primary, size: .large, height: 52) - .disabled(store.mode == .code ? !store.canFindClass : !store.canJoin) - - if store.mode == .code { - Button { send(.createClassTapped) } label: { - Text("클래스 새로 만들기") - } - .ctaButtonStyle(.primary, size: .large, height: 52) - .disabled(store.isLoading) - } + var bottomAction: some View { + Button { send(store.mode == .code ? .findTapped : .joinTapped) } label: { + Text(store.primaryButtonTitle) } + .ctaButtonStyle(.primary, size: .large, height: 52) + .disabled(store.mode == .code ? !store.canFindClass : !store.canJoin) } } From ed681b6207a8fc27a18f6bcddf143efaeabb2610 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 10:24:26 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=EC=BD=94=EB=93=9C=20=ED=91=9C=EC=8B=9C=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 리뷰 의견에 따라 여섯 자리 표시값을 상태의 파생값으로 옮기고 뷰는 이를 그대로 렌더링합니다. --- .../Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift | 5 +++++ .../Class/Sources/ClassJoin/View/ClassJoinView.swift | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift index 43fc7c60..36da9947 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift @@ -60,6 +60,11 @@ public struct ClassJoinFeature { mode == .code && joinCode.count == 6 && !isLoading } + public var codeCharacters: [String] { + let characters = Array(joinCode.prefix(6)).map(String.init) + return characters + Array(repeating: "-", count: max(0, 6 - characters.count)) + } + public var primaryButtonTitle: String { switch mode { case .code, .nickname: return "클래스 참여하기" diff --git a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift index 37c874e0..6e616f7d 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift @@ -81,7 +81,7 @@ private extension ClassJoinView { ZStack { HStack(spacing: 14) { ForEach(0..<6, id: \.self) { index in - Text(codeCharacter(at: index)) + Text(store.codeCharacters[index]) .pretendardFont(.headingMedium) .foregroundStyle(.gray800) .frame(maxWidth: .infinity) @@ -113,11 +113,6 @@ private extension ClassJoinView { } } - func codeCharacter(at index: Int) -> String { - let characters = Array(store.joinCode) - return characters.indices.contains(index) ? String(characters[index]) : "-" - } - func codeBorderColor(at index: Int) -> Color { if store.errorMessage != nil { return .borderErrorDefault } if isCodeFocused && index == min(store.joinCode.count, 5) { return .secondary500 } From 783697167777290b86f21b2bfb05a747ff9be4f5 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:05:11 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=AA=A8=EB=93=88=20=EA=B7=9C=EC=B9=99=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClassInterface 하나로 계약을 통합하고 Repository와 UseCase 구현을 정리합니다. 클래스 API를 API·APIEndpoint 모듈로 옮기고 Entity와 DTO 변환 파일을 기능별로 배치합니다. --- .../Navigation/Class/ClassCoordinator.swift | 17 +- .../{ => AIChat}/ClassAIChatContext.swift | 0 .../{ => AIChat}/ClassAIChatMessage.swift | 0 .../ClassAIFeedbackContext.swift | 0 .../ClassAIFeedbackEvidence.swift | 0 .../ClassAIFeedbackKind.swift | 0 .../ClassAIFeedbackPoint.swift | 0 .../ClassAIFeedbackResult.swift | 0 .../ClassAIFeedbackVoteChoice.swift | 0 .../{ => AIFeedback}/ClassAIInsight.swift | 0 .../ClassAIPersonalFeedback.swift | 0 .../{ => Battle}/ClassBattleDetail.swift | 0 .../{ => Battle}/ClassBattleOption.swift | 0 .../{ => Battle}/ClassBattleSession.swift | 0 .../Entity/{ => Battle}/ClassBattleStep.swift | 0 .../{ => Battle}/ClassBattleSummary.swift | 0 .../Entity/{ => Battle}/ClassBattleType.swift | 0 .../ClassOwnerDashboardData.swift | 0 .../Entity/{ => Debate}/ClassDebate.swift | 0 .../{ => Debate}/ClassDebateMessage.swift | 0 .../{ => Debate}/ClassDebateSpeaker.swift | 0 .../{ => Debate}/ClassDebateStatus.swift | 0 .../Entity/{ => Error}/ClassAIChatError.swift | 0 .../{ => Error}/ClassAIFeedbackError.swift | 0 .../Entity/{ => Error}/ClassBattleError.swift | 0 .../Entity/{ => Error}/ClassError.swift | 0 .../Entity/{ => Error}/ClassTicketError.swift | 0 .../ClassPerspectiveAuthor.swift | 0 .../ClassPerspectiveItem.swift | 0 .../ClassPerspectiveOption.swift | 0 .../ClassPerspectivePage.swift | 0 .../ClassPerspectiveReceipt.swift | 0 .../ClassParticipationMetrics.swift | 0 .../Entity/{ => Report}/ClassReport.swift | 0 .../{ => Report}/ClassReportLayout.swift | 0 .../{ => Report}/ClassReportOpinion.swift | 0 .../{ => Report}/ClassReportOpinionKind.swift | 0 .../ClassReportReplyContext.swift | 0 .../Entity/{ => Report}/ClassReportTab.swift | 0 .../Entity/{ => Result}/ClassResult.swift | 0 .../{ => Result}/ClassResultOption.swift | 0 .../{ => Result}/ClassResultStats.swift | 0 .../{ => Room}/ClassAudienceLevel.swift | 0 .../Entity/{ => Room}/ClassCategory.swift | 0 .../Entity/{ => Room}/ClassCreation.swift | 0 .../Entity/{ => Room}/ClassJoinPreview.swift | 0 .../Sources/Entity/{ => Room}/ClassRole.swift | 0 .../Sources/Entity/{ => Room}/ClassRoom.swift | 0 .../Entity/{ => Room}/ClassStatus.swift | 0 .../Entity/{ => Ticket}/ClassTicket.swift | 0 .../{ => Ticket}/ClassTicketPreview.swift | 0 .../{ => Topic}/ClassTopicCandidate.swift | 0 .../Entity/{ => Topic}/ClassTopicDraft.swift | 0 .../Entity/{ => Topic}/ClassTopicFilter.swift | 0 .../{ => Topic}/ClassTopicValidation.swift | 0 .../{ => Topic}/ClassTopicVersion.swift | 0 .../Entity/{ => Vote}/ClassVoteChoice.swift | 0 .../Entity/{ => Vote}/ClassVoteHistory.swift | 0 .../Entity/{ => Vote}/ClassVoteReceipt.swift | 0 .../Entity/{ => Vote}/ClassVoteStats.swift | 0 .../{ => Vote}/ClassVoteStatsOption.swift | 0 .../ClassAIRepositoryInterface.swift | 51 --- .../Sources/Repository/ClassInterface.swift | 47 ++ .../Repository/ClassMemberInterface.swift | 46 -- .../Default/MockClassAIChatUseCase.swift | 2 +- .../Default/MockClassAIFeedbackUseCase.swift | 2 +- .../MockClassRepository+Features.swift | 158 +++++++ .../Default/MockClassRepository.swift | 5 +- .../UnavailableClassBattleRepository.swift | 28 -- .../UseCase/ClassAIChatInterface.swift | 37 -- .../UseCase/ClassAIFeedbackInterface.swift | 29 -- .../UseCase/ClassBattleInterface.swift | 49 -- .../UseCase/ClassDebateInterface.swift | 50 -- .../UseCase/ClassResultInterface.swift | 50 -- .../UseCase/ClassTicketInterface.swift | 42 -- .../UseCase/ClassTopicDraftInterface.swift | 62 --- Projects/Domain/ClassDomain/Project.swift | 4 + .../Sources/ClassLiveDependencies.swift | 91 +--- .../Model/DTO/ClassAIFeedbackDTO.swift | 85 ---- .../ClassBattleRecommendationDataDTO.swift | 36 -- .../Model/DTO/ClassBattleSessionDTO.swift | 50 -- .../Sources/Model/DTO/ClassDebateDTO.swift | 31 -- .../Sources/Model/DTO/ClassMemberDTO.swift | 19 - .../Model/DTO/ClassPerspectiveDTO.swift | 44 -- .../Sources/Model/DTO/ClassResultDTO.swift | 34 -- .../Sources/Model/DTO/ClassTicketDTO.swift | 19 - .../Model/DTO/ClassTicketPreviewDTO.swift | 13 - .../Model/DTO/ClassTopicDraftDTO.swift | 62 --- .../Sources/Model/DTO/ClassVoteDTO.swift | 43 -- .../Sources/Model/DTO/ClassroomDataDTO.swift | 99 ---- .../Model/Mapper/ClassAIFeedbackDTO+.swift | 102 +++++ .../ClassBattleRecommendationDataDTO+.swift | 40 ++ .../Model/Mapper/ClassBattleSessionDTO+.swift | 61 +++ .../Model/Mapper/ClassDebateDTO+.swift | 37 ++ .../Model/Mapper/ClassMemberDTO+.swift | 25 + .../Model/Mapper/ClassPerspectiveDTO+.swift | 59 +++ .../Model/Mapper/ClassResultDTO+.swift | 45 ++ .../Model/Mapper/ClassTicketDTO+.swift | 24 + .../Model/Mapper/ClassTicketPreviewDTO+.swift | 18 + .../Model/Mapper/ClassTopicDraftDTO+.swift | 72 +++ .../Sources/Model/Mapper/ClassVoteDTO+.swift | 58 +++ .../Model/Mapper/ClassroomDataDTO+.swift | 108 +++++ .../Repository/ClassAIRepositoryImpl.swift | 50 -- .../ClassBattleRepositoryImpl.swift | 134 ------ .../Repository/ClassLiveRepository.swift | 55 --- .../ClassMemberRepositoryImpl.swift | 55 --- .../Repository/ClassRepositoryImpl.swift | 427 +++++++++++++++++- .../ClassResultRepositoryImpl.swift | 83 ---- .../ClassTicketRepositoryImpl.swift | 55 --- .../ClassTopicDraftRepositoryImpl.swift | 52 --- .../Sources/Service/ClassBattleService.swift | 74 --- .../Sources/Service/ClassDebateService.swift | 57 --- .../Sources/Service/ClassMemberService.swift | 53 --- .../Sources/Service/ClassResultService.swift | 46 -- .../Sources/Service/ClassTicketService.swift | 46 -- .../Service/ClassTopicDraftService.swift | 108 ----- .../Sources/Service/ClassroomService.swift | 98 ---- .../UseCase/ClassAIChatUseCaseImpl.swift | 48 -- .../UseCase/ClassAIFeedbackUseCaseImpl.swift | 21 - .../UseCase/ClassBattleUseCaseImpl.swift | 66 --- .../UseCase/ClassDebateUseCaseImpl.swift | 35 -- .../UseCase/ClassMemberUseCaseImpl.swift | 21 - .../UseCase/ClassResultUseCaseImpl.swift | 24 - .../UseCase/ClassTicketUseCaseImpl.swift | 21 - .../UseCase/ClassTopicDraftUseCaseImpl.swift | 40 -- .../Sources/UseCase/ClassUseCase.swift | 224 +++++++++ .../Sources/UseCase/ClassUseCaseImpl.swift | 64 --- .../Reducer/ClassAIChatFeature.swift | 14 +- .../Reducer/ClassAIVoteFeature.swift | 14 +- .../Reducer/ClassBattleFlowFeature.swift | 8 +- .../ClassJoin/Reducer/ClassJoinFeature.swift | 5 +- .../Reducer/ClassMyOpinionsFeature.swift | 4 +- .../Reducer/ClassOwnerDashboardFeature.swift | 16 +- .../Reducer/ClassRecommendFeature.swift | 25 +- .../Reducer/ClassReportFeature.swift | 10 +- .../Reducer/ClassTicketFeature.swift | 6 +- .../API/Sources/Base/PieckeDomain.swift | 3 + .../API/Sources/Class/ClassBattleAPI.swift | 23 + .../Class/ClassBattleRecommendationAPI.swift | 9 + .../API/Sources/Class/ClassDebateAPI.swift | 13 + .../API/Sources/Class/ClassMemberAPI.swift | 13 + .../API/Sources/Class/ClassResultAPI.swift | 15 + .../API/Sources/Class/ClassTicketAPI.swift | 13 + .../Sources/Class/ClassTopicDraftAPI.swift | 17 + .../API/Sources/Class/ClassroomAPI.swift | 17 + Projects/Service/APIEndpoint/Project.swift | 4 +- .../ClassBattleRecommendationService.swift | 30 +- .../Sources/Class/ClassBattleService.swift | 54 +++ .../Sources/Class/ClassDebateService.swift | 41 ++ .../Sources/Class/ClassMemberService.swift | 40 ++ .../Sources/Class/ClassResultService.swift | 36 ++ .../Sources/Class/ClassTicketService.swift | 37 ++ .../Class/ClassTopicDraftService.swift | 49 ++ .../Sources/Class/ClassroomService.swift | 60 +++ .../ClassBattleRecommendationRequests.swift | 8 + .../Class/Request/ClassBattleRequests.swift | 16 + .../Class/Request/ClassDebateRequests.swift | 7 + .../Class/Request/ClassMemberRequests.swift | 5 + .../Class/Request/ClassTicketRequests.swift | 5 + .../Request/ClassTopicDraftRequests.swift | 56 +++ .../Class/Request/ClassroomRequests.swift | 36 ++ 161 files changed, 2157 insertions(+), 2463 deletions(-) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIChat}/ClassAIChatContext.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIChat}/ClassAIChatMessage.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackContext.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackEvidence.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackKind.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackPoint.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackResult.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIFeedbackVoteChoice.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIInsight.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => AIFeedback}/ClassAIPersonalFeedback.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleDetail.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleOption.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleSession.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleStep.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleSummary.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Battle}/ClassBattleType.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => ClassOwner}/ClassOwnerDashboardData.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Debate}/ClassDebate.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Debate}/ClassDebateMessage.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Debate}/ClassDebateSpeaker.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Debate}/ClassDebateStatus.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Error}/ClassAIChatError.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Error}/ClassAIFeedbackError.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Error}/ClassBattleError.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Error}/ClassError.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Error}/ClassTicketError.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Perspective}/ClassPerspectiveAuthor.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Perspective}/ClassPerspectiveItem.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Perspective}/ClassPerspectiveOption.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Perspective}/ClassPerspectivePage.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Perspective}/ClassPerspectiveReceipt.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassParticipationMetrics.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReport.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReportLayout.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReportOpinion.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReportOpinionKind.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReportReplyContext.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Report}/ClassReportTab.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Result}/ClassResult.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Result}/ClassResultOption.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Result}/ClassResultStats.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassAudienceLevel.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassCategory.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassCreation.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassJoinPreview.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassRole.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassRoom.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Room}/ClassStatus.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Ticket}/ClassTicket.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Ticket}/ClassTicketPreview.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Topic}/ClassTopicCandidate.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Topic}/ClassTopicDraft.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Topic}/ClassTopicFilter.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Topic}/ClassTopicValidation.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Topic}/ClassTopicVersion.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Vote}/ClassVoteChoice.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Vote}/ClassVoteHistory.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Vote}/ClassVoteReceipt.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Vote}/ClassVoteStats.swift (100%) rename Projects/Domain/ClassDomain/Interface/Sources/Entity/{ => Vote}/ClassVoteStatsOption.swift (100%) delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassAIRepositoryInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassMemberInterface.swift rename Projects/Domain/ClassDomain/Interface/Sources/{UseCase => Repository}/Default/MockClassAIChatUseCase.swift (95%) rename Projects/Domain/ClassDomain/Interface/Sources/{UseCase => Repository}/Default/MockClassAIFeedbackUseCase.swift (89%) create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository+Features.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/UnavailableClassBattleRepository.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIChatInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIFeedbackInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassBattleInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassDebateInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassResultInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTicketInterface.swift delete mode 100644 Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTopicDraftInterface.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleRecommendationDataDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleSessionDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassDebateDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassMemberDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassPerspectiveDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassResultDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketPreviewDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTopicDraftDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassVoteDTO+.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassroomDataDTO+.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassAIRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassBattleRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassLiveRepository.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassMemberRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassResultRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassTicketRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassTopicDraftRepositoryImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassBattleService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassDebateService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassMemberService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassResultService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassTicketService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassTopicDraftService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/Service/ClassroomService.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassAIChatUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassAIFeedbackUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassBattleUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassDebateUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassMemberUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassResultUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassTicketUseCaseImpl.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassTopicDraftUseCaseImpl.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift create mode 100644 Projects/Service/API/Sources/Class/ClassBattleAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassDebateAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassMemberAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassResultAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassTicketAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift create mode 100644 Projects/Service/API/Sources/Class/ClassroomAPI.swift rename Projects/{Domain/ClassDomain/Sources/Service => Service/APIEndpoint/Sources/Class}/ClassBattleRecommendationService.swift (56%) create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift create mode 100644 Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift diff --git a/Projects/App/Sources/Navigation/Class/ClassCoordinator.swift b/Projects/App/Sources/Navigation/Class/ClassCoordinator.swift index 4b3dc741..cadf4db5 100644 --- a/Projects/App/Sources/Navigation/Class/ClassCoordinator.swift +++ b/Projects/App/Sources/Navigation/Class/ClassCoordinator.swift @@ -14,7 +14,6 @@ import TCAFlow @FlowCoordinator(screen: "ClassScreen", navigation: true) public struct ClassCoordinator { - @Dependency(\.classMemberUseCase) private var classMemberUseCase @Dependency(\.classUseCase) private var classUseCase @Dependency(\.uuid) private var uuid @Dependency(\.date) private var date @@ -365,9 +364,9 @@ private extension ClassCoordinator { id: routeID, action: .members(.loadingStarted(requestID)) ))) - let fetch: Effect = .run { [classMemberUseCase] send in + let fetch: Effect = .run { [classUseCase] send in do { - let memberList = try await classMemberUseCase.fetchMembers(classID: roomID) + let memberList = try await classUseCase.fetchMembers(classID: roomID) await send(.inner(.membersLoaded( routeID: routeID, roomID: roomID, @@ -835,10 +834,10 @@ private extension ClassCoordinator { guard memberID != currentMemberID else { return .send(.inner(.memberMutationFailed(roomID, "본인은 내보낼 수 없어요."))) } - return .run { [classMemberUseCase, classUseCase] send in + return .run { [classUseCase] send in do { - try await classMemberUseCase.removeMember(classID: roomID, memberID: memberID) - let memberList = try await classMemberUseCase.fetchMembers(classID: roomID) + try await classUseCase.removeMember(classID: roomID, memberID: memberID) + let memberList = try await classUseCase.fetchMembers(classID: roomID) let room = try await classUseCase.fetchClass(id: roomID) await send(.inner(.memberMutationCompleted(room, memberList.members))) } catch { @@ -858,10 +857,10 @@ private extension ClassCoordinator { guard memberID == currentMemberID else { return .send(.inner(.memberMutationFailed(roomID, "본인 이름만 바꿀 수 있어요."))) } - return .run { [classMemberUseCase, classUseCase] send in + return .run { [classUseCase] send in do { - _ = try await classMemberUseCase.updateMyDisplayName(classID: roomID, name: name) - let memberList = try await classMemberUseCase.fetchMembers(classID: roomID) + _ = try await classUseCase.updateMyDisplayName(classID: roomID, name: name) + let memberList = try await classUseCase.fetchMembers(classID: roomID) let room = try await classUseCase.fetchClass(id: roomID) await send(.inner(.memberMutationCompleted(room, memberList.members))) } catch { diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatContext.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIChat/ClassAIChatContext.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatContext.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIChat/ClassAIChatContext.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatMessage.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIChat/ClassAIChatMessage.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatMessage.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIChat/ClassAIChatMessage.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackContext.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackContext.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackContext.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackContext.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackEvidence.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackEvidence.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackEvidence.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackEvidence.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackKind.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackKind.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackKind.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackKind.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackPoint.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackPoint.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackPoint.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackPoint.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackResult.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackResult.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackResult.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackResult.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackVoteChoice.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackVoteChoice.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackVoteChoice.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIFeedbackVoteChoice.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIInsight.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIInsight.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIInsight.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIInsight.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIPersonalFeedback.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIPersonalFeedback.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIPersonalFeedback.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/AIFeedback/ClassAIPersonalFeedback.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleDetail.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleDetail.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleDetail.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleDetail.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleOption.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleOption.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleOption.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleOption.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleSession.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleSession.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleSession.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleSession.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleStep.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleStep.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleStep.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleStep.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleSummary.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleSummary.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleSummary.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleSummary.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleType.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleType.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleType.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Battle/ClassBattleType.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassOwnerDashboardData.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassOwner/ClassOwnerDashboardData.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassOwnerDashboardData.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassOwner/ClassOwnerDashboardData.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebate.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebate.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebate.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebate.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateMessage.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateMessage.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateMessage.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateMessage.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateSpeaker.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateSpeaker.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateSpeaker.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateSpeaker.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateStatus.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateStatus.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassDebateStatus.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Debate/ClassDebateStatus.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatError.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassAIChatError.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIChatError.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassAIChatError.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackError.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassAIFeedbackError.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAIFeedbackError.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassAIFeedbackError.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleError.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassBattleError.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassBattleError.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassBattleError.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassError.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassError.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassError.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassError.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicketError.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassTicketError.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicketError.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Error/ClassTicketError.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveAuthor.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveAuthor.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveAuthor.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveAuthor.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveItem.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveItem.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveItem.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveItem.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveOption.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveOption.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveOption.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveOption.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectivePage.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectivePage.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectivePage.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectivePage.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveReceipt.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveReceipt.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassPerspectiveReceipt.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Perspective/ClassPerspectiveReceipt.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassParticipationMetrics.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassParticipationMetrics.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassParticipationMetrics.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassParticipationMetrics.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReport.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReport.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReport.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReport.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportLayout.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportLayout.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportLayout.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportLayout.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportOpinion.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportOpinion.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportOpinion.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportOpinion.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportOpinionKind.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportOpinionKind.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportOpinionKind.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportOpinionKind.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportReplyContext.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportReplyContext.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportReplyContext.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportReplyContext.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportTab.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportTab.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassReportTab.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Report/ClassReportTab.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResult.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResult.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResult.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResult.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResultOption.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResultOption.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResultOption.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResultOption.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResultStats.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResultStats.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassResultStats.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Result/ClassResultStats.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAudienceLevel.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassAudienceLevel.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassAudienceLevel.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassAudienceLevel.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassCategory.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCategory.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassCategory.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCategory.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassCreation.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassCreation.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassJoinPreview.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassJoinPreview.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassJoinPreview.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassJoinPreview.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassRole.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassRole.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassRole.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassRole.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassRoom.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassRoom.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassRoom.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassRoom.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassStatus.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassStatus.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassStatus.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassStatus.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicket.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Ticket/ClassTicket.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicket.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Ticket/ClassTicket.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicketPreview.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Ticket/ClassTicketPreview.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTicketPreview.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Ticket/ClassTicketPreview.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicCandidate.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicCandidate.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicCandidate.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicCandidate.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicDraft.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicDraft.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicDraft.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicDraft.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicFilter.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicFilter.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicFilter.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicFilter.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicValidation.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicValidation.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicValidation.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicValidation.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicVersion.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicVersion.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassTopicVersion.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Topic/ClassTopicVersion.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteChoice.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteChoice.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteChoice.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteChoice.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteHistory.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteHistory.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteHistory.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteHistory.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteReceipt.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteReceipt.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteReceipt.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteReceipt.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteStats.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteStats.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteStats.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteStats.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteStatsOption.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteStatsOption.swift similarity index 100% rename from Projects/Domain/ClassDomain/Interface/Sources/Entity/ClassVoteStatsOption.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Entity/Vote/ClassVoteStatsOption.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassAIRepositoryInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassAIRepositoryInterface.swift deleted file mode 100644 index 8c6d07d2..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassAIRepositoryInterface.swift +++ /dev/null @@ -1,51 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassAIRepositoryInterface: Sendable { - func fetchDebate(classID: Int) async throws -> ClassDebate - func startDebate(classID: Int) async throws -> ClassDebate - func submitDebateReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate - func retryDebate(classID: Int) async throws -> ClassDebate -} - -public enum ClassAIRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassAIRepositoryInterface { - UnimplementedClassAIRepository() - } -} - -public extension DependencyValues { - var classAIRepository: any ClassAIRepositoryInterface { - get { self[ClassAIRepositoryDependency.self] } - set { self[ClassAIRepositoryDependency.self] = newValue } - } -} - -private struct UnimplementedClassAIRepository: ClassAIRepositoryInterface { - func fetchDebate(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.noSession - } - - func startDebate(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.noSession - } - - func submitDebateReply( - classID _: Int, - round _: Int, - content _: String, - requestKey _: UUID - ) async throws -> ClassDebate { - throw ClassAIChatError.noSession - } - - func retryDebate(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.noSession - } - -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift index ef97d9b3..9ecc201e 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift @@ -21,6 +21,53 @@ public protocol ClassInterface: Sendable { func leaveClass(id: Int) async throws func fetchReport(roomID: Int) async throws -> ClassReport? func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? + func fetchBattle(classID: Int) async throws -> ClassBattleSession + func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt + func completeTTS(classID: Int) async throws + func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt + func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory + func fetchVoteStats(classID: Int) async throws -> ClassVoteStats + func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt + func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage + func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] + func fetchMembers(classID: Int) async throws -> ClassMemberList + func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember + func removeMember(classID: Int, memberID: Int) async throws + func fetchDebate(classID: Int) async throws -> ClassDebate + func startDebate(classID: Int) async throws -> ClassDebate + func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate + func retryDebate(classID: Int) async throws -> ClassDebate + func previewTicket(code: String) async throws -> ClassTicketPreview + func registerTicket(code: String) async throws -> ClassTicket + func fetchMyTicket() async throws -> ClassTicket + func fetchResult(roomID: Int) async throws -> ClassResult + func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback + func fetchInsight(roomID: Int) async throws -> ClassAIInsight + func retryInsight(roomID: Int) async throws + func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft + func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft + func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft + func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion + func validateTopicDraft( + draftID: Int, + baseVersionID: Int, + candidate: ClassTopicCandidate + ) async throws -> ClassTopicValidation + func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws + func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String + func endAIChat(_ id: UUID) async + func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult } public extension ClassInterface { diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassMemberInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassMemberInterface.swift deleted file mode 100644 index 0c6c00cd..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassMemberInterface.swift +++ /dev/null @@ -1,46 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassMemberInterface: Sendable { - func fetchMembers(classID: Int) async throws -> ClassMemberList - func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember - func removeMember(classID: Int, memberID: Int) async throws -} - -public enum ClassMemberRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassMemberInterface { - UnimplementedClassMemberInterface() - } -} - -public enum ClassMemberUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassMemberInterface { - UnimplementedClassMemberInterface() - } -} - -public extension DependencyValues { - var classMemberRepository: any ClassMemberInterface { - get { self[ClassMemberRepositoryDependency.self] } - set { self[ClassMemberRepositoryDependency.self] = newValue } - } - - var classMemberUseCase: any ClassMemberInterface { - get { self[ClassMemberUseCaseDependency.self] } - set { self[ClassMemberUseCaseDependency.self] = newValue } - } -} - -private struct UnimplementedClassMemberInterface: ClassMemberInterface { - func fetchMembers(classID _: Int) async throws -> ClassMemberList { - throw ClassError.unknown("멤버 데이터를 불러올 수 없어요.") - } - - func updateMyDisplayName(classID _: Int, name _: String) async throws -> ClassMember { - throw ClassError.unknown("이름 변경을 사용할 수 없어요.") - } - - func removeMember(classID _: Int, memberID _: Int) async throws { - throw ClassError.unknown("멤버 변경을 사용할 수 없어요.") - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIChatUseCase.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIChatUseCase.swift similarity index 95% rename from Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIChatUseCase.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIChatUseCase.swift index b2df93c1..599947e1 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIChatUseCase.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIChatUseCase.swift @@ -7,7 +7,7 @@ import Foundation -public struct MockClassAIChatUseCase: ClassAIChatInterface { +public struct MockClassAIChatUseCase { public var startHandler: @Sendable (UUID, ClassAIChatContext) async throws -> Void public var respondHandler: @Sendable (UUID, String, Bool) async throws -> String public var endHandler: @Sendable (UUID) async -> Void diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIFeedbackUseCase.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIFeedbackUseCase.swift similarity index 89% rename from Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIFeedbackUseCase.swift rename to Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIFeedbackUseCase.swift index 9b34f723..bdfca59b 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/Default/MockClassAIFeedbackUseCase.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassAIFeedbackUseCase.swift @@ -5,7 +5,7 @@ // Created by Picke on 10/12/26. // -public struct MockClassAIFeedbackUseCase: ClassAIFeedbackInterface { +public struct MockClassAIFeedbackUseCase { public var generateHandler: @Sendable (ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult public init( diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository+Features.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository+Features.swift new file mode 100644 index 00000000..ee0404ce --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository+Features.swift @@ -0,0 +1,158 @@ +import Foundation + +public extension MockClassRepository { + public func fetchBattle(classID: Int) async throws -> ClassBattleSession { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchBattle") + } + + public func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: submitPreVote") + } + + public func completeTTS(classID: Int) async throws { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: completeTTS") + } + + public func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: submitPostVote") + } + + public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchMyVotes") + } + + public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchVoteStats") + } + + public func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: publishPerspective") + } + + public func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchPerspectives") + } + + public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchMyPerspectives") + } + + public func fetchMembers(classID: Int) async throws -> ClassMemberList { + let members = try await fetchMembers(roomID: classID) + let currentID = try await currentMemberID(roomID: classID) + return ClassMemberList(members: members, currentMemberID: currentID) + } + + public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { + let currentID = try await currentMemberID(roomID: classID) + return try await updateDisplayName( + roomID: classID, + memberID: currentID, + currentMemberID: currentID, + name: name + ) + } + + public func removeMember(classID: Int, memberID: Int) async throws { + let currentID = try await currentMemberID(roomID: classID) + _ = try await removeMember(roomID: classID, memberID: memberID, currentMemberID: currentID) + } + + public func fetchDebate(classID: Int) async throws -> ClassDebate { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchDebate") + } + + public func startDebate(classID: Int) async throws -> ClassDebate { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: startDebate") + } + + public func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: submitDebateReply") + } + + public func retryDebate(classID: Int) async throws -> ClassDebate { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: retryDebate") + } + + public func previewTicket(code: String) async throws -> ClassTicketPreview { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: previewTicket") + } + + public func registerTicket(code: String) async throws -> ClassTicket { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: registerTicket") + } + + public func fetchMyTicket() async throws -> ClassTicket { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchMyTicket") + } + + public func fetchResult(roomID: Int) async throws -> ClassResult { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchResult") + } + + public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchPersonalResult") + } + + public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchInsight") + } + + public func retryInsight(roomID: Int) async throws { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: retryInsight") + } + + public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: createTopicDraft") + } + + public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: fetchTopicDraft") + } + + public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: suggestTopicDraft") + } + + public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: selectTopicDraft") + } + + public func validateTopicDraft( + draftID: Int, + baseVersionID: Int, + candidate: ClassTopicCandidate + ) async throws -> ClassTopicValidation { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: validateTopicDraft") + } + + public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: startAIChat") + } + + public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: respondAIChat") + } + + public func endAIChat(_ id: UUID) async { + return + } + + public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { + if let feedbackHandler { + return try await feedbackHandler(context) + } + throw ClassError.unknown("모의 클래스 기능이 설정되지 않았습니다: generateAIFeedback") + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift index c7e529d8..8739f0cc 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift @@ -42,13 +42,15 @@ public actor MockClassRepository: ClassInterface { private var reports: [Int: ClassReport] private var dashboards: [Int: ClassOwnerDashboardData] private var nextID: Int + let feedbackHandler: (@Sendable (ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult)? public init( rooms: [ClassRoom] = ClassRoom.mocks + [.mockClosedSociety], availableRooms: [ClassRoom] = [ClassRoom.mockJoinable], reports: [Int: ClassReport]? = nil, dashboards: [Int: ClassOwnerDashboardData]? = nil, - members: [Int: [ClassMember]]? = nil + members: [Int: [ClassMember]]? = nil, + generateAIFeedback: (@Sendable (ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult)? = nil ) { let initialRooms = rooms + availableRooms let initialIDs = Set(initialRooms.map(\.id)) @@ -93,6 +95,7 @@ public actor MockClassRepository: ClassInterface { (room.id, (memberLists[room.id]?.map(\.id).max() ?? 0) + 1) }) nextID = (rooms + availableRooms).map(\.id).max().map { $0 + 1 } ?? 1 + feedbackHandler = generateAIFeedback } public func fetchMembers(roomID: Int) async throws -> [ClassMember] { diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/UnavailableClassBattleRepository.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/UnavailableClassBattleRepository.swift deleted file mode 100644 index 01d9779a..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/UnavailableClassBattleRepository.swift +++ /dev/null @@ -1,28 +0,0 @@ -public struct UnavailableClassBattleRepository: ClassBattleInterface { - public init() {} - - public func fetchBattle(classID _: Int) async throws -> ClassBattleSession { throw ClassBattleError.invalidResponse } - public func submitPreVote( - classID _: Int, - optionID _: Int - ) async throws -> ClassVoteReceipt { throw ClassBattleError.invalidResponse } - public func completeTTS(classID _: Int) async throws { throw ClassBattleError.invalidResponse } - public func submitPostVote( - classID _: Int, - optionID _: Int - ) async throws -> ClassVoteReceipt { throw ClassBattleError.invalidResponse } - public func fetchMyVotes(classID _: Int) async throws -> ClassVoteHistory { throw ClassBattleError.invalidResponse } - public func fetchVoteStats(classID _: Int) async throws -> ClassVoteStats { throw ClassBattleError.invalidResponse } - public func publishPerspective( - classID _: Int, - content _: String - ) async throws -> ClassPerspectiveReceipt { throw ClassBattleError.invalidResponse } - public func fetchPerspectives( - classID _: Int, - cursor _: String?, - size _: Int?, - optionID _: Int?, - sort _: String? - ) async throws -> ClassPerspectivePage { throw ClassBattleError.invalidResponse } - public func fetchMyPerspectives(classID _: Int) async throws -> [ClassPerspectiveItem] { throw ClassBattleError.invalidResponse } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIChatInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIChatInterface.swift deleted file mode 100644 index eea02991..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIChatInterface.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ClassAIChatInterface.swift -// ClassDomainInterface -// -// Created by Picke on 10/11/26. -// - -import ComposableArchitecture -import Foundation - -public protocol ClassAIChatInterface: Sendable { - func start(_ id: UUID, _ context: ClassAIChatContext) async throws - /// `isFinalRound` 가 참이면 질문 없이 논점을 정리하며 토론을 마무리하는 답변을 만든다. - func respond( - _ id: UUID, - _ text: String, - isFinalRound: Bool - ) async throws -> String - func end(_ id: UUID) async -} - -public enum ClassAIChatUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassAIChatInterface { - MockClassAIChatUseCase() - } - - public static var previewValue: any ClassAIChatInterface { - MockClassAIChatUseCase() - } -} - -public extension DependencyValues { - var classAIChatUseCase: any ClassAIChatInterface { - get { self[ClassAIChatUseCaseDependency.self] } - set { self[ClassAIChatUseCaseDependency.self] = newValue } - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIFeedbackInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIFeedbackInterface.swift deleted file mode 100644 index 46153ad6..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassAIFeedbackInterface.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ClassAIFeedbackInterface.swift -// ClassDomainInterface -// -// Created by Picke on 10/12/26. -// - -import ComposableArchitecture - -public protocol ClassAIFeedbackInterface: Sendable { - func generate(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult -} - -public enum ClassAIFeedbackUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassAIFeedbackInterface { - MockClassAIFeedbackUseCase() - } - - public static var previewValue: any ClassAIFeedbackInterface { - MockClassAIFeedbackUseCase() - } -} - -public extension DependencyValues { - var classAIFeedbackUseCase: any ClassAIFeedbackInterface { - get { self[ClassAIFeedbackUseCaseDependency.self] } - set { self[ClassAIFeedbackUseCaseDependency.self] = newValue } - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassBattleInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassBattleInterface.swift deleted file mode 100644 index c659b3d1..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassBattleInterface.swift +++ /dev/null @@ -1,49 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassBattleInterface: Sendable { - func fetchBattle(classID: Int) async throws -> ClassBattleSession - func submitPreVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt - func completeTTS(classID: Int) async throws - func submitPostVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt - func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory - func fetchVoteStats(classID: Int) async throws -> ClassVoteStats - func publishPerspective( - classID: Int, - content: String - ) async throws -> ClassPerspectiveReceipt - func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage - func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] -} - -public enum ClassBattleRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassBattleInterface { UnavailableClassBattleRepository() } -} - -public enum ClassBattleUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassBattleInterface { UnavailableClassBattleRepository() } -} - -public extension DependencyValues { - var classBattleRepository: any ClassBattleInterface { - get { self[ClassBattleRepositoryDependency.self] } - set { self[ClassBattleRepositoryDependency.self] = newValue } - } - - var classBattleUseCase: any ClassBattleInterface { - get { self[ClassBattleUseCaseDependency.self] } - set { self[ClassBattleUseCaseDependency.self] = newValue } - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassDebateInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassDebateInterface.swift deleted file mode 100644 index dcb052c0..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassDebateInterface.swift +++ /dev/null @@ -1,50 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassDebateInterface: Sendable { - func fetch(classID: Int) async throws -> ClassDebate - func start(classID: Int) async throws -> ClassDebate - func submitReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate - func retry(classID: Int) async throws -> ClassDebate -} - -public enum ClassDebateUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassDebateInterface { - UnimplementedClassDebateUseCase() - } -} - -public extension DependencyValues { - var classDebateUseCase: any ClassDebateInterface { - get { self[ClassDebateUseCaseDependency.self] } - set { self[ClassDebateUseCaseDependency.self] = newValue } - } -} - -private struct UnimplementedClassDebateUseCase: ClassDebateInterface { - func fetch(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.unavailable - } - - func start(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.unavailable - } - - func submitReply( - classID _: Int, - round _: Int, - content _: String, - requestKey _: UUID - ) async throws -> ClassDebate { - throw ClassAIChatError.unavailable - } - - func retry(classID _: Int) async throws -> ClassDebate { - throw ClassAIChatError.unavailable - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassResultInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassResultInterface.swift deleted file mode 100644 index c678f963..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassResultInterface.swift +++ /dev/null @@ -1,50 +0,0 @@ -import ComposableArchitecture - -public protocol ClassResultInterface: Sendable { - func fetch(roomID: Int) async throws -> ClassResult - func fetchPersonal(roomID: Int) async throws -> ClassAIPersonalFeedback - func fetchInsight(roomID: Int) async throws -> ClassAIInsight - func retryInsight(roomID: Int) async throws -} - -public enum ClassResultUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassResultInterface { - UnimplementedClassResultUseCase() - } -} - -public enum ClassResultRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassResultInterface { - UnimplementedClassResultUseCase() - } -} - -public extension DependencyValues { - var classResultRepository: any ClassResultInterface { - get { self[ClassResultRepositoryDependency.self] } - set { self[ClassResultRepositoryDependency.self] = newValue } - } - - var classResultUseCase: any ClassResultInterface { - get { self[ClassResultUseCaseDependency.self] } - set { self[ClassResultUseCaseDependency.self] = newValue } - } -} - -private struct UnimplementedClassResultUseCase: ClassResultInterface { - func fetch(roomID _: Int) async throws -> ClassResult { - throw ClassError.network("클래스 결과 API가 연결되지 않았습니다.") - } - - func fetchPersonal(roomID _: Int) async throws -> ClassAIPersonalFeedback { - throw ClassAIFeedbackError.temporarilyUnavailable - } - - func fetchInsight(roomID _: Int) async throws -> ClassAIInsight { - throw ClassAIFeedbackError.temporarilyUnavailable - } - - func retryInsight(roomID _: Int) async throws { - throw ClassAIFeedbackError.temporarilyUnavailable - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTicketInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTicketInterface.swift deleted file mode 100644 index 76ceb5aa..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTicketInterface.swift +++ /dev/null @@ -1,42 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassTicketInterface: Sendable { - func preview(code: String) async throws -> ClassTicketPreview - func register(code: String) async throws -> ClassTicket - func fetchMyTicket() async throws -> ClassTicket -} - -public enum ClassTicketRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassTicketInterface { UnimplementedClassTicketInterface() } -} - -public enum ClassTicketUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassTicketInterface { UnimplementedClassTicketInterface() } -} - -private struct UnimplementedClassTicketInterface: ClassTicketInterface { - func preview(code _: String) async throws -> ClassTicketPreview { - throw ClassTicketError.network("이용권 코드 확인을 사용할 수 없어요.") - } - - func register(code _: String) async throws -> ClassTicket { - throw ClassTicketError.network("이용권 등록을 사용할 수 없어요.") - } - - func fetchMyTicket() async throws -> ClassTicket { - throw ClassTicketError.network("내 이용권 조회를 사용할 수 없어요.") - } -} - -public extension DependencyValues { - var classTicketRepository: any ClassTicketInterface { - get { self[ClassTicketRepositoryDependency.self] } - set { self[ClassTicketRepositoryDependency.self] = newValue } - } - - var classTicketUseCase: any ClassTicketInterface { - get { self[ClassTicketUseCaseDependency.self] } - set { self[ClassTicketUseCaseDependency.self] = newValue } - } -} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTopicDraftInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTopicDraftInterface.swift deleted file mode 100644 index 24a3edc4..00000000 --- a/Projects/Domain/ClassDomain/Interface/Sources/UseCase/ClassTopicDraftInterface.swift +++ /dev/null @@ -1,62 +0,0 @@ -import ComposableArchitecture -import Foundation - -public protocol ClassTopicDraftInterface: Sendable { - func create(filter: ClassTopicFilter) async throws -> ClassTopicDraft - func fetch(id: Int) async throws -> ClassTopicDraft - func suggest( - id: Int, - filter: ClassTopicFilter? - ) async throws -> ClassTopicDraft - func select( - draftID: Int, - batchNo: Int, - candidateID: Int - ) async throws -> ClassTopicVersion - func validate( - draftID: Int, - baseVersionID: Int, - candidate: ClassTopicCandidate - ) async throws -> ClassTopicValidation -} - -public enum ClassTopicDraftRepositoryDependency: TestDependencyKey { - public static var testValue: any ClassTopicDraftInterface { UnimplementedClassTopicDraft() } -} - -public enum ClassTopicDraftUseCaseDependency: TestDependencyKey { - public static var testValue: any ClassTopicDraftInterface { UnimplementedClassTopicDraft() } -} - -public extension DependencyValues { - var classTopicDraftRepository: any ClassTopicDraftInterface { - get { self[ClassTopicDraftRepositoryDependency.self] } - set { self[ClassTopicDraftRepositoryDependency.self] = newValue } - } - - var classTopicDraftUseCase: any ClassTopicDraftInterface { - get { self[ClassTopicDraftUseCaseDependency.self] } - set { self[ClassTopicDraftUseCaseDependency.self] = newValue } - } -} - -private struct UnimplementedClassTopicDraft: ClassTopicDraftInterface { - func create(filter: ClassTopicFilter) async throws -> ClassTopicDraft { throw ClassError.unknown("주제 초안 미설정") } - func fetch(id: Int) async throws -> ClassTopicDraft { throw ClassError.unknown("주제 초안 미설정") } - func suggest( - id: Int, - filter: ClassTopicFilter? - ) async throws -> ClassTopicDraft { throw ClassError.unknown("주제 초안 미설정") } - - func select( - draftID: Int, - batchNo: Int, - candidateID: Int - ) async throws -> ClassTopicVersion { throw ClassError.unknown("주제 초안 미설정") } - - func validate( - draftID: Int, - baseVersionID: Int, - candidate: ClassTopicCandidate - ) async throws -> ClassTopicValidation { throw ClassError.unknown("주제 초안 미설정") } -} diff --git a/Projects/Domain/ClassDomain/Project.swift b/Projects/Domain/ClassDomain/Project.swift index d39905aa..a2fd2068 100644 --- a/Projects/Domain/ClassDomain/Project.swift +++ b/Projects/Domain/ClassDomain/Project.swift @@ -18,6 +18,10 @@ let project = Project.makeModule( settings: .settings(), dependencies: [ .serviceAssembly, + .service(.apiEndpoint), + ], + testDependencies: [ + .service(.apiEndpoint), ], hasTests: true, hasInterface: true, diff --git a/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift b/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift index 98f712b2..78608474 100644 --- a/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift +++ b/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift @@ -1,97 +1,10 @@ -// -// ClassLiveDependencies.swift -// ClassDomain -// -// Created by Picke on 10/11/26. -// - import ClassDomainInterface import ComposableArchitecture extension ClassUseCaseDependency: DependencyKey { - public static var liveValue: any ClassInterface { - ClassUseCaseImpl() - } -} - -extension ClassTicketUseCaseDependency: DependencyKey { - public static var liveValue: any ClassTicketInterface { - ClassTicketUseCaseImpl() - } -} - -extension ClassTicketRepositoryDependency: DependencyKey { - public static var liveValue: any ClassTicketInterface { - ClassTicketRepositoryImpl() - } -} - -extension ClassBattleUseCaseDependency: DependencyKey { - public static var liveValue: any ClassBattleInterface { - ClassBattleUseCaseImpl() - } -} - -extension ClassBattleRepositoryDependency: DependencyKey { - public static var liveValue: any ClassBattleInterface { - ClassBattleRepositoryImpl() - } -} - -extension ClassMemberUseCaseDependency: DependencyKey { - public static var liveValue: any ClassMemberInterface { - ClassMemberUseCaseImpl() - } -} - -extension ClassMemberRepositoryDependency: DependencyKey { - public static var liveValue: any ClassMemberInterface { - ClassMemberRepositoryImpl() - } -} - -extension ClassAIChatUseCaseDependency: DependencyKey { - public static var liveValue: any ClassAIChatInterface { - ClassAIChatUseCaseImpl() - } -} - -extension ClassDebateUseCaseDependency: DependencyKey { - public static var liveValue: any ClassDebateInterface { - ClassDebateUseCaseImpl() - } -} - -extension ClassAIRepositoryDependency: DependencyKey { - public static var liveValue: any ClassAIRepositoryInterface { - ClassAIRepositoryImpl() - } -} - -extension ClassAIFeedbackUseCaseDependency: DependencyKey { - public static var liveValue: any ClassAIFeedbackInterface { - ClassAIFeedbackUseCaseImpl() - } -} - -extension ClassResultUseCaseDependency: DependencyKey { - public static var liveValue: any ClassResultInterface { ClassResultUseCaseImpl() } -} - -extension ClassResultRepositoryDependency: DependencyKey { - public static var liveValue: any ClassResultInterface { ClassResultRepositoryImpl() } -} - -extension ClassTopicDraftUseCaseDependency: DependencyKey { - public static var liveValue: any ClassTopicDraftInterface { ClassTopicDraftUseCaseImpl() } -} - -extension ClassTopicDraftRepositoryDependency: DependencyKey { - public static var liveValue: any ClassTopicDraftInterface { ClassTopicDraftRepositoryImpl() } + public static var liveValue: any ClassInterface { ClassUseCaseImpl() } } extension ClassRepositoryDependency: DependencyKey { - public static var liveValue: any ClassInterface { - ClassLiveRepository(api: ClassRepositoryImpl()) - } + public static var liveValue: any ClassInterface { ClassRepositoryImpl() } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassAIFeedbackDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassAIFeedbackDTO.swift index 85268bff..08c157a4 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassAIFeedbackDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassAIFeedbackDTO.swift @@ -6,9 +6,6 @@ struct ClassPersonalReportDTO: Decodable, Sendable { let title: String let stance: String? - func toDomain() -> ClassAIFeedbackVoteChoice { - ClassAIFeedbackVoteChoice(id: optionId, title: title, stance: stance) - } } struct Point: Decodable, Sendable { @@ -24,15 +21,6 @@ struct ClassPersonalReportDTO: Decodable, Sendable { let quote: String? let content: String - func toDomain() -> ClassAIFeedbackEvidence { - ClassAIFeedbackEvidence( - sourceType: sourceType, - sourceID: sourceId, - content: content, - quote: quote, - field: field - ) - } } let headline: String @@ -44,30 +32,6 @@ struct ClassPersonalReportDTO: Decodable, Sendable { let coverage: String? let excludedCount: Int? - func toDomain( - preOption: VoteOption?, - postOption: VoteOption?, - perspectiveCount: Int, - commentCount: Int - ) -> ClassAIPersonalFeedback { - let points = [strength, nextFocus].compactMap(\.self).map { - ClassAIFeedbackPoint(title: $0.title, detail: $0.body) - } - return ClassAIPersonalFeedback( - summaryTitle: headline, - summaryDetail: judgmentBasis, - points: points, - nextQuestion: nextQuestion, - nextHint: "", - evidence: evidence?.map { $0.toDomain() } ?? [], - coverage: coverage, - excludedCount: excludedCount ?? 0, - preOption: preOption?.toDomain(), - postOption: postOption?.toDomain(), - perspectiveCount: perspectiveCount, - commentCount: commentCount - ) - } } let state: String @@ -76,27 +40,6 @@ struct ClassPersonalReportDTO: Decodable, Sendable { let postOption: VoteOption? let perspectiveCount: Int? let commentCount: Int? - - func toDomain() throws -> ClassAIPersonalFeedback { - switch state { - case "SUCCEEDED", "LIMITED": - guard let feedback else { throw ClassAIFeedbackError.generationFailed } - return feedback.toDomain( - preOption: preOption, - postOption: postOption, - perspectiveCount: perspectiveCount ?? 0, - commentCount: commentCount ?? 0 - ) - case "BEFORE_CLOSE", "PENDING", "GENERATING": - throw ClassAIFeedbackError.modelNotReady - case "NOT_ELIGIBLE": - throw ClassAIFeedbackError.insufficientInput - case "FAILED": - throw ClassAIFeedbackError.generationFailed - default: - throw ClassAIFeedbackError.generationFailed - } - } } struct ClassInsightDTO: Decodable, Sendable { @@ -107,14 +50,6 @@ struct ClassInsightDTO: Decodable, Sendable { let content: String let quote: String? - func toDomain() -> ClassAIFeedbackEvidence { - ClassAIFeedbackEvidence( - sourceType: sourceType, - sourceID: id ?? sourceId, - content: content, - quote: quote - ) - } } let state: String @@ -124,26 +59,6 @@ struct ClassInsightDTO: Decodable, Sendable { let highlights: [Evidence]? let coverage: String? let excludedCount: Int? - - func toDomain() throws -> ClassAIInsight { - switch state { - case "SUCCEEDED", "LIMITED": - guard let insightText, !insightText.isEmpty else { throw ClassAIFeedbackError.generationFailed } - return ClassAIInsight( - text: insightText, - evidence: evidence?.map { $0.toDomain() } ?? [], - highlights: highlights?.map { $0.toDomain() } ?? [], - coverage: coverage, - excludedCount: excludedCount ?? 0 - ) - case "BEFORE_CLOSE", "PENDING", "GENERATING": - throw ClassAIFeedbackError.modelNotReady - case "FAILED": - throw retryable ? ClassAIFeedbackError.serverRetryAvailable : .generationFailed - default: - throw ClassAIFeedbackError.generationFailed - } - } } struct ClassInsightRetryDTO: Decodable, Sendable { diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleRecommendationDataDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleRecommendationDataDTO.swift index 503fad8f..ee5731b4 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleRecommendationDataDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleRecommendationDataDTO.swift @@ -14,42 +14,6 @@ struct ClassBattleRecommendationItemDTO: Decodable, Sendable { let difficulty: String let audioDuration: Int? let options: [ClassBattleRecommendationOptionDTO] - - func toDomain() throws -> ClassBattleSummary { - guard let category = ClassCategory.allCases.first(where: { $0.title == self.category }) else { - throw ClassError.unknown("알 수 없는 배틀 카테고리: \(category)") - } - let level: ClassAudienceLevel - switch difficulty { - case "BEGINNER": level = .middle - case "INTERMEDIATE": level = .high - case "ADVANCED": level = .adult - default: throw ClassError.unknown("알 수 없는 배틀 수준: \(difficulty)") - } - guard let optionA = options.first(where: { $0.position == "A" }), - let optionB = options.first(where: { $0.position == "B" }) - else { - throw ClassError.unknown("배틀의 A/B 선택지가 누락됐습니다.") - } - return ClassBattleSummary( - id: battleId, - title: title, - summary: summary ?? "", - thumbnailURL: thumbnailUrl.flatMap(URL.init(string:)), - philosopherA: optionA.representative ?? "", - optionATitle: optionA.title, - optionADescription: optionA.stance, - philosopherAImageURL: optionA.imageUrl.flatMap(URL.init(string:)), - philosopherB: optionB.representative ?? "", - optionBTitle: optionB.title, - optionBDescription: optionB.stance, - philosopherBImageURL: optionB.imageUrl.flatMap(URL.init(string:)), - category: category, - level: level, - audioDuration: audioDuration ?? 0, - viewCount: 0 - ) - } } struct ClassBattleRecommendationOptionDTO: Decodable, Sendable { diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleSessionDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleSessionDTO.swift index dc845db8..c6ed6220 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleSessionDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassBattleSessionDTO.swift @@ -13,16 +13,6 @@ struct ClassBattleSessionDTO: Decodable, Sendable { let representative: String? let imageUrl: String? - func toDomain() -> ClassBattleOption { - ClassBattleOption( - id: optionId, - label: label, - title: title, - stance: stance ?? "", - representative: representative ?? "", - imageURL: imageUrl.flatMap(URL.init(string:)) - ) - } } let battleId: Int @@ -40,24 +30,6 @@ struct ClassBattleSessionDTO: Decodable, Sendable { let userVoteStatus: String? let currentStep: String? - func toDomain() throws -> ClassBattleDetail { - guard let currentStep = ClassBattleStep(rawValue: currentStep ?? "NONE") else { - throw ClassBattleError.invalidResponse - } - return ClassBattleDetail( - id: battleInfo.battleId, - title: battleInfo.title, - summary: battleInfo.summary ?? "", - thumbnailURL: battleInfo.thumbnailUrl.flatMap(URL.init(string:)), - description: description ?? "", - shareURL: shareUrl.flatMap(URL.init(string:)), - userVoteStatus: userVoteStatus, - currentStep: currentStep, - participantsCount: battleInfo.participantsCount ?? 0, - audioDuration: battleInfo.audioDuration ?? 0, - options: battleInfo.options.map { $0.toDomain() } - ) - } } let classId: Int @@ -68,26 +40,4 @@ struct ClassBattleSessionDTO: Decodable, Sendable { let canParticipate: Bool let participationCompleted: Bool let battle: Battle - - func toDomain() throws -> ClassBattleSession { - let role: ClassRole - switch myRole { - case "OWNER": role = .owner - case "MEMBER": role = .member - default: throw ClassBattleError.invalidResponse - } - guard let closeAt = ServerDateParser.parse(closeAt) else { - throw ClassBattleError.invalidResponse - } - return ClassBattleSession( - classID: classId, - myRole: role, - closeAt: closeAt, - closed: closed, - commentRequired: commentRequired, - canParticipate: canParticipate, - participationCompleted: participationCompleted, - battle: try battle.toDomain() - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassDebateDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassDebateDTO.swift index 3af7fee7..82f94ab8 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassDebateDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassDebateDTO.swift @@ -15,17 +15,6 @@ struct ClassDebateDTO: Decodable, Sendable { let speaker: String let content: String - func toDomain() throws -> ClassDebateMessage { - guard let speaker = ClassDebateSpeaker(rawValue: speaker) else { - throw ClassAIChatError.invalidOutput - } - return ClassDebateMessage( - id: messageId, - round: round, - speaker: speaker, - content: content - ) - } } let sessionId: Int @@ -40,24 +29,4 @@ struct ClassDebateDTO: Decodable, Sendable { let aiNotice: String let replyMinLength: Int let replyMaxLength: Int - - func toDomain() throws -> ClassDebate { - guard let status = ClassDebateStatus(rawValue: status) else { - throw ClassAIChatError.invalidOutput - } - return try ClassDebate( - sessionID: sessionId, - status: status, - round: round, - canReply: canReply, - canRetry: canRetry, - philosopherName: philosopher.name, - participantOptionTitle: participantOption.title, - opponentOptionTitle: opponentOption.title, - messages: messages.map { try $0.toDomain() }, - aiNotice: aiNotice, - replyMinLength: replyMinLength, - replyMaxLength: replyMaxLength - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassMemberDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassMemberDTO.swift index 4ac9bf56..ac7f0ba8 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassMemberDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassMemberDTO.swift @@ -7,23 +7,4 @@ struct ClassMemberDTO: Decodable, Sendable { let role: String let characterUrl: URL? let me: Bool - - func toDomain() -> ClassMember { - ClassMember( - id: memberId, - name: displayName, - isOwner: role == "OWNER", - characterURL: characterUrl - ) - } - - static func toMemberList(_ payload: [Self]) throws -> ClassMemberList { - guard let current = payload.first(where: \.me) else { - throw ClassError.unknown("내 멤버 정보를 찾을 수 없어요.") - } - return ClassMemberList( - members: payload.map { $0.toDomain() }, - currentMemberID: current.memberId - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassPerspectiveDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassPerspectiveDTO.swift index 37b4ab9b..1bb7d9b9 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassPerspectiveDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassPerspectiveDTO.swift @@ -6,13 +6,6 @@ struct ClassPerspectiveReceiptDTO: Decodable, Sendable { let perspectiveId: Int let status: String let createdAt: String - - func toDomain() throws -> ClassPerspectiveReceipt { - guard let createdAt = ServerDateParser.parse(createdAt) else { - throw ClassBattleError.invalidResponse - } - return ClassPerspectiveReceipt(id: perspectiveId, status: status, createdAt: createdAt) - } } struct ClassPerspectiveItemDTO: Decodable, Sendable { @@ -22,14 +15,6 @@ struct ClassPerspectiveItemDTO: Decodable, Sendable { let characterType: String? let characterImageUrl: String? - func toDomain() -> ClassPerspectiveAuthor { - ClassPerspectiveAuthor( - userTag: userTag, - nickname: nickname, - characterType: characterType, - characterImageURL: characterImageUrl.flatMap(URL.init(string:)) - ) - } } struct Option: Decodable, Sendable { @@ -38,9 +23,6 @@ struct ClassPerspectiveItemDTO: Decodable, Sendable { let title: String let stance: String? - func toDomain() -> ClassPerspectiveOption { - ClassPerspectiveOption(id: optionId, label: label, title: title, stance: stance) - } } let perspectiveId: Int @@ -53,36 +35,10 @@ struct ClassPerspectiveItemDTO: Decodable, Sendable { let isMyPerspective: Bool? let status: String? let createdAt: String - - func toDomain() throws -> ClassPerspectiveItem { - guard let createdAt = ServerDateParser.parse(createdAt) else { - throw ClassBattleError.invalidResponse - } - return ClassPerspectiveItem( - id: perspectiveId, - author: user.toDomain(), - option: option.toDomain(), - content: content, - likeCount: likeCount, - commentCount: commentCount, - isLiked: isLiked, - isMyPerspective: isMyPerspective, - status: status, - createdAt: createdAt - ) - } } struct ClassPerspectivePageDTO: Decodable, Sendable { let items: [ClassPerspectiveItemDTO] let nextCursor: String? let hasNext: Bool - - func toDomain() throws -> ClassPerspectivePage { - ClassPerspectivePage( - items: try items.map { try $0.toDomain() }, - nextCursor: nextCursor, - hasNext: hasNext - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassResultDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassResultDTO.swift index 2a1e6df9..e311e01a 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassResultDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassResultDTO.swift @@ -11,15 +11,6 @@ struct ClassResultDTO: Decodable, Sendable { let postVoteCount: Int let postVoteRatio: Double - func toDomain() -> ClassResultOption { - ClassResultOption( - id: optionId, - title: title, - preVoteCount: preVoteCount, - postVoteCount: postVoteCount, - postVoteRatio: postVoteRatio - ) - } } let activeMemberCount: Int @@ -30,17 +21,6 @@ struct ClassResultDTO: Decodable, Sendable { let postVoteCount: Int let options: [Option] - func toDomain() -> ClassResultStats { - ClassResultStats( - activeMemberCount: activeMemberCount, - completedCount: completedCount, - participationRate: participationRate, - commentWriterCount: commentWriterCount, - commentRate: commentRate, - postVoteCount: postVoteCount, - options: options.map { $0.toDomain() } - ) - } } let classId: Int @@ -49,18 +29,4 @@ struct ClassResultDTO: Decodable, Sendable { let stats: Stats let myReportState: String? let insightState: String? - - func toDomain() throws -> ClassResult { - guard let closeAt = ServerDateParser.parse(closeAt) else { - throw ClassError.network("클래스 마감일을 읽을 수 없습니다.") - } - return ClassResult( - classID: classId, - closed: closed, - closeAt: closeAt, - stats: stats.toDomain(), - myReportState: myReportState, - insightState: insightState - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketDTO.swift index 1c177b87..b575b48f 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketDTO.swift @@ -10,23 +10,4 @@ struct ClassTicketDTO: Decodable, Sendable { let expiresAt: String let remainingDays: Int let readGraceEndsAt: String - - func toDomain() throws -> ClassTicket { - guard - let status = ClassTicket.Status(rawValue: status), - let activatedAt = ServerDateParser.parse(activatedAt), - let expiresAt = ServerDateParser.parse(expiresAt), - let readGraceEndsAt = ServerDateParser.parse(readGraceEndsAt) - else { throw ClassTicketError.invalidResponse } - - return ClassTicket( - id: ticketId, - capacity: capacity, - status: status, - activatedAt: activatedAt, - expiresAt: expiresAt, - remainingDays: remainingDays, - readGraceEndsAt: readGraceEndsAt - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketPreviewDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketPreviewDTO.swift index 81214532..798ebb91 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketPreviewDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTicketPreviewDTO.swift @@ -7,17 +7,4 @@ struct ClassTicketPreviewDTO: Decodable, Sendable { let registerBy: String let usageMonths: Int let registeredByMe: Bool - - func toDomain() throws -> ClassTicketPreview { - guard let date = ServerDateParser.parse(registerBy) else { - throw ClassTicketError.invalidResponse - } - - return ClassTicketPreview( - capacity: capacity, - registerBy: date, - usageMonths: usageMonths, - registeredByMe: registeredByMe - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTopicDraftDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTopicDraftDTO.swift index 7162ec10..52d69840 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTopicDraftDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassTopicDraftDTO.swift @@ -14,19 +14,6 @@ struct ClassTopicCandidateDTO: Decodable, Sendable { let category: String let difficulty: String let options: [Option] - - func toDomain() -> ClassTopicCandidate { - ClassTopicCandidate( - id: candidateId, - question: question, - description: description, - category: category, - difficulty: difficulty, - options: options.map { - .init(label: $0.label, rationale: $0.rationale, philosopher: $0.philosopher) - } - ) - } } struct ClassTopicVersionDTO: Decodable, Sendable { @@ -48,29 +35,6 @@ struct ClassTopicVersionDTO: Decodable, Sendable { let category: String let difficulty: String let options: [Option] - - func toDomain() -> ClassTopicVersion { - ClassTopicVersion( - id: topicVersionId, - draftID: draftId, - versionNo: versionNo, - source: source, - question: question, - description: description, - category: category, - difficulty: difficulty, - options: options.map { - .init( - position: $0.position, - label: $0.label, - rationale: $0.rationale, - philosopher: $0.philosopher, - philosopherName: $0.philosopherName, - philosopherImageKey: $0.philosopherImageKey - ) - } - ) - } } struct ClassTopicDraftDTO: Decodable, Sendable { @@ -85,22 +49,6 @@ struct ClassTopicDraftDTO: Decodable, Sendable { let remainingValidations: Int let candidates: [ClassTopicCandidateDTO] let latestVersion: ClassTopicVersionDTO? - - func toDomain() -> ClassTopicDraft { - ClassTopicDraft( - id: draftId, - topicSeed: topicSeed, - difficulty: difficulty, - categories: categories, - suggestStatus: suggestStatus, - clarificationMessage: clarificationMessage, - batchNo: batchNo, - remainingSuggestions: remainingSuggestions, - remainingValidations: remainingValidations, - candidates: candidates.map { $0.toDomain() }, - latestVersion: latestVersion?.toDomain() - ) - } } struct ClassTopicValidationDTO: Decodable, Sendable { @@ -115,14 +63,4 @@ struct ClassTopicValidationDTO: Decodable, Sendable { let validatedTopicVersionId: Int? let remainingValidations: Int let version: ClassTopicVersionDTO? - - func toDomain() -> ClassTopicValidation { - ClassTopicValidation( - valid: valid, - issues: issues.map { .init(field: $0.field, code: $0.code, message: $0.message) }, - validatedTopicVersionID: validatedTopicVersionId, - remainingValidations: remainingValidations, - version: version?.toDomain() - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassVoteDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassVoteDTO.swift index 399fb072..5f209386 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassVoteDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassVoteDTO.swift @@ -5,13 +5,6 @@ import PickeCoreUtility struct ClassVoteReceiptDTO: Decodable, Sendable { let voteId: Int let status: String - - func toDomain() throws -> ClassVoteReceipt { - guard let status = ClassBattleStep(rawValue: status) else { - throw ClassBattleError.invalidResponse - } - return ClassVoteReceipt(voteID: voteId, status: status) - } } struct ClassVoteHistoryDTO: Decodable, Sendable { @@ -20,9 +13,6 @@ struct ClassVoteHistoryDTO: Decodable, Sendable { let label: String let title: String - func toDomain() -> ClassVoteChoice { - ClassVoteChoice(optionID: optionId, label: label, title: title) - } } let battleTitle: String @@ -30,19 +20,6 @@ struct ClassVoteHistoryDTO: Decodable, Sendable { let postVote: Choice? let status: String let opinionChanged: Bool? - - func toDomain() throws -> ClassVoteHistory { - guard let status = ClassBattleStep(rawValue: status) else { - throw ClassBattleError.invalidResponse - } - return ClassVoteHistory( - battleTitle: battleTitle, - preVote: preVote.toDomain(), - postVote: postVote?.toDomain(), - status: status, - opinionChanged: opinionChanged - ) - } } struct ClassVoteStatsDTO: Decodable, Sendable { @@ -53,29 +30,9 @@ struct ClassVoteStatsDTO: Decodable, Sendable { let voteCount: Int let ratio: Double - func toDomain() -> ClassVoteStatsOption { - ClassVoteStatsOption( - id: optionId, - title: title, - imageURL: imageUrl.flatMap(URL.init(string:)), - voteCount: voteCount, - ratio: ratio - ) - } } let options: [Option] let totalCount: Int let updatedAt: String - - func toDomain() throws -> ClassVoteStats { - guard let updatedAt = ServerDateParser.parse(updatedAt) else { - throw ClassBattleError.invalidResponse - } - return ClassVoteStats( - options: options.map { $0.toDomain() }, - totalCount: totalCount, - updatedAt: updatedAt - ) - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassroomDataDTO.swift b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassroomDataDTO.swift index 54a0b963..01dd32fa 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassroomDataDTO.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/DTO/ClassroomDataDTO.swift @@ -14,25 +14,6 @@ struct ClassListItemDTO: Decodable, Sendable { let capacity: Int let closeAt: String let closed: Bool - - func toDomain() throws -> ClassRoom { - let closeDate = try closeAt.classDate() - return ClassRoom( - id: classId, - name: name, - joinCode: "", - battle: .classProjection(id: battleId, title: battleTitle, thumbnailUrl: battleThumbnailUrl), - deadline: .distantFuture, - closeAt: closeDate, - memberCount: memberCount, - role: try myRole.classRole(), - status: closed ? .closed : .open, - allowsAnonymousOpinion: false, - requiresComment: false, - memberCapacity: capacity, - battleType: battleType - ) - } } struct ClassDetailDataDTO: Decodable, Sendable { @@ -57,31 +38,6 @@ struct ClassDetailDataDTO: Decodable, Sendable { let closed: Bool let commentRequired: Bool let ownerParticipates: Bool - - func toDomain() throws -> ClassRoom { - return ClassRoom( - id: classId, - name: name, - joinCode: inviteCode ?? "", - battle: .classProjection( - id: battle.battleId, - title: battle.title, - summary: battle.summary ?? "", - thumbnailUrl: battle.thumbnailUrl - ), - deadline: try deadline.map { try $0.classDate() } ?? .distantFuture, - closeAt: try closeAt.classDate(), - memberCount: memberCount, - role: try myRole.classRole(), - status: closed ? .closed : .open, - allowsAnonymousOpinion: false, - requiresComment: commentRequired, - ownerParticipates: ownerParticipates, - memberCapacity: capacity, - battleType: battleType, - ticketExpiresAt: try ticketExpiresAt.classDate() - ) - } } struct ClassJoinPreviewDataDTO: Decodable, Sendable { @@ -93,59 +49,4 @@ struct ClassJoinPreviewDataDTO: Decodable, Sendable { let memberCount: Int let capacity: Int let closeAt: String - - func toDomain() throws -> ClassJoinPreview { - ClassJoinPreview( - inviteCode: inviteCode, - name: name, - battleTitle: battleTitle, - battleThumbnailURL: battleThumbnailUrl.flatMap(URL.init(string:)), - battleType: battleType, - memberCount: memberCount, - capacity: capacity, - closeAt: try closeAt.classDate() - ) - } -} - -private extension ClassBattleSummary { - static func classProjection( - id: Int, - title: String, - summary: String = "", - thumbnailUrl: String? - ) -> Self { - ClassBattleSummary( - id: id, - title: title, - summary: summary, - thumbnailURL: thumbnailUrl.flatMap(URL.init(string:)), - philosopherA: "", - optionATitle: "", - philosopherB: "", - optionBTitle: "", - category: .philosophy, - categoryDisplayTitle: "", - level: .middle, - audioDuration: 0, - viewCount: 0 - ) - } -} - -private extension String { - func classDate() throws -> Date { - guard let date = date(as: .serverDateTime) else { - throw ClassError.unknown("클래스 날짜 형식을 읽을 수 없습니다: \(self)") - } - return date - } - - func classRole() throws -> ClassRole { - switch self { - case "OWNER": return .owner - case "MEMBER": return .member - default: throw ClassError.unknown("알 수 없는 클래스 역할: \(self)") - } - } } diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift new file mode 100644 index 00000000..bee45cba --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift @@ -0,0 +1,102 @@ +import ClassDomainInterface + +extension ClassPersonalReportDTO.VoteOption { + func toDomain() -> ClassAIFeedbackVoteChoice { + ClassAIFeedbackVoteChoice(id: optionId, title: title, stance: stance) + } +} + +extension ClassPersonalReportDTO.Feedback.Evidence { + func toDomain() -> ClassAIFeedbackEvidence { + ClassAIFeedbackEvidence( + sourceType: sourceType, + sourceID: sourceId, + content: content, + quote: quote, + field: field + ) + } +} + +extension ClassPersonalReportDTO.Feedback { + func toDomain( + preOption: VoteOption?, + postOption: VoteOption?, + perspectiveCount: Int, + commentCount: Int + ) -> ClassAIPersonalFeedback { + let points = [strength, nextFocus].compactMap(\.self).map { + ClassAIFeedbackPoint(title: $0.title, detail: $0.body) + } + return ClassAIPersonalFeedback( + summaryTitle: headline, + summaryDetail: judgmentBasis, + points: points, + nextQuestion: nextQuestion, + nextHint: "", + evidence: evidence?.map { $0.toDomain() } ?? [], + coverage: coverage, + excludedCount: excludedCount ?? 0, + preOption: preOption?.toDomain(), + postOption: postOption?.toDomain(), + perspectiveCount: perspectiveCount, + commentCount: commentCount + ) + } +} + +extension ClassPersonalReportDTO { + func toDomain() throws -> ClassAIPersonalFeedback { + switch state { + case "SUCCEEDED", "LIMITED": + guard let feedback else { throw ClassAIFeedbackError.generationFailed } + return feedback.toDomain( + preOption: preOption, + postOption: postOption, + perspectiveCount: perspectiveCount ?? 0, + commentCount: commentCount ?? 0 + ) + case "BEFORE_CLOSE", "PENDING", "GENERATING": + throw ClassAIFeedbackError.modelNotReady + case "NOT_ELIGIBLE": + throw ClassAIFeedbackError.insufficientInput + case "FAILED": + throw ClassAIFeedbackError.generationFailed + default: + throw ClassAIFeedbackError.generationFailed + } + } +} + +extension ClassInsightDTO.Evidence { + func toDomain() -> ClassAIFeedbackEvidence { + ClassAIFeedbackEvidence( + sourceType: sourceType, + sourceID: id ?? sourceId, + content: content, + quote: quote + ) + } +} + +extension ClassInsightDTO { + func toDomain() throws -> ClassAIInsight { + switch state { + case "SUCCEEDED", "LIMITED": + guard let insightText, !insightText.isEmpty else { throw ClassAIFeedbackError.generationFailed } + return ClassAIInsight( + text: insightText, + evidence: evidence?.map { $0.toDomain() } ?? [], + highlights: highlights?.map { $0.toDomain() } ?? [], + coverage: coverage, + excludedCount: excludedCount ?? 0 + ) + case "BEFORE_CLOSE", "PENDING", "GENERATING": + throw ClassAIFeedbackError.modelNotReady + case "FAILED": + throw retryable ? ClassAIFeedbackError.serverRetryAvailable : .generationFailed + default: + throw ClassAIFeedbackError.generationFailed + } + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleRecommendationDataDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleRecommendationDataDTO+.swift new file mode 100644 index 00000000..2f3d57b1 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleRecommendationDataDTO+.swift @@ -0,0 +1,40 @@ +import ClassDomainInterface +import Foundation + +extension ClassBattleRecommendationItemDTO { + func toDomain() throws -> ClassBattleSummary { + guard let category = ClassCategory.allCases.first(where: { $0.title == self.category }) else { + throw ClassError.unknown("알 수 없는 배틀 카테고리: \(category)") + } + let level: ClassAudienceLevel + switch difficulty { + case "BEGINNER": level = .middle + case "INTERMEDIATE": level = .high + case "ADVANCED": level = .adult + default: throw ClassError.unknown("알 수 없는 배틀 수준: \(difficulty)") + } + guard let optionA = options.first(where: { $0.position == "A" }), + let optionB = options.first(where: { $0.position == "B" }) + else { + throw ClassError.unknown("배틀의 A/B 선택지가 누락됐습니다.") + } + return ClassBattleSummary( + id: battleId, + title: title, + summary: summary ?? "", + thumbnailURL: thumbnailUrl.flatMap(URL.init(string:)), + philosopherA: optionA.representative ?? "", + optionATitle: optionA.title, + optionADescription: optionA.stance, + philosopherAImageURL: optionA.imageUrl.flatMap(URL.init(string:)), + philosopherB: optionB.representative ?? "", + optionBTitle: optionB.title, + optionBDescription: optionB.stance, + philosopherBImageURL: optionB.imageUrl.flatMap(URL.init(string:)), + category: category, + level: level, + audioDuration: audioDuration ?? 0, + viewCount: 0 + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleSessionDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleSessionDTO+.swift new file mode 100644 index 00000000..da1592e7 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassBattleSessionDTO+.swift @@ -0,0 +1,61 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassBattleSessionDTO.Battle.Info.Option { + func toDomain() -> ClassBattleOption { + ClassBattleOption( + id: optionId, + label: label, + title: title, + stance: stance ?? "", + representative: representative ?? "", + imageURL: imageUrl.flatMap(URL.init(string:)) + ) + } +} + +extension ClassBattleSessionDTO.Battle { + func toDomain() throws -> ClassBattleDetail { + guard let currentStep = ClassBattleStep(rawValue: currentStep ?? "NONE") else { + throw ClassBattleError.invalidResponse + } + return ClassBattleDetail( + id: battleInfo.battleId, + title: battleInfo.title, + summary: battleInfo.summary ?? "", + thumbnailURL: battleInfo.thumbnailUrl.flatMap(URL.init(string:)), + description: description ?? "", + shareURL: shareUrl.flatMap(URL.init(string:)), + userVoteStatus: userVoteStatus, + currentStep: currentStep, + participantsCount: battleInfo.participantsCount ?? 0, + audioDuration: battleInfo.audioDuration ?? 0, + options: battleInfo.options.map { $0.toDomain() } + ) + } +} + +extension ClassBattleSessionDTO { + func toDomain() throws -> ClassBattleSession { + let role: ClassRole + switch myRole { + case "OWNER": role = .owner + case "MEMBER": role = .member + default: throw ClassBattleError.invalidResponse + } + guard let closeAt = ServerDateParser.parse(closeAt) else { + throw ClassBattleError.invalidResponse + } + return ClassBattleSession( + classID: classId, + myRole: role, + closeAt: closeAt, + closed: closed, + commentRequired: commentRequired, + canParticipate: canParticipate, + participationCompleted: participationCompleted, + battle: try battle.toDomain() + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassDebateDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassDebateDTO+.swift new file mode 100644 index 00000000..7b8bbb91 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassDebateDTO+.swift @@ -0,0 +1,37 @@ +import ClassDomainInterface + +extension ClassDebateDTO.Message { + func toDomain() throws -> ClassDebateMessage { + guard let speaker = ClassDebateSpeaker(rawValue: speaker) else { + throw ClassAIChatError.invalidOutput + } + return ClassDebateMessage( + id: messageId, + round: round, + speaker: speaker, + content: content + ) + } +} + +extension ClassDebateDTO { + func toDomain() throws -> ClassDebate { + guard let status = ClassDebateStatus(rawValue: status) else { + throw ClassAIChatError.invalidOutput + } + return try ClassDebate( + sessionID: sessionId, + status: status, + round: round, + canReply: canReply, + canRetry: canRetry, + philosopherName: philosopher.name, + participantOptionTitle: participantOption.title, + opponentOptionTitle: opponentOption.title, + messages: messages.map { try $0.toDomain() }, + aiNotice: aiNotice, + replyMinLength: replyMinLength, + replyMaxLength: replyMaxLength + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassMemberDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassMemberDTO+.swift new file mode 100644 index 00000000..bf06f21d --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassMemberDTO+.swift @@ -0,0 +1,25 @@ +import ClassDomainInterface +import Foundation + +extension ClassMemberDTO { + func toDomain() -> ClassMember { + ClassMember( + id: memberId, + name: displayName, + isOwner: role == "OWNER", + characterURL: characterUrl + ) + } +} + +extension ClassMemberDTO { + static func toMemberList(_ payload: [Self]) throws -> ClassMemberList { + guard let current = payload.first(where: \.me) else { + throw ClassError.unknown("내 멤버 정보를 찾을 수 없어요.") + } + return ClassMemberList( + members: payload.map { $0.toDomain() }, + currentMemberID: current.memberId + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassPerspectiveDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassPerspectiveDTO+.swift new file mode 100644 index 00000000..e99be720 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassPerspectiveDTO+.swift @@ -0,0 +1,59 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassPerspectiveReceiptDTO { + func toDomain() throws -> ClassPerspectiveReceipt { + guard let createdAt = ServerDateParser.parse(createdAt) else { + throw ClassBattleError.invalidResponse + } + return ClassPerspectiveReceipt(id: perspectiveId, status: status, createdAt: createdAt) + } +} + +extension ClassPerspectiveItemDTO.User { + func toDomain() -> ClassPerspectiveAuthor { + ClassPerspectiveAuthor( + userTag: userTag, + nickname: nickname, + characterType: characterType, + characterImageURL: characterImageUrl.flatMap(URL.init(string:)) + ) + } +} + +extension ClassPerspectiveItemDTO.Option { + func toDomain() -> ClassPerspectiveOption { + ClassPerspectiveOption(id: optionId, label: label, title: title, stance: stance) + } +} + +extension ClassPerspectiveItemDTO { + func toDomain() throws -> ClassPerspectiveItem { + guard let createdAt = ServerDateParser.parse(createdAt) else { + throw ClassBattleError.invalidResponse + } + return ClassPerspectiveItem( + id: perspectiveId, + author: user.toDomain(), + option: option.toDomain(), + content: content, + likeCount: likeCount, + commentCount: commentCount, + isLiked: isLiked, + isMyPerspective: isMyPerspective, + status: status, + createdAt: createdAt + ) + } +} + +extension ClassPerspectivePageDTO { + func toDomain() throws -> ClassPerspectivePage { + ClassPerspectivePage( + items: try items.map { try $0.toDomain() }, + nextCursor: nextCursor, + hasNext: hasNext + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassResultDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassResultDTO+.swift new file mode 100644 index 00000000..6ed67760 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassResultDTO+.swift @@ -0,0 +1,45 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassResultDTO.Stats.Option { + func toDomain() -> ClassResultOption { + ClassResultOption( + id: optionId, + title: title, + preVoteCount: preVoteCount, + postVoteCount: postVoteCount, + postVoteRatio: postVoteRatio + ) + } +} + +extension ClassResultDTO.Stats { + func toDomain() -> ClassResultStats { + ClassResultStats( + activeMemberCount: activeMemberCount, + completedCount: completedCount, + participationRate: participationRate, + commentWriterCount: commentWriterCount, + commentRate: commentRate, + postVoteCount: postVoteCount, + options: options.map { $0.toDomain() } + ) + } +} + +extension ClassResultDTO { + func toDomain() throws -> ClassResult { + guard let closeAt = ServerDateParser.parse(closeAt) else { + throw ClassError.network("클래스 마감일을 읽을 수 없습니다.") + } + return ClassResult( + classID: classId, + closed: closed, + closeAt: closeAt, + stats: stats.toDomain(), + myReportState: myReportState, + insightState: insightState + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketDTO+.swift new file mode 100644 index 00000000..bec26639 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketDTO+.swift @@ -0,0 +1,24 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassTicketDTO { + func toDomain() throws -> ClassTicket { + guard + let status = ClassTicket.Status(rawValue: status), + let activatedAt = ServerDateParser.parse(activatedAt), + let expiresAt = ServerDateParser.parse(expiresAt), + let readGraceEndsAt = ServerDateParser.parse(readGraceEndsAt) + else { throw ClassTicketError.invalidResponse } + + return ClassTicket( + id: ticketId, + capacity: capacity, + status: status, + activatedAt: activatedAt, + expiresAt: expiresAt, + remainingDays: remainingDays, + readGraceEndsAt: readGraceEndsAt + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketPreviewDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketPreviewDTO+.swift new file mode 100644 index 00000000..b02f5ba6 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTicketPreviewDTO+.swift @@ -0,0 +1,18 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassTicketPreviewDTO { + func toDomain() throws -> ClassTicketPreview { + guard let date = ServerDateParser.parse(registerBy) else { + throw ClassTicketError.invalidResponse + } + + return ClassTicketPreview( + capacity: capacity, + registerBy: date, + usageMonths: usageMonths, + registeredByMe: registeredByMe + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTopicDraftDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTopicDraftDTO+.swift new file mode 100644 index 00000000..3c796894 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassTopicDraftDTO+.swift @@ -0,0 +1,72 @@ +import ClassDomainInterface +import Foundation + +extension ClassTopicCandidateDTO { + func toDomain() -> ClassTopicCandidate { + ClassTopicCandidate( + id: candidateId, + question: question, + description: description, + category: category, + difficulty: difficulty, + options: options.map { + .init(label: $0.label, rationale: $0.rationale, philosopher: $0.philosopher) + } + ) + } +} + +extension ClassTopicVersionDTO { + func toDomain() -> ClassTopicVersion { + ClassTopicVersion( + id: topicVersionId, + draftID: draftId, + versionNo: versionNo, + source: source, + question: question, + description: description, + category: category, + difficulty: difficulty, + options: options.map { + .init( + position: $0.position, + label: $0.label, + rationale: $0.rationale, + philosopher: $0.philosopher, + philosopherName: $0.philosopherName, + philosopherImageKey: $0.philosopherImageKey + ) + } + ) + } +} + +extension ClassTopicDraftDTO { + func toDomain() -> ClassTopicDraft { + ClassTopicDraft( + id: draftId, + topicSeed: topicSeed, + difficulty: difficulty, + categories: categories, + suggestStatus: suggestStatus, + clarificationMessage: clarificationMessage, + batchNo: batchNo, + remainingSuggestions: remainingSuggestions, + remainingValidations: remainingValidations, + candidates: candidates.map { $0.toDomain() }, + latestVersion: latestVersion?.toDomain() + ) + } +} + +extension ClassTopicValidationDTO { + func toDomain() -> ClassTopicValidation { + ClassTopicValidation( + valid: valid, + issues: issues.map { .init(field: $0.field, code: $0.code, message: $0.message) }, + validatedTopicVersionID: validatedTopicVersionId, + remainingValidations: remainingValidations, + version: version?.toDomain() + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassVoteDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassVoteDTO+.swift new file mode 100644 index 00000000..14d33d7a --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassVoteDTO+.swift @@ -0,0 +1,58 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassVoteReceiptDTO { + func toDomain() throws -> ClassVoteReceipt { + guard let status = ClassBattleStep(rawValue: status) else { + throw ClassBattleError.invalidResponse + } + return ClassVoteReceipt(voteID: voteId, status: status) + } +} + +extension ClassVoteHistoryDTO.Choice { + func toDomain() -> ClassVoteChoice { + ClassVoteChoice(optionID: optionId, label: label, title: title) + } +} + +extension ClassVoteHistoryDTO { + func toDomain() throws -> ClassVoteHistory { + guard let status = ClassBattleStep(rawValue: status) else { + throw ClassBattleError.invalidResponse + } + return ClassVoteHistory( + battleTitle: battleTitle, + preVote: preVote.toDomain(), + postVote: postVote?.toDomain(), + status: status, + opinionChanged: opinionChanged + ) + } +} + +extension ClassVoteStatsDTO.Option { + func toDomain() -> ClassVoteStatsOption { + ClassVoteStatsOption( + id: optionId, + title: title, + imageURL: imageUrl.flatMap(URL.init(string:)), + voteCount: voteCount, + ratio: ratio + ) + } +} + +extension ClassVoteStatsDTO { + func toDomain() throws -> ClassVoteStats { + guard let updatedAt = ServerDateParser.parse(updatedAt) else { + throw ClassBattleError.invalidResponse + } + return ClassVoteStats( + options: options.map { $0.toDomain() }, + totalCount: totalCount, + updatedAt: updatedAt + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassroomDataDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassroomDataDTO+.swift new file mode 100644 index 00000000..290a6675 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassroomDataDTO+.swift @@ -0,0 +1,108 @@ +import ClassDomainInterface +import Foundation +import PickeCoreUtility + +extension ClassListItemDTO { + func toDomain() throws -> ClassRoom { + let closeDate = try closeAt.classDate() + return ClassRoom( + id: classId, + name: name, + joinCode: "", + battle: .classProjection(id: battleId, title: battleTitle, thumbnailUrl: battleThumbnailUrl), + deadline: .distantFuture, + closeAt: closeDate, + memberCount: memberCount, + role: try myRole.classRole(), + status: closed ? .closed : .open, + allowsAnonymousOpinion: false, + requiresComment: false, + memberCapacity: capacity, + battleType: battleType + ) + } +} + +extension ClassDetailDataDTO { + func toDomain() throws -> ClassRoom { + return ClassRoom( + id: classId, + name: name, + joinCode: inviteCode ?? "", + battle: .classProjection( + id: battle.battleId, + title: battle.title, + summary: battle.summary ?? "", + thumbnailUrl: battle.thumbnailUrl + ), + deadline: try deadline.map { try $0.classDate() } ?? .distantFuture, + closeAt: try closeAt.classDate(), + memberCount: memberCount, + role: try myRole.classRole(), + status: closed ? .closed : .open, + allowsAnonymousOpinion: false, + requiresComment: commentRequired, + ownerParticipates: ownerParticipates, + memberCapacity: capacity, + battleType: battleType, + ticketExpiresAt: try ticketExpiresAt.classDate() + ) + } +} + +extension ClassJoinPreviewDataDTO { + func toDomain() throws -> ClassJoinPreview { + ClassJoinPreview( + inviteCode: inviteCode, + name: name, + battleTitle: battleTitle, + battleThumbnailURL: battleThumbnailUrl.flatMap(URL.init(string:)), + battleType: battleType, + memberCount: memberCount, + capacity: capacity, + closeAt: try closeAt.classDate() + ) + } +} + +private extension ClassBattleSummary { + static func classProjection( + id: Int, + title: String, + summary: String = "", + thumbnailUrl: String? + ) -> Self { + ClassBattleSummary( + id: id, + title: title, + summary: summary, + thumbnailURL: thumbnailUrl.flatMap(URL.init(string:)), + philosopherA: "", + optionATitle: "", + philosopherB: "", + optionBTitle: "", + category: .philosophy, + categoryDisplayTitle: "", + level: .middle, + audioDuration: 0, + viewCount: 0 + ) + } +} + +private extension String { + func classDate() throws -> Date { + guard let date = date(as: .serverDateTime) else { + throw ClassError.unknown("클래스 날짜 형식을 읽을 수 없습니다: \(self)") + } + return date + } + + func classRole() throws -> ClassRole { + switch self { + case "OWNER": return .owner + case "MEMBER": return .member + default: throw ClassError.unknown("알 수 없는 클래스 역할: \(self)") + } + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassAIRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassAIRepositoryImpl.swift deleted file mode 100644 index a88a6a46..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassAIRepositoryImpl.swift +++ /dev/null @@ -1,50 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation -import PickeNetwork - -public struct ClassAIRepositoryImpl: ClassAIRepositoryInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func fetchDebate(classID: Int) async throws -> ClassDebate { - try await debate(.fetch(classID)) - } - - public func startDebate(classID: Int) async throws -> ClassDebate { - try await debate(.start(classID)) - } - - public func retryDebate(classID: Int) async throws -> ClassDebate { - try await debate(.retry(classID)) - } - - public func submitDebateReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate { - try await debate(.submitReply(classID: classID, round: round, content: content, requestKey: requestKey)) - } - - private func debate(_ service: ClassDebateService) async throws -> ClassDebate { - do { - let payload = try await client.send(service, as: ClassDebateDTO.self) - return try payload.toDomain() - } catch let error as PickeNetworkError { - if case let .response(response) = error { - switch response.code { - case "CLASS_404_DEBATE": throw ClassAIChatError.noSession - case "CLASS_400_DEBATE_REPLY": throw ClassAIChatError.contentRejected - case "CLASS_409_DEBATE_STATE", "CLASS_409_DEBATE_ROUND", "CLASS_409_DEBATE_RETRY": - throw ClassAIChatError.temporarilyUnavailable - default: throw ClassAIChatError.unexpectedFailure - } - } - throw ClassAIChatError.temporarilyUnavailable - } - } - -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassBattleRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassBattleRepositoryImpl.swift deleted file mode 100644 index 15855b62..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassBattleRepositoryImpl.swift +++ /dev/null @@ -1,134 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation -import PickeNetwork - -public struct ClassBattleRepositoryImpl: ClassBattleInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func fetchBattle(classID: Int) async throws -> ClassBattleSession { - do { - let payload = try await client.send(ClassBattleService.detail(classID: classID), as: ClassBattleSessionDTO.self) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func submitPreVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - do { - let payload = try await client.send( - ClassBattleService.preVote(classID: classID, optionID: optionID), - as: ClassVoteReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func completeTTS(classID: Int) async throws { - do { - _ = try await client.send( - ClassBattleService.ttsComplete(classID: classID), - as: PickeEmptyResponse.self - ) - } catch { throw Self.map(error) } - } - - public func submitPostVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - do { - let payload = try await client.send( - ClassBattleService.postVote(classID: classID, optionID: optionID), - as: ClassVoteReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { - do { - let payload = try await client.send(ClassBattleService.myVotes(classID: classID), as: ClassVoteHistoryDTO.self) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { - do { - let payload = try await client.send(ClassBattleService.voteStats(classID: classID), as: ClassVoteStatsDTO.self) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func publishPerspective( - classID: Int, - content: String - ) async throws -> ClassPerspectiveReceipt { - do { - let payload = try await client.send( - ClassBattleService.publishPerspective(classID: classID, content: content), - as: ClassPerspectiveReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage { - do { - let payload = try await client.send( - ClassBattleService.perspectives( - classID: classID, - cursor: cursor, - size: size, - optionID: optionID, - sort: sort - ), - as: ClassPerspectivePageDTO.self - ) - return try payload.toDomain() - } catch { throw Self.map(error) } - } - - public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { - do { - let payload = try await client.send( - ClassBattleService.myPerspectives(classID: classID), - as: [ClassPerspectiveItemDTO].self - ) - return try payload.map { try $0.toDomain() } - } catch { throw Self.map(error) } - } - - private static func map(_ error: Error) -> ClassBattleError { - if let error = error as? ClassBattleError { return error } - if let error = error as? PickeNetworkError, - case let .response(response) = error - { - switch response.code { - case "CLASS_404": return .classNotFound - case "CLASS_403_MEMBER": return .membershipRequired - case "CLASS_409_TICKET_ENDED": return .ticketEnded - case "CLASS_409_CLOSED": return .closed - case "CLASS_409_PRE_VOTE_LOCKED": return .preVoteLocked - case "BATTLE_OPT_404": return .invalidOption - case "CLASS_409_TTS": return .ttsUnavailable - case "VOTE_404": return .voteNotFound - case "VOTE_409_PRE": return .preVoteRequired - case "VOTE_409_SUB": return .alreadySubmitted - case "CLASS_409_DEBATE_INCOMPLETE": return .debateIncomplete - default: return .network(response.message ?? "클래스 배틀 요청을 처리하지 못했어요.") - } - } - return .network(error.localizedDescription) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassLiveRepository.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassLiveRepository.swift deleted file mode 100644 index f2397dd1..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassLiveRepository.swift +++ /dev/null @@ -1,55 +0,0 @@ -import ClassDomainInterface -import Foundation - -/// 운영 경로에서는 서버에 연결되지 않은 기능을 성공으로 가장하지 않는다. -struct ClassLiveRepository: ClassInterface { - let api: any ClassInterface - - func fetchMyClasses() async throws -> [ClassRoom] { - try await api.fetchMyClasses() - } - - func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { - try await api.fetchRecommendedBattles(filter: filter) - } - - func createClass(_ creation: ClassCreation) async throws -> ClassRoom { - try await api.createClass(creation) - } - - func fetchClass(id: Int) async throws -> ClassRoom { - try await api.fetchClass(id: id) - } - - func fetchClass(joinCode: String) async throws -> ClassRoom { - try await api.fetchClass(joinCode: joinCode) - } - - func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { - try await api.fetchClassPreview(joinCode: joinCode) - } - - func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { - try await api.joinClass(joinCode: joinCode, nickname: nickname) - } - - func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { - try await api.updateDeadline(id: id, deadline: deadline) - } - - func deleteClass(id: Int) async throws { - try await api.deleteClass(id: id) - } - - func leaveClass(id: Int) async throws { - try await api.leaveClass(id: id) - } - - func fetchReport(roomID: Int) async throws -> ClassReport? { - try await api.fetchReport(roomID: roomID) - } - - func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? { - try await api.fetchOwnerDashboard(roomID: roomID) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassMemberRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassMemberRepositoryImpl.swift deleted file mode 100644 index 1c100886..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassMemberRepositoryImpl.swift +++ /dev/null @@ -1,55 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation -import PickeNetwork - -public struct ClassMemberRepositoryImpl: ClassMemberInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func fetchMembers(classID: Int) async throws -> ClassMemberList { - do { - let data = try await client.send( - ClassMemberService.members(classID: classID), - as: [ClassMemberDTO].self - ) - return try ClassMemberDTO.toMemberList(data) - } catch { - throw Self.classError(error) - } - } - - public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { - do { - let data = try await client.send( - ClassMemberService.updateMyName(classID: classID, name: name), - as: ClassMemberDTO.self - ) - return data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func removeMember(classID: Int, memberID: Int) async throws { - do { - _ = try await client.send( - ClassMemberService.remove(classID: classID, memberID: memberID), - as: PickeEmptyResponse.self - ) - } catch { - throw Self.classError(error) - } - } - - private static func classError(_ error: Error) -> ClassError { - guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } - guard case let .response(response) = networkError else { return ClassError.from(networkError) } - return ClassErrorMapper.map( - code: response.code, - message: response.message, - fallback: "멤버 요청을 처리하지 못했어요." - ) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift index 971011f7..1c9eef1f 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift @@ -5,12 +5,13 @@ // Created by Picke on 10/11/26. // +import APIEndpoint import ClassDomainInterface import Dependencies import Foundation import PickeNetwork -public struct ClassRepositoryImpl: ClassInterface { +public final class ClassRepositoryImpl: ClassInterface { private static let unavailableMessage = "클래스 서버 API가 연결되지 않았습니다." @Dependency(\.networkClient) private var client @@ -136,3 +137,427 @@ public struct ClassRepositoryImpl: ClassInterface { ) } } + +extension ClassRepositoryImpl { + public func fetchBattle(classID: Int) async throws -> ClassBattleSession { + do { + let payload = try await client.send(ClassBattleService.detail(classID: classID), as: ClassBattleSessionDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func submitPreVote( + classID: Int, + optionID: Int + ) async throws -> ClassVoteReceipt { + do { + let payload = try await client.send( + ClassBattleService.preVote(classID: classID, optionID: optionID), + as: ClassVoteReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func completeTTS(classID: Int) async throws { + do { + _ = try await client.send( + ClassBattleService.ttsComplete(classID: classID), + as: PickeEmptyResponse.self + ) + } catch { throw Self.battleError(error) } + } + + public func submitPostVote( + classID: Int, + optionID: Int + ) async throws -> ClassVoteReceipt { + do { + let payload = try await client.send( + ClassBattleService.postVote(classID: classID, optionID: optionID), + as: ClassVoteReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { + do { + let payload = try await client.send(ClassBattleService.myVotes(classID: classID), as: ClassVoteHistoryDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { + do { + let payload = try await client.send(ClassBattleService.voteStats(classID: classID), as: ClassVoteStatsDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func publishPerspective( + classID: Int, + content: String + ) async throws -> ClassPerspectiveReceipt { + do { + let payload = try await client.send( + ClassBattleService.publishPerspective(classID: classID, content: content), + as: ClassPerspectiveReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage { + do { + let payload = try await client.send( + ClassBattleService.perspectives( + classID: classID, + cursor: cursor, + size: size, + optionID: optionID, + sort: sort + ), + as: ClassPerspectivePageDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { + do { + let payload = try await client.send( + ClassBattleService.myPerspectives(classID: classID), + as: [ClassPerspectiveItemDTO].self + ) + return try payload.map { try $0.toDomain() } + } catch { throw Self.battleError(error) } + } + + private static func battleError(_ error: Error) -> ClassBattleError { + if let error = error as? ClassBattleError { return error } + if let error = error as? PickeNetworkError, + case let .response(response) = error + { + switch response.code { + case "CLASS_404": return .classNotFound + case "CLASS_403_MEMBER": return .membershipRequired + case "CLASS_409_TICKET_ENDED": return .ticketEnded + case "CLASS_409_CLOSED": return .closed + case "CLASS_409_PRE_VOTE_LOCKED": return .preVoteLocked + case "BATTLE_OPT_404": return .invalidOption + case "CLASS_409_TTS": return .ttsUnavailable + case "VOTE_404": return .voteNotFound + case "VOTE_409_PRE": return .preVoteRequired + case "VOTE_409_SUB": return .alreadySubmitted + case "CLASS_409_DEBATE_INCOMPLETE": return .debateIncomplete + default: return .network(response.message ?? "클래스 배틀 요청을 처리하지 못했어요.") + } + } + return .network(error.localizedDescription) + } +} + +extension ClassRepositoryImpl { + public func fetchMembers(classID: Int) async throws -> ClassMemberList { + do { + let data = try await client.send( + ClassMemberService.members(classID: classID), + as: [ClassMemberDTO].self + ) + return try ClassMemberDTO.toMemberList(data) + } catch { + throw Self.memberError(error) + } + } + + public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { + do { + let data = try await client.send( + ClassMemberService.updateMyName(classID: classID, name: name), + as: ClassMemberDTO.self + ) + return data.toDomain() + } catch { + throw Self.memberError(error) + } + } + + public func removeMember(classID: Int, memberID: Int) async throws { + do { + _ = try await client.send( + ClassMemberService.remove(classID: classID, memberID: memberID), + as: PickeEmptyResponse.self + ) + } catch { + throw Self.memberError(error) + } + } + + private static func memberError(_ error: Error) -> ClassError { + guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } + guard case let .response(response) = networkError else { return ClassError.from(networkError) } + return ClassErrorMapper.map( + code: response.code, + message: response.message, + fallback: "멤버 요청을 처리하지 못했어요." + ) + } +} + +extension ClassRepositoryImpl { + public func previewTicket(code: String) async throws -> ClassTicketPreview { + do { + let payload = try await client.send(ClassTicketService.preview(code: code), as: ClassTicketPreviewDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + public func registerTicket(code: String) async throws -> ClassTicket { + do { + let payload = try await client.send(ClassTicketService.register(code: code), as: ClassTicketDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + public func fetchMyTicket() async throws -> ClassTicket { + do { + let payload = try await client.send(ClassTicketService.me, as: ClassTicketDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + static func ticketError(_ error: Error) -> ClassTicketError { + if let error = error as? ClassTicketError { return error } + if let error = error as? PickeNetworkError { + if case let .response(response) = error { + switch response.code { + case "COMMON_400", "TICKET_404_CODE": return .invalidCode + case "TICKET_409_REVOKED": return .revoked + case "TICKET_409_REG_EXP": return .registrationExpired + case "TICKET_409_CLAIMED": return .claimed + case "TICKET_409_ACTIVE": return .activeTicket + case "TICKET_404": return .notFound + default: return .network(response.message ?? "이용권 정보를 불러오지 못했어요.") + } + } + } + return .network(error.localizedDescription) + } +} + +extension ClassRepositoryImpl { + public func fetchResult(roomID: Int) async throws -> ClassResult { + do { + let payload = try await client.send(ClassResultService.result(roomID), as: ClassResultDTO.self) + return try payload.toDomain() + } catch let error as PickeNetworkError { + if case let .response(response) = error { + throw Self.resultError(code: response.code, message: response.message) + } + throw ClassError.from(error) + } catch { + throw ClassError.from(error) + } + } + + public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { + do { + let payload = try await client.send(ClassResultService.myReport(roomID), as: ClassPersonalReportDTO.self) + return try payload.toDomain() + } catch { + throw Self.feedbackError(error) + } + } + + public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { + do { + let payload = try await client.send(ClassResultService.insight(roomID), as: ClassInsightDTO.self) + return try payload.toDomain() + } catch { + throw Self.feedbackError(error) + } + } + + public func retryInsight(roomID: Int) async throws { + do { + let payload = try await client.send(ClassResultService.retryInsight(roomID), as: ClassInsightRetryDTO.self) + guard payload.state == "PENDING" else { throw ClassAIFeedbackError.generationFailed } + } catch { + throw Self.feedbackError(error) + } + } + + static func resultError( + code: String?, + message: String? + ) -> ClassError { + switch code { + case "CLASS_409_RESULT_EXPIRED": return .resultExpired + case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied + default: return .network(message ?? "클래스 결과를 불러오지 못했어요.") + } + } + + static func feedbackError(code: String?) -> ClassAIFeedbackError { + switch code { + case "CLASS_409_REPORT_RETRY": return .generationFailed + case "CLASS_409_RESULT_EXPIRED": return .resultExpired + case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied + default: return .temporarilyUnavailable + } + } + + static func feedbackError(_ error: Error) -> ClassAIFeedbackError { + if let mapped = error as? ClassAIFeedbackError { return mapped } + if error is DecodingError { return .invalidResponse } + if let networkError = error as? PickeNetworkError { + switch networkError { + case .decoding: return .invalidResponse + case let .response(response): return feedbackError(code: response.code) + default: break + } + } + return .temporarilyUnavailable + } +} + +extension ClassRepositoryImpl { + public func fetchDebate(classID: Int) async throws -> ClassDebate { + try await debate(.fetch(classID)) + } + + public func startDebate(classID: Int) async throws -> ClassDebate { + try await debate(.start(classID)) + } + + public func retryDebate(classID: Int) async throws -> ClassDebate { + try await debate(.retry(classID)) + } + + public func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate { + try await debate(.submitReply(classID: classID, round: round, content: content, requestKey: requestKey)) + } + + private func debate(_ service: ClassDebateService) async throws -> ClassDebate { + do { + let payload = try await client.send(service, as: ClassDebateDTO.self) + return try payload.toDomain() + } catch let error as PickeNetworkError { + if case let .response(response) = error { + switch response.code { + case "CLASS_404_DEBATE": throw ClassAIChatError.noSession + case "CLASS_400_DEBATE_REPLY": throw ClassAIChatError.contentRejected + case "CLASS_409_DEBATE_STATE", "CLASS_409_DEBATE_ROUND", "CLASS_409_DEBATE_RETRY": + throw ClassAIChatError.temporarilyUnavailable + default: throw ClassAIChatError.unexpectedFailure + } + } + throw ClassAIChatError.temporarilyUnavailable + } + } + +} + +extension ClassRepositoryImpl { + public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.create(filter), as: ClassTopicDraftDTO.self).toDomain() + } + + public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.detail(id), as: ClassTopicDraftDTO.self).toDomain() + } + + public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.suggest(id, filter), as: ClassTopicDraftDTO.self).toDomain() + } + + public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { + try await sendTopicDraft( + ClassTopicDraftService.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID), + as: ClassTopicVersionDTO.self + ).toDomain() + } + + public func validateTopicDraft(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) async throws -> ClassTopicValidation { + try await sendTopicDraft( + ClassTopicDraftService.validate(draftID: draftID, baseVersionID: baseVersionID, candidate: candidate), + as: ClassTopicValidationDTO.self + ).toDomain() + } + + private func sendTopicDraft( + _ request: some PickeDataRequest, + as type: T.Type + ) async throws -> T { + do { + return try await client.send(request, as: type) + } catch let error as PickeNetworkError { + if case let .response(response) = error { + throw ClassError.network(response.message ?? "AI 주제 요청을 처리하지 못했어요.") + } + throw ClassError.from(error) + } catch { + throw ClassError.from(error) + } + } +} + +extension ClassRepositoryImpl { + public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + try await ClassAIChatFoundationEngine.shared.start(id: id, context: context) + return + } + #endif + throw ClassAIChatError.unavailable + } + + public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + return try await ClassAIChatFoundationEngine.shared.respond( + id: id, + text: text, + isFinalRound: isFinalRound + ) + } + #endif + throw ClassAIChatError.unavailable + } + + public func endAIChat(_ id: UUID) async { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + await ClassAIChatFoundationEngine.shared.end(id: id) + } + #endif + } + + public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + return try await ClassAIFeedbackFoundationEngine.generate(context: context) + } + #endif + throw ClassAIFeedbackError.deviceNotEligible + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassResultRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassResultRepositoryImpl.swift deleted file mode 100644 index 49aeb2b4..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassResultRepositoryImpl.swift +++ /dev/null @@ -1,83 +0,0 @@ -import ClassDomainInterface -import Dependencies -import PickeNetwork - -public struct ClassResultRepositoryImpl: ClassResultInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func fetch(roomID: Int) async throws -> ClassResult { - do { - let payload = try await client.send(ClassResultService.result(roomID), as: ClassResultDTO.self) - return try payload.toDomain() - } catch let error as PickeNetworkError { - if case let .response(response) = error { - throw Self.resultError(code: response.code, message: response.message) - } - throw ClassError.from(error) - } catch { - throw ClassError.from(error) - } - } - - public func fetchPersonal(roomID: Int) async throws -> ClassAIPersonalFeedback { - do { - let payload = try await client.send(ClassResultService.myReport(roomID), as: ClassPersonalReportDTO.self) - return try payload.toDomain() - } catch { - throw Self.feedbackError(error) - } - } - - public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { - do { - let payload = try await client.send(ClassResultService.insight(roomID), as: ClassInsightDTO.self) - return try payload.toDomain() - } catch { - throw Self.feedbackError(error) - } - } - - public func retryInsight(roomID: Int) async throws { - do { - let payload = try await client.send(ClassResultService.retryInsight(roomID), as: ClassInsightRetryDTO.self) - guard payload.state == "PENDING" else { throw ClassAIFeedbackError.generationFailed } - } catch { - throw Self.feedbackError(error) - } - } - - static func resultError( - code: String?, - message: String? - ) -> ClassError { - switch code { - case "CLASS_409_RESULT_EXPIRED": return .resultExpired - case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied - default: return .network(message ?? "클래스 결과를 불러오지 못했어요.") - } - } - - static func feedbackError(code: String?) -> ClassAIFeedbackError { - switch code { - case "CLASS_409_REPORT_RETRY": return .generationFailed - case "CLASS_409_RESULT_EXPIRED": return .resultExpired - case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied - default: return .temporarilyUnavailable - } - } - - static func feedbackError(_ error: Error) -> ClassAIFeedbackError { - if let mapped = error as? ClassAIFeedbackError { return mapped } - if error is DecodingError { return .invalidResponse } - if let networkError = error as? PickeNetworkError { - switch networkError { - case .decoding: return .invalidResponse - case let .response(response): return feedbackError(code: response.code) - default: break - } - } - return .temporarilyUnavailable - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassTicketRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassTicketRepositoryImpl.swift deleted file mode 100644 index eea34db9..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassTicketRepositoryImpl.swift +++ /dev/null @@ -1,55 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation -import PickeNetwork - -public struct ClassTicketRepositoryImpl: ClassTicketInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func preview(code: String) async throws -> ClassTicketPreview { - do { - let payload = try await client.send(ClassTicketService.preview(code: code), as: ClassTicketPreviewDTO.self) - return try payload.toDomain() - } catch { - throw Self.map(error) - } - } - - public func register(code: String) async throws -> ClassTicket { - do { - let payload = try await client.send(ClassTicketService.register(code: code), as: ClassTicketDTO.self) - return try payload.toDomain() - } catch { - throw Self.map(error) - } - } - - public func fetchMyTicket() async throws -> ClassTicket { - do { - let payload = try await client.send(ClassTicketService.me, as: ClassTicketDTO.self) - return try payload.toDomain() - } catch { - throw Self.map(error) - } - } - - static func map(_ error: Error) -> ClassTicketError { - if let error = error as? ClassTicketError { return error } - if let error = error as? PickeNetworkError { - if case let .response(response) = error { - switch response.code { - case "COMMON_400", "TICKET_404_CODE": return .invalidCode - case "TICKET_409_REVOKED": return .revoked - case "TICKET_409_REG_EXP": return .registrationExpired - case "TICKET_409_CLAIMED": return .claimed - case "TICKET_409_ACTIVE": return .activeTicket - case "TICKET_404": return .notFound - default: return .network(response.message ?? "이용권 정보를 불러오지 못했어요.") - } - } - } - return .network(error.localizedDescription) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassTopicDraftRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassTopicDraftRepositoryImpl.swift deleted file mode 100644 index f2a28513..00000000 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassTopicDraftRepositoryImpl.swift +++ /dev/null @@ -1,52 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation -import PickeNetwork - -public struct ClassTopicDraftRepositoryImpl: ClassTopicDraftInterface { - @Dependency(\.networkClient) private var client - - public init() {} - - public func create(filter: ClassTopicFilter) async throws -> ClassTopicDraft { - try await send(ClassTopicDraftService.create(filter), as: ClassTopicDraftDTO.self).toDomain() - } - - public func fetch(id: Int) async throws -> ClassTopicDraft { - try await send(ClassTopicDraftService.detail(id), as: ClassTopicDraftDTO.self).toDomain() - } - - public func suggest(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { - try await send(ClassTopicDraftService.suggest(id, filter), as: ClassTopicDraftDTO.self).toDomain() - } - - public func select(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { - try await send( - ClassTopicDraftService.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID), - as: ClassTopicVersionDTO.self - ).toDomain() - } - - public func validate(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) async throws -> ClassTopicValidation { - try await send( - ClassTopicDraftService.validate(draftID: draftID, baseVersionID: baseVersionID, candidate: candidate), - as: ClassTopicValidationDTO.self - ).toDomain() - } - - private func send( - _ request: some PickeDataRequest, - as type: T.Type - ) async throws -> T { - do { - return try await client.send(request, as: type) - } catch let error as PickeNetworkError { - if case let .response(response) = error { - throw ClassError.network(response.message ?? "AI 주제 요청을 처리하지 못했어요.") - } - throw ClassError.from(error) - } catch { - throw ClassError.from(error) - } - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassBattleService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassBattleService.swift deleted file mode 100644 index ff57bab0..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassBattleService.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Alamofire -import API -import Foundation -import PickeNetwork - -enum ClassBattleService { - case detail(classID: Int) - case preVote(classID: Int, optionID: Int) - case ttsComplete(classID: Int) - case postVote(classID: Int, optionID: Int) - case myVotes(classID: Int) - case voteStats(classID: Int) - case publishPerspective(classID: Int, content: String) - case perspectives(classID: Int, cursor: String?, size: Int?, optionID: Int?, sort: String?) - case myPerspectives(classID: Int) -} - -extension ClassBattleService: PickeDataRequest { - var domain: any PickeDomainType { ClassBattleDomain() } - - var path: String { - switch self { - case let .detail(classID): return "classes/\(classID)/battle" - case let .preVote(classID, _): return "classes/\(classID)/battle/votes/pre" - case let .ttsComplete(classID): return "classes/\(classID)/battle/votes/tts-complete" - case let .postVote(classID, _): return "classes/\(classID)/battle/votes/post" - case let .myVotes(classID): return "classes/\(classID)/battle/votes/me" - case let .voteStats(classID): return "classes/\(classID)/battle/vote-stats" - case let .publishPerspective(classID, _): return "classes/\(classID)/battle/perspectives" - case let .perspectives(classID, _, _, _, _): return "classes/\(classID)/battle/perspectives" - case let .myPerspectives(classID): return "classes/\(classID)/battle/perspectives/me" - } - } - - var method: HTTPMethod { - switch self { - case .detail, .myVotes, .voteStats, .perspectives, .myPerspectives: return .get - case .preVote, .ttsComplete, .postVote, .publishPerspective: return .post - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case let .preVote(_, optionID), let .postVote(_, optionID): - return VoteBody(optionId: optionID) - case let .publishPerspective(_, content): - return PerspectiveBody(content: content) - case let .perspectives(_, cursor, size, optionID, sort): - return PerspectivesQuery(cursor: cursor, size: size, optionId: optionID, sort: sort) - case .detail, .ttsComplete, .myVotes, .voteStats, .myPerspectives: - return nil - } - } -} - -private struct ClassBattleDomain: PickeDomainType { - var baseURLString: String { PieckeDomain.battle.baseURLString } - var url: String { "api/v1/" } -} - -private struct VoteBody: Encodable, Sendable { - let optionId: Int -} - -private struct PerspectiveBody: Encodable, Sendable { - let content: String -} - -private struct PerspectivesQuery: Encodable, Sendable { - let cursor: String? - let size: Int? - let optionId: Int? - let sort: String? -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassDebateService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassDebateService.swift deleted file mode 100644 index 9500fd78..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassDebateService.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Alamofire -import API -import Foundation -import PickeNetwork - -enum ClassDebateService { - case fetch(Int) - case start(Int) - case submitReply(classID: Int, round: Int, content: String, requestKey: UUID) - case retry(Int) -} - -extension ClassDebateService: PickeDataRequest { - var domain: any PickeDomainType { - ClassDebateDomain() - } - - var path: String { - switch self { - case let .fetch(id), let .start(id): return "classes/\(id)/battle/debate" - case let .submitReply(classID, _, _, _): return "classes/\(classID)/battle/debate/replies" - case let .retry(id): return "classes/\(id)/battle/debate/retry" - } - } - - var method: HTTPMethod { - switch self { - case .fetch: return .get - case .start, .submitReply, .retry: return .post - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case let .submitReply(_, round, content, requestKey): - return ReplyBody(roundIndex: round, content: content, requestKey: requestKey.uuidString) - case .fetch, .start, .retry: - return nil - } - } -} - -private struct ClassDebateDomain: PickeDomainType { - var baseURLString: String { - PieckeDomain.battle.baseURLString - } - - var url: String { - "api/v1/" - } -} - -private struct ReplyBody: Encodable, Sendable { - let roundIndex: Int - let content: String - let requestKey: String -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassMemberService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassMemberService.swift deleted file mode 100644 index 4bc34deb..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassMemberService.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Alamofire -import API -import Foundation -import PickeNetwork - -enum ClassMemberService { - case members(classID: Int) - case updateMyName(classID: Int, name: String) - case remove(classID: Int, memberID: Int) -} - -extension ClassMemberService: PickeDataRequest { - var domain: any PickeDomainType { - ClassMemberDomain() - } - - var path: String { - switch self { - case let .members(classID): return "classes/\(classID)/members" - case let .updateMyName(classID, _): return "classes/\(classID)/members/me" - case let .remove(classID, memberID): return "classes/\(classID)/members/\(memberID)" - } - } - - var method: HTTPMethod { - switch self { - case .members: return .get - case .updateMyName: return .patch - case .remove: return .delete - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case .members, .remove: return nil - case let .updateMyName(_, name): return ClassMemberNameBody(displayName: name) - } - } -} - -private struct ClassMemberDomain: PickeDomainType { - var baseURLString: String { - PieckeDomain.battle.baseURLString - } - - var url: String { - "api/v1/" - } -} - -private struct ClassMemberNameBody: Encodable, Sendable { - let displayName: String -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassResultService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassResultService.swift deleted file mode 100644 index a17cb75c..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassResultService.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Alamofire -import API -import PickeNetwork - -enum ClassResultService { - case result(Int) - case myReport(Int) - case insight(Int) - case retryInsight(Int) -} - -extension ClassResultService: PickeDataRequest { - var domain: any PickeDomainType { - ClassResultDomain() - } - - var path: String { - switch self { - case let .result(id): return "classes/\(id)/result" - case let .myReport(id): return "classes/\(id)/result/my-report" - case let .insight(id): return "classes/\(id)/result/insight" - case let .retryInsight(id): return "classes/\(id)/result/insight/retry" - } - } - - var method: HTTPMethod { - switch self { - case .result, .myReport, .insight: return .get - case .retryInsight: return .post - } - } - - var parameters: (any Encodable & Sendable)? { - nil - } -} - -private struct ClassResultDomain: PickeDomainType { - var baseURLString: String { - PieckeDomain.battle.baseURLString - } - - var url: String { - "api/v1/" - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassTicketService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassTicketService.swift deleted file mode 100644 index 6c91df78..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassTicketService.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Alamofire -import API -import Foundation -import PickeNetwork - -enum ClassTicketService { - case preview(code: String) - case register(code: String) - case me -} - -extension ClassTicketService: PickeDataRequest { - var domain: any PickeDomainType { ClassTicketDomain() } - - var path: String { - switch self { - case .preview: return "class-tickets/preview" - case .register: return "class-tickets/register" - case .me: return "class-tickets/me" - } - } - - var method: HTTPMethod { - switch self { - case .preview, .me: return .get - case .register: return .post - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case let .preview(code): return ClassTicketCodeRequest(code: code) - case let .register(code): return ClassTicketCodeRequest(code: code) - case .me: return nil - } - } -} - -private struct ClassTicketDomain: PickeDomainType { - var baseURLString: String { PieckeDomain.battle.baseURLString } - var url: String { "api/v1/" } -} - -private struct ClassTicketCodeRequest: Encodable, Sendable { - let code: String -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassTopicDraftService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassTopicDraftService.swift deleted file mode 100644 index 61f83be5..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassTopicDraftService.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Alamofire -import API -import ClassDomainInterface -import Foundation -import PickeNetwork - -enum ClassTopicDraftService { - case create(ClassTopicFilter) - case detail(Int) - case suggest(Int, ClassTopicFilter?) - case select(draftID: Int, batchNo: Int, candidateID: Int) - case validate(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) -} - -extension ClassTopicDraftService: PickeDataRequest { - var domain: any PickeDomainType { ClassTopicDraftDomain() } - - var path: String { - switch self { - case .create: return "class-topic-drafts" - case let .detail(id): return "class-topic-drafts/\(id)" - case let .suggest(id, _): return "class-topic-drafts/\(id)/suggestions" - case let .select(draftID, _, _): return "class-topic-drafts/\(draftID)/selection" - case let .validate(draftID, _, _): return "class-topic-drafts/\(draftID)/validations" - } - } - - var method: HTTPMethod { - switch self { - case .detail: return .get - case .create, .suggest, .select, .validate: return .post - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case let .create(filter): - return TopicFilterBody(filter: filter) - case .detail: - return nil - case let .suggest(_, filter): - return filter.map(TopicFilterBody.init) - case let .select(_, batchNo, candidateID): - return TopicSelectionBody(batchNo: batchNo, candidateId: candidateID) - case let .validate(_, baseVersionID, candidate): - return TopicValidationBody(baseVersionId: baseVersionID, candidate: candidate) - } - } -} - -private struct ClassTopicDraftDomain: PickeDomainType { - var baseURLString: String { PieckeDomain.battle.baseURLString } - var url: String { "api/v1/" } -} - -private struct TopicFilterBody: Encodable, Sendable { - let topicSeed: String - let difficulty: String - let categories: [String] - - init(filter: ClassTopicFilter) { - topicSeed = filter.keyword.trimmingCharacters(in: .whitespacesAndNewlines) - difficulty = filter.level.topicAPIValue - categories = filter.categories.reduce(into: [String]()) { titles, category in - if !titles.contains(category.title) { titles.append(category.title) } - } - } -} - -private struct TopicSelectionBody: Encodable, Sendable { - let batchNo: Int - let candidateId: Int -} - -private struct TopicValidationBody: Encodable, Sendable { - struct Option: Encodable, Sendable { - let label: String - let rationale: String - let philosopher: String? - } - - let baseVersionId: Int - let question: String - let description: String - let options: [Option] - - init( - baseVersionId: Int, - candidate: ClassTopicCandidate - ) { - self.baseVersionId = baseVersionId - question = candidate.question - description = candidate.description - options = candidate.options.map { - Option(label: $0.label, rationale: $0.rationale, philosopher: $0.philosopher) - } - } -} - -private extension ClassAudienceLevel { - var topicAPIValue: String { - switch self { - case .middle: return "BEGINNER" - case .high: return "INTERMEDIATE" - case .adult: return "ADVANCED" - } - } -} diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassroomService.swift b/Projects/Domain/ClassDomain/Sources/Service/ClassroomService.swift deleted file mode 100644 index d436aa02..00000000 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassroomService.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Alamofire -import API -import ClassDomainInterface -import Foundation -import PickeCoreUtility -import PickeNetwork - -enum ClassroomService { - case myClasses - case create(ClassCreation) - case detail(Int) - case updateDeadline(id: Int, deadline: Date) - case delete(Int) - case joinPreview(String) - case join(code: String, nickname: String) - case leave(Int) -} - -extension ClassroomService: PickeDataRequest { - var domain: any PickeDomainType { ClassroomDomain() } - - var path: String { - switch self { - case .myClasses, .create: - return "classes" - case let .detail(id), let .delete(id), let .updateDeadline(id, _): - return "classes/\(id)" - case .joinPreview: - return "classes/join/preview" - case .join: - return "classes/join" - case let .leave(id): - return "classes/\(id)/members/me" - } - } - - var method: HTTPMethod { - switch self { - case .myClasses, .detail, .joinPreview: return .get - case .create, .join: return .post - case .updateDeadline: return .patch - case .delete, .leave: return .delete - } - } - - var parameters: (any Encodable & Sendable)? { - switch self { - case let .create(creation): - return CreateClassBody(creation: creation) - case let .updateDeadline(_, deadline): - return UpdateClassBody(deadline: deadline.formatted(.serverDateTime)) - case let .joinPreview(code): - return ClassJoinPreviewQuery(inviteCode: code) - case let .join(code, nickname): - return JoinClassBody(inviteCode: code, displayName: nickname) - case .myClasses, .detail, .delete, .leave: - return nil - } - } -} - -private struct ClassroomDomain: PickeDomainType { - var baseURLString: String { PieckeDomain.battle.baseURLString } - var url: String { "api/v1/" } -} - -private struct CreateClassBody: Encodable, Sendable { - let name: String - let battleId: Int? - let topicVersionId: Int? - let deadline: String? - let commentRequired: Bool - let ownerParticipates: Bool - - init(creation: ClassCreation) { - name = creation.name - battleId = creation.battleId - topicVersionId = creation.topicVersionId - deadline = creation.deadline == .distantFuture - ? nil - : creation.deadline.formatted(.serverDateTime) - commentRequired = creation.requiresComment - ownerParticipates = creation.ownerParticipates - } -} - -private struct UpdateClassBody: Encodable, Sendable { - let deadline: String -} - -private struct ClassJoinPreviewQuery: Encodable, Sendable { - let inviteCode: String -} - -private struct JoinClassBody: Encodable, Sendable { - let inviteCode: String - let displayName: String -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIChatUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIChatUseCaseImpl.swift deleted file mode 100644 index 774531a5..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIChatUseCaseImpl.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ClassAIChatUseCaseImpl.swift -// ClassDomain -// -// Created by Picke on 10/11/26. -// - -import ClassDomainInterface -import Foundation - -public struct ClassAIChatUseCaseImpl: ClassAIChatInterface { - public init() {} - - public func start(_ id: UUID, _ context: ClassAIChatContext) async throws { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - try await ClassAIChatFoundationEngine.shared.start(id: id, context: context) - return - } - #endif - throw ClassAIChatError.unavailable - } - - public func respond( - _ id: UUID, - _ text: String, - isFinalRound: Bool - ) async throws -> String { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - return try await ClassAIChatFoundationEngine.shared.respond( - id: id, - text: text, - isFinalRound: isFinalRound - ) - } - #endif - throw ClassAIChatError.unavailable - } - - public func end(_ id: UUID) async { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - await ClassAIChatFoundationEngine.shared.end(id: id) - } - #endif - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIFeedbackUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIFeedbackUseCaseImpl.swift deleted file mode 100644 index 0d640b12..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassAIFeedbackUseCaseImpl.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ClassAIFeedbackUseCaseImpl.swift -// ClassDomain -// -// Created by Picke on 10/12/26. -// - -import ClassDomainInterface - -public struct ClassAIFeedbackUseCaseImpl: ClassAIFeedbackInterface { - public init() {} - - public func generate(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - return try await ClassAIFeedbackFoundationEngine.generate(context: context) - } - #endif - throw ClassAIFeedbackError.deviceNotEligible - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassBattleUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassBattleUseCaseImpl.swift deleted file mode 100644 index 8f68e36f..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassBattleUseCaseImpl.swift +++ /dev/null @@ -1,66 +0,0 @@ -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassBattleUseCaseImpl: ClassBattleInterface { - @Dependency(\.classBattleRepository) private var repository - - public init() {} - - public func fetchBattle(classID: Int) async throws -> ClassBattleSession { - try await repository.fetchBattle(classID: classID) - } - - public func submitPreVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - try await repository.submitPreVote(classID: classID, optionID: optionID) - } - - public func completeTTS(classID: Int) async throws { - try await repository.completeTTS(classID: classID) - } - - public func submitPostVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - try await repository.submitPostVote(classID: classID, optionID: optionID) - } - - public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { - try await repository.fetchMyVotes(classID: classID) - } - - public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { - try await repository.fetchVoteStats(classID: classID) - } - - public func publishPerspective( - classID: Int, - content: String - ) async throws -> ClassPerspectiveReceipt { - try await repository.publishPerspective(classID: classID, content: content) - } - - public func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage { - try await repository.fetchPerspectives( - classID: classID, - cursor: cursor, - size: size, - optionID: optionID, - sort: sort - ) - } - - public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { - try await repository.fetchMyPerspectives(classID: classID) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassDebateUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassDebateUseCaseImpl.swift deleted file mode 100644 index 7976c43d..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassDebateUseCaseImpl.swift +++ /dev/null @@ -1,35 +0,0 @@ -import ClassDomainInterface -import Dependencies -import Foundation - -public struct ClassDebateUseCaseImpl: ClassDebateInterface { - @Dependency(\.classAIRepository) private var repository - - public init() {} - - public func fetch(classID: Int) async throws -> ClassDebate { - try await repository.fetchDebate(classID: classID) - } - - public func start(classID: Int) async throws -> ClassDebate { - try await repository.startDebate(classID: classID) - } - - public func submitReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate { - try await repository.submitDebateReply( - classID: classID, - round: round, - content: content, - requestKey: requestKey - ) - } - - public func retry(classID: Int) async throws -> ClassDebate { - try await repository.retryDebate(classID: classID) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassMemberUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassMemberUseCaseImpl.swift deleted file mode 100644 index 969f1ea6..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassMemberUseCaseImpl.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassMemberUseCaseImpl: ClassMemberInterface { - @Dependency(\.classMemberRepository) private var repository - - public init() {} - - public func fetchMembers(classID: Int) async throws -> ClassMemberList { - try await repository.fetchMembers(classID: classID) - } - - public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { - try await repository.updateMyDisplayName(classID: classID, name: name) - } - - public func removeMember(classID: Int, memberID: Int) async throws { - try await repository.removeMember(classID: classID, memberID: memberID) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassResultUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassResultUseCaseImpl.swift deleted file mode 100644 index 4afb50c9..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassResultUseCaseImpl.swift +++ /dev/null @@ -1,24 +0,0 @@ -import ClassDomainInterface -import Dependencies - -public struct ClassResultUseCaseImpl: ClassResultInterface { - @Dependency(\.classResultRepository) private var repository - - public init() {} - - public func fetch(roomID: Int) async throws -> ClassResult { - try await repository.fetch(roomID: roomID) - } - - public func fetchPersonal(roomID: Int) async throws -> ClassAIPersonalFeedback { - try await repository.fetchPersonal(roomID: roomID) - } - - public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { - try await repository.fetchInsight(roomID: roomID) - } - - public func retryInsight(roomID: Int) async throws { - try await repository.retryInsight(roomID: roomID) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassTicketUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassTicketUseCaseImpl.swift deleted file mode 100644 index 286276a9..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassTicketUseCaseImpl.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassTicketUseCaseImpl: ClassTicketInterface { - @Dependency(\.classTicketRepository) private var repository - - public init() {} - - public func preview(code: String) async throws -> ClassTicketPreview { - try await repository.preview(code: code) - } - - public func register(code: String) async throws -> ClassTicket { - try await repository.register(code: code) - } - - public func fetchMyTicket() async throws -> ClassTicket { - try await repository.fetchMyTicket() - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassTopicDraftUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassTopicDraftUseCaseImpl.swift deleted file mode 100644 index 6d38842b..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassTopicDraftUseCaseImpl.swift +++ /dev/null @@ -1,40 +0,0 @@ -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassTopicDraftUseCaseImpl: ClassTopicDraftInterface { - @Dependency(\.classTopicDraftRepository) private var repository - - public init() {} - - public func create(filter: ClassTopicFilter) async throws -> ClassTopicDraft { - try await repository.create(filter: filter) - } - - public func fetch(id: Int) async throws -> ClassTopicDraft { - try await repository.fetch(id: id) - } - - public func suggest( - id: Int, - filter: ClassTopicFilter? - ) async throws -> ClassTopicDraft { - try await repository.suggest(id: id, filter: filter) - } - - public func select( - draftID: Int, - batchNo: Int, - candidateID: Int - ) async throws -> ClassTopicVersion { - try await repository.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID) - } - - public func validate( - draftID: Int, - baseVersionID: Int, - candidate: ClassTopicCandidate - ) async throws -> ClassTopicValidation { - try await repository.validate(draftID: draftID, baseVersionID: baseVersionID, candidate: candidate) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift new file mode 100644 index 00000000..7e837974 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift @@ -0,0 +1,224 @@ +// +// ClassUseCase.swift +// ClassDomain +// +// Created by Picke on 10/11/26. +// + +import ClassDomainInterface +import ComposableArchitecture +import Foundation + +public struct ClassUseCaseImpl: ClassInterface { + @Dependency(\.classRepository) private var repository + + public init() {} + + public func fetchMyClasses() async throws -> [ClassRoom] { + try await repository.fetchMyClasses() + } + + public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { + try await repository.fetchRecommendedBattles(filter: filter) + } + + public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { + try await repository.createClass(creation) + } + + public func fetchClass(id: Int) async throws -> ClassRoom { + try await repository.fetchClass(id: id) + } + + public func fetchClass(joinCode: String) async throws -> ClassRoom { + try await repository.fetchClass(joinCode: joinCode) + } + + public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { + try await repository.fetchClassPreview(joinCode: joinCode) + } + + public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { + try await repository.joinClass(joinCode: joinCode, nickname: nickname) + } + + public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { + try await repository.updateDeadline(id: id, deadline: deadline) + } + + public func deleteClass(id: Int) async throws { + try await repository.deleteClass(id: id) + } + + public func leaveClass(id: Int) async throws { + try await repository.leaveClass(id: id) + } + + public func fetchReport(roomID: Int) async throws -> ClassReport? { + try await repository.fetchReport(roomID: roomID) + } + + public func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? { + try await repository.fetchOwnerDashboard(roomID: roomID) + } +} + +extension ClassUseCaseImpl { + public func fetchBattle(classID: Int) async throws -> ClassBattleSession { + try await repository.fetchBattle(classID: classID) + } + + public func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + try await repository.submitPreVote(classID: classID, optionID: optionID) + } + + public func completeTTS(classID: Int) async throws { + try await repository.completeTTS(classID: classID) + } + + public func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + try await repository.submitPostVote(classID: classID, optionID: optionID) + } + + public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { + try await repository.fetchMyVotes(classID: classID) + } + + public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { + try await repository.fetchVoteStats(classID: classID) + } + + public func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt { + try await repository.publishPerspective(classID: classID, content: content) + } + + public func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage { + try await repository.fetchPerspectives( + classID: classID, + cursor: cursor, + size: size, + optionID: optionID, + sort: sort + ) + } + + public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { + try await repository.fetchMyPerspectives(classID: classID) + } + + public func fetchMembers(classID: Int) async throws -> ClassMemberList { + try await repository.fetchMembers(classID: classID) + } + + public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { + try await repository.updateMyDisplayName(classID: classID, name: name) + } + + public func removeMember(classID: Int, memberID: Int) async throws { + try await repository.removeMember(classID: classID, memberID: memberID) + } + + public func fetchDebate(classID: Int) async throws -> ClassDebate { + try await repository.fetchDebate(classID: classID) + } + + public func startDebate(classID: Int) async throws -> ClassDebate { + try await repository.startDebate(classID: classID) + } + + public func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate { + try await repository.submitDebateReply( + classID: classID, + round: round, + content: content, + requestKey: requestKey + ) + } + + public func retryDebate(classID: Int) async throws -> ClassDebate { + try await repository.retryDebate(classID: classID) + } + + public func previewTicket(code: String) async throws -> ClassTicketPreview { + try await repository.previewTicket(code: code) + } + + public func registerTicket(code: String) async throws -> ClassTicket { + try await repository.registerTicket(code: code) + } + + public func fetchMyTicket() async throws -> ClassTicket { + try await repository.fetchMyTicket() + } + + public func fetchResult(roomID: Int) async throws -> ClassResult { + try await repository.fetchResult(roomID: roomID) + } + + public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { + try await repository.fetchPersonalResult(roomID: roomID) + } + + public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { + try await repository.fetchInsight(roomID: roomID) + } + + public func retryInsight(roomID: Int) async throws { + try await repository.retryInsight(roomID: roomID) + } + + public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { + try await repository.createTopicDraft(filter: filter) + } + + public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { + try await repository.fetchTopicDraft(id: id) + } + + public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { + try await repository.suggestTopicDraft(id: id, filter: filter) + } + + public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { + try await repository.selectTopicDraft(draftID: draftID, batchNo: batchNo, candidateID: candidateID) + } + + public func validateTopicDraft( + draftID: Int, + baseVersionID: Int, + candidate: ClassTopicCandidate + ) async throws -> ClassTopicValidation { + try await repository.validateTopicDraft( + draftID: draftID, + baseVersionID: baseVersionID, + candidate: candidate + ) + } + + public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { + try await repository.startAIChat(id, context) + } + + public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { + try await repository.respondAIChat(id, text, isFinalRound: isFinalRound) + } + + public func endAIChat(_ id: UUID) async { + await repository.endAIChat(id) + } + + public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { + try await repository.generateAIFeedback(context: context) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift deleted file mode 100644 index 23eed939..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ClassUseCaseImpl.swift -// ClassDomain -// -// Created by Picke on 10/11/26. -// - -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassUseCaseImpl: ClassInterface { - @Dependency(\.classRepository) private var repository - - public init() {} - - public func fetchMyClasses() async throws -> [ClassRoom] { - try await repository.fetchMyClasses() - } - - public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { - try await repository.fetchRecommendedBattles(filter: filter) - } - - public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { - try await repository.createClass(creation) - } - - public func fetchClass(id: Int) async throws -> ClassRoom { - try await repository.fetchClass(id: id) - } - - public func fetchClass(joinCode: String) async throws -> ClassRoom { - try await repository.fetchClass(joinCode: joinCode) - } - - public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { - try await repository.fetchClassPreview(joinCode: joinCode) - } - - public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { - try await repository.joinClass(joinCode: joinCode, nickname: nickname) - } - - public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { - try await repository.updateDeadline(id: id, deadline: deadline) - } - - public func deleteClass(id: Int) async throws { - try await repository.deleteClass(id: id) - } - - public func leaveClass(id: Int) async throws { - try await repository.leaveClass(id: id) - } - - public func fetchReport(roomID: Int) async throws -> ClassReport? { - try await repository.fetchReport(roomID: roomID) - } - - public func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? { - try await repository.fetchOwnerDashboard(roomID: roomID) - } -} diff --git a/Projects/Feature/Class/Sources/ClassAIChat/Reducer/ClassAIChatFeature.swift b/Projects/Feature/Class/Sources/ClassAIChat/Reducer/ClassAIChatFeature.swift index 54aa59d2..698eeb0f 100644 --- a/Projects/Feature/Class/Sources/ClassAIChat/Reducer/ClassAIChatFeature.swift +++ b/Projects/Feature/Class/Sources/ClassAIChat/Reducer/ClassAIChatFeature.swift @@ -168,7 +168,7 @@ public struct ClassAIChatFeature { case response } - @Dependency(\.classDebateUseCase) private var debateUseCase + @Dependency(\.classUseCase) private var debateUseCase public var body: some Reducer { Reduce { state, action in @@ -214,18 +214,18 @@ extension ClassAIChatFeature { do { let initial: ClassDebate if retryServerGeneration { - initial = try await debateUseCase.retry(classID: roomID) + initial = try await debateUseCase.retryDebate(classID: roomID) } else { do { - initial = try await debateUseCase.fetch(classID: roomID) + initial = try await debateUseCase.fetchDebate(classID: roomID) } catch ClassAIChatError.noSession { - initial = try await debateUseCase.start(classID: roomID) + initial = try await debateUseCase.startDebate(classID: roomID) } } var debate = initial while debate.status == .generating, !debate.canRetry, !Task.isCancelled { try await Task.sleep(for: .seconds(2)) - debate = try await debateUseCase.fetch(classID: roomID) + debate = try await debateUseCase.fetchDebate(classID: roomID) } guard !Task.isCancelled else { return } await send(.inner(.debateLoaded(requestID, .success(debate)))) @@ -298,7 +298,7 @@ extension ClassAIChatFeature { let round = debate.round return .run { send in do { - var result = try await debateUseCase.submitReply( + var result = try await debateUseCase.submitDebateReply( classID: roomID, round: round, content: text, @@ -306,7 +306,7 @@ extension ClassAIChatFeature { ) while result.status == .generating, !result.canRetry, !Task.isCancelled { try await Task.sleep(for: .seconds(2)) - result = try await debateUseCase.fetch(classID: roomID) + result = try await debateUseCase.fetchDebate(classID: roomID) } guard !Task.isCancelled else { return } await send(.inner(.replyLoaded(requestID, .success(result)))) diff --git a/Projects/Feature/Class/Sources/ClassAIVote/Reducer/ClassAIVoteFeature.swift b/Projects/Feature/Class/Sources/ClassAIVote/Reducer/ClassAIVoteFeature.swift index 52c5d12b..ea9f1235 100644 --- a/Projects/Feature/Class/Sources/ClassAIVote/Reducer/ClassAIVoteFeature.swift +++ b/Projects/Feature/Class/Sources/ClassAIVote/Reducer/ClassAIVoteFeature.swift @@ -176,7 +176,7 @@ public struct ClassAIVoteFeature { @Dependency(\.uuid) private var uuid @Dependency(\.battleUseCase) private var battleUseCase - @Dependency(\.classBattleUseCase) private var classBattleUseCase + @Dependency(\.classUseCase) private var classUseCase public var body: some Reducer { Reduce { state, action in @@ -253,7 +253,7 @@ extension ClassAIVoteFeature { let classOptionID: Int? = state.isLiveClassBattle ? state.classOptionIDs[optionID - 1] : nil return .run { [ battleUseCase, - classBattleUseCase, + classUseCase, instanceID = state.instanceID, battleID = state.battleID, classID = state.room.id, @@ -262,9 +262,9 @@ extension ClassAIVoteFeature { let result = await Result { if let classOptionID { let receipt = if isPostVote { - try await classBattleUseCase.submitPostVote(classID: classID, optionID: classOptionID) + try await classUseCase.submitPostVote(classID: classID, optionID: classOptionID) } else { - try await classBattleUseCase.submitPreVote(classID: classID, optionID: classOptionID) + try await classUseCase.submitPreVote(classID: classID, optionID: classOptionID) } return PreVoteResult(voteId: receipt.voteID, status: .created) } @@ -289,7 +289,7 @@ extension ClassAIVoteFeature { state.errorMessage = nil return .run { [ battleUseCase, - classBattleUseCase, + classUseCase, instanceID = state.instanceID, battleID = state.battleID, classID = state.room.id, @@ -297,10 +297,10 @@ extension ClassAIVoteFeature { ] send in let result = await Result { if isLiveClassBattle { - let session = try await classBattleUseCase.fetchBattle(classID: classID) + let session = try await classUseCase.fetchBattle(classID: classID) let history: ClassVoteHistory? do { - history = try await classBattleUseCase.fetchMyVotes(classID: classID) + history = try await classUseCase.fetchMyVotes(classID: classID) } catch ClassBattleError.voteNotFound { history = nil } diff --git a/Projects/Feature/Class/Sources/ClassBattleFlow/Reducer/ClassBattleFlowFeature.swift b/Projects/Feature/Class/Sources/ClassBattleFlow/Reducer/ClassBattleFlowFeature.swift index dcd1c531..b1452489 100644 --- a/Projects/Feature/Class/Sources/ClassBattleFlow/Reducer/ClassBattleFlowFeature.swift +++ b/Projects/Feature/Class/Sources/ClassBattleFlow/Reducer/ClassBattleFlowFeature.swift @@ -177,7 +177,7 @@ public struct ClassBattleFlowFeature { } @Dependency(\.uuid) private var uuid - @Dependency(\.classBattleUseCase) private var classBattleUseCase + @Dependency(\.classUseCase) private var classUseCase @Dependency(\.battleUseCase) private var battleUseCase @Dependency(\.audioPlayer) private var audioPlayer @@ -271,7 +271,7 @@ extension ClassBattleFlowFeature { let optionID = state.selectedOptionID let preVoteOptionID = state.voteHistory?.preVote.optionID let draft = state.draft.trimmingCharacters(in: .whitespacesAndNewlines) - return .run { [useCase = classBattleUseCase] send in + return .run { [useCase = classUseCase] send in let result = await Result { switch step { case .none: @@ -330,7 +330,7 @@ extension ClassBattleFlowFeature { state.loadRequestID = requestID state.viewState = .loading state.errorMessage = nil - return .run { [useCase = classBattleUseCase] send in + return .run { [useCase = classUseCase] send in let result = await Result { let session = try await useCase.fetchBattle(classID: classID) let history = session.battle.currentStep == .none @@ -461,7 +461,7 @@ extension ClassBattleFlowFeature { state.nextPerspectiveCursor = nil state.perspectives = [] } - return .run { [useCase = classBattleUseCase] send in + return .run { [useCase = classUseCase] send in let result = await Result { try await useCase.fetchPerspectives( classID: classID, diff --git a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift index 36da9947..d7bda912 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/Reducer/ClassJoinFeature.swift @@ -155,7 +155,6 @@ public struct ClassJoinFeature { } @Dependency(\.classUseCase) private var classUseCase - @Dependency(\.classMemberUseCase) private var classMemberUseCase @Dependency(\.uuid) private var uuid public var body: some Reducer { @@ -288,9 +287,9 @@ extension ClassJoinFeature { state.activeOwnerRequestID = requestID state.errorMessage = nil state.nicknameErrorMessage = nil - return .run { [classMemberUseCase, room = state.preview] send in + return .run { [classUseCase, room = state.preview] send in let result = await Result { () async throws -> ClassRoom in - _ = try await classMemberUseCase.updateMyDisplayName(classID: roomID, name: nickname) + _ = try await classUseCase.updateMyDisplayName(classID: roomID, name: nickname) guard let room else { throw ClassError.invalidCode } return room } diff --git a/Projects/Feature/Class/Sources/ClassMyOpinions/Reducer/ClassMyOpinionsFeature.swift b/Projects/Feature/Class/Sources/ClassMyOpinions/Reducer/ClassMyOpinionsFeature.swift index 272ef0fd..c9c6e3c4 100644 --- a/Projects/Feature/Class/Sources/ClassMyOpinions/Reducer/ClassMyOpinionsFeature.swift +++ b/Projects/Feature/Class/Sources/ClassMyOpinions/Reducer/ClassMyOpinionsFeature.swift @@ -74,7 +74,7 @@ public struct ClassMyOpinionsFeature { case fetch } - @Dependency(\.classBattleUseCase) private var classBattleUseCase + @Dependency(\.classUseCase) private var classUseCase public var body: some Reducer { Reduce { state, action in @@ -91,7 +91,7 @@ public struct ClassMyOpinionsFeature { guard let classID = state.classID else { return .none } state.isLoading = true state.errorMessage = nil - return .run { [useCase = classBattleUseCase] send in + return .run { [useCase = classUseCase] send in let result = await Result { try await useCase.fetchMyPerspectives(classID: classID) } .mapError { ($0 as? ClassBattleError) ?? .network($0.localizedDescription) } await send(.inner(.perspectivesResponse(result))) diff --git a/Projects/Feature/Class/Sources/ClassOwnerDashboard/Reducer/ClassOwnerDashboardFeature.swift b/Projects/Feature/Class/Sources/ClassOwnerDashboard/Reducer/ClassOwnerDashboardFeature.swift index 81db4533..3e87f44c 100644 --- a/Projects/Feature/Class/Sources/ClassOwnerDashboard/Reducer/ClassOwnerDashboardFeature.swift +++ b/Projects/Feature/Class/Sources/ClassOwnerDashboard/Reducer/ClassOwnerDashboardFeature.swift @@ -376,7 +376,7 @@ public struct ClassOwnerDashboardFeature { case openFeedback(ClassOwnerMember) } - @Dependency(\.classResultUseCase) private var classResultUseCase + @Dependency(\.classUseCase) private var classUseCase @Dependency(\.uuid) private var uuid public var body: some Reducer { @@ -495,12 +495,12 @@ extension ClassOwnerDashboardFeature { state.requestID = requestID state.sourceViewState = .loading let fetch = Effect.run { [ - classResultUseCase, + classUseCase, instanceID = state.instanceID, roomID = state.room.id ] send in let result = await Result { - try await classResultUseCase.fetch(roomID: roomID) + try await classUseCase.fetchResult(roomID: roomID) } .mapError(ClassError.from) await send(.inner(.resultResponse( @@ -522,14 +522,14 @@ extension ClassOwnerDashboardFeature { state.insightRequestID = requestID state.insightError = nil state.isGeneratingInsight = true - return .run { [classResultUseCase, instanceID = state.instanceID, roomID = state.room.id] send in + return .run { [classUseCase, instanceID = state.instanceID, roomID = state.room.id] send in var wasUnknown = false var wasCancelled = false let result = await Result { if retryServerGeneration { - try await classResultUseCase.retryInsight(roomID: roomID) + try await classUseCase.retryInsight(roomID: roomID) } - return try await ClassAIFeedbackResult.serverInsight(classResultUseCase.fetchInsight(roomID: roomID)) + return try await ClassAIFeedbackResult.serverInsight(classUseCase.fetchInsight(roomID: roomID)) } .mapError { error in wasCancelled = error is CancellationError @@ -551,11 +551,11 @@ extension ClassOwnerDashboardFeature { state.feedbackRequestID = requestID state.feedbackError = nil state.isGeneratingFeedback = true - return .run { [classResultUseCase, instanceID = state.instanceID, roomID = state.room.id] send in + return .run { [classUseCase, instanceID = state.instanceID, roomID = state.room.id] send in var wasUnknown = false var wasCancelled = false let result = await Result { - try await ClassAIFeedbackResult.personal(classResultUseCase.fetchPersonal(roomID: roomID)) + try await ClassAIFeedbackResult.personal(classUseCase.fetchPersonalResult(roomID: roomID)) } .mapError { error in wasCancelled = error is CancellationError diff --git a/Projects/Feature/Class/Sources/ClassRecommend/Reducer/ClassRecommendFeature.swift b/Projects/Feature/Class/Sources/ClassRecommend/Reducer/ClassRecommendFeature.swift index e67dcecb..0472d4fa 100644 --- a/Projects/Feature/Class/Sources/ClassRecommend/Reducer/ClassRecommendFeature.swift +++ b/Projects/Feature/Class/Sources/ClassRecommend/Reducer/ClassRecommendFeature.swift @@ -133,7 +133,6 @@ public struct ClassRecommendFeature { } @Dependency(\.classUseCase) private var classUseCase - @Dependency(\.classTopicDraftUseCase) private var topicDraftUseCase public var body: some Reducer { Reduce { state, action in @@ -255,9 +254,9 @@ extension ClassRecommendFeature { case let .createDraft(filter): state.isLoading = true state.viewState = .loading - return .run { [topicDraftUseCase] send in + return .run { [classUseCase] send in let result = await Result { - try await topicDraftUseCase.create(filter: filter) + try await classUseCase.createTopicDraft(filter: filter) } .mapError(ClassError.from) guard !Task.isCancelled else { return } @@ -268,12 +267,12 @@ extension ClassRecommendFeature { case let .suggest(draftID): state.isLoading = true state.viewState = .loading - return .run { [topicDraftUseCase] send in + return .run { [classUseCase] send in let result = await Result { - let draft = try await topicDraftUseCase.suggest(id: draftID, filter: nil) + let draft = try await classUseCase.suggestTopicDraft(id: draftID, filter: nil) guard !Task.isCancelled else { throw CancellationError() } await send(.inner(.suggestionStarted(draft))) - return try await Self.pollSuggestion(draft: draft, useCase: topicDraftUseCase) + return try await Self.pollSuggestion(draft: draft, useCase: classUseCase) } .mapError(ClassError.from) guard !Task.isCancelled else { return } @@ -284,10 +283,10 @@ extension ClassRecommendFeature { case let .poll(draftID): state.isLoading = true state.viewState = .loading - return .run { [topicDraftUseCase] send in + return .run { [classUseCase] send in let result = await Result { - let draft = try await topicDraftUseCase.fetch(id: draftID) - return try await Self.pollSuggestion(draft: draft, useCase: topicDraftUseCase) + let draft = try await classUseCase.fetchTopicDraft(id: draftID) + return try await Self.pollSuggestion(draft: draft, useCase: classUseCase) } .mapError(ClassError.from) guard !Task.isCancelled else { return } @@ -297,9 +296,9 @@ extension ClassRecommendFeature { case let .select(draftID, batchNo, candidateID): state.isLoading = true - return .run { [topicDraftUseCase] send in + return .run { [classUseCase] send in let result = await Result { - try await topicDraftUseCase.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID) + try await classUseCase.selectTopicDraft(draftID: draftID, batchNo: batchNo, candidateID: candidateID) } .mapError(ClassError.from) guard !Task.isCancelled else { return } @@ -369,12 +368,12 @@ extension ClassRecommendFeature { private static func pollSuggestion( draft: ClassTopicDraft, - useCase: any ClassTopicDraftInterface + useCase: any ClassInterface ) async throws -> ClassTopicDraft { var current = draft for _ in 0 ..< 90 where current.suggestStatus == "GENERATING" { try await Task.sleep(for: .seconds(2)) - current = try await useCase.fetch(id: draft.id) + current = try await useCase.fetchTopicDraft(id: draft.id) } guard current.suggestStatus != "GENERATING" else { throw ClassError.unknown("AI 주제 생성 시간이 초과됐어요. 다시 확인해 주세요.") diff --git a/Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift b/Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift index 41b0d079..a63fa5b3 100644 --- a/Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift +++ b/Projects/Feature/Class/Sources/ClassReport/Reducer/ClassReportFeature.swift @@ -170,7 +170,7 @@ public struct ClassReportFeature { case generate(UUID) } - @Dependency(\.classResultUseCase) private var classResultUseCase + @Dependency(\.classUseCase) private var classUseCase @Dependency(\.uuid) private var uuid public var body: some Reducer { @@ -244,12 +244,12 @@ extension ClassReportFeature { state.requestID = requestID state.sourceViewState = .loading let fetch = Effect.run { [ - classResultUseCase, + classUseCase, instanceID = state.instanceID, roomID = state.room.id ] send in let result = await Result { - try await classResultUseCase.fetch(roomID: roomID) + try await classUseCase.fetchResult(roomID: roomID) } .mapError(ClassError.from) await send(.inner(.resultResponse( @@ -266,11 +266,11 @@ extension ClassReportFeature { state.feedbackRequestID = requestID state.feedbackError = nil state.isGeneratingFeedback = true - return .run { [classResultUseCase, instanceID = state.instanceID, roomID = state.room.id] send in + return .run { [classUseCase, instanceID = state.instanceID, roomID = state.room.id] send in var wasUnknown = false var wasCancelled = false let result = await Result { - try await ClassAIFeedbackResult.personal(classResultUseCase.fetchPersonal(roomID: roomID)) + try await ClassAIFeedbackResult.personal(classUseCase.fetchPersonalResult(roomID: roomID)) } .mapError { error in wasCancelled = error is CancellationError diff --git a/Projects/Feature/Class/Sources/ClassTicket/Reducer/ClassTicketFeature.swift b/Projects/Feature/Class/Sources/ClassTicket/Reducer/ClassTicketFeature.swift index 6afb6a2e..eb8123c8 100644 --- a/Projects/Feature/Class/Sources/ClassTicket/Reducer/ClassTicketFeature.swift +++ b/Projects/Feature/Class/Sources/ClassTicket/Reducer/ClassTicketFeature.swift @@ -113,7 +113,7 @@ public struct ClassTicketFeature { case existingTicket(UUID) } - @Dependency(\.classTicketUseCase) private var ticketUseCase + @Dependency(\.classUseCase) private var ticketUseCase public var body: some Reducer { BindingReducer() @@ -158,7 +158,7 @@ public struct ClassTicketFeature { let submittedCode = state.code.trimmingCharacters(in: .whitespacesAndNewlines) return .run { [ticketUseCase] send in let result: Result = await Result { - try await ticketUseCase.register(code: submittedCode) + try await ticketUseCase.registerTicket(code: submittedCode) } .mapError(Self.mapError) await send(.registrationResponse(result)) @@ -190,7 +190,7 @@ public struct ClassTicketFeature { let submittedCode = state.code.trimmingCharacters(in: .whitespacesAndNewlines) return .run { [ticketUseCase] send in let result: Result = await Result { - try await ticketUseCase.preview(code: submittedCode) + try await ticketUseCase.previewTicket(code: submittedCode) } .mapError(Self.mapError) await send(.checkResponse(result)) diff --git a/Projects/Service/API/Sources/Base/PieckeDomain.swift b/Projects/Service/API/Sources/Base/PieckeDomain.swift index 88c40bb5..51ff7046 100644 --- a/Projects/Service/API/Sources/Base/PieckeDomain.swift +++ b/Projects/Service/API/Sources/Base/PieckeDomain.swift @@ -22,6 +22,7 @@ public enum PieckeDomain { case notification case device case ads + case classroom } extension PieckeDomain: PickeDomainType { @@ -55,6 +56,8 @@ extension PieckeDomain: PickeDomainType { return "api/v1/devices" case .ads: return "api/v1/ads" + case .classroom: + return "api/v1/" } } } diff --git a/Projects/Service/API/Sources/Class/ClassBattleAPI.swift b/Projects/Service/API/Sources/Class/ClassBattleAPI.swift new file mode 100644 index 00000000..2aefadcf --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassBattleAPI.swift @@ -0,0 +1,23 @@ +public enum ClassBattleAPI { + case detail(classID: Int) + case preVote(classID: Int) + case ttsComplete(classID: Int) + case postVote(classID: Int) + case myVotes(classID: Int) + case voteStats(classID: Int) + case perspectives(classID: Int) + case myPerspectives(classID: Int) + + public var description: String { + switch self { + case let .detail(id): return "classes/\(id)/battle" + case let .preVote(id): return "classes/\(id)/battle/votes/pre" + case let .ttsComplete(id): return "classes/\(id)/battle/votes/tts-complete" + case let .postVote(id): return "classes/\(id)/battle/votes/post" + case let .myVotes(id): return "classes/\(id)/battle/votes/me" + case let .voteStats(id): return "classes/\(id)/battle/vote-stats" + case let .perspectives(id): return "classes/\(id)/battle/perspectives" + case let .myPerspectives(id): return "classes/\(id)/battle/perspectives/me" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift b/Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift new file mode 100644 index 00000000..d9a2caca --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift @@ -0,0 +1,9 @@ +public enum ClassBattleRecommendationAPI { + case recommendations + + public var description: String { + switch self { + case .recommendations: return "class-battle-recommendations" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassDebateAPI.swift b/Projects/Service/API/Sources/Class/ClassDebateAPI.swift new file mode 100644 index 00000000..f9808d76 --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassDebateAPI.swift @@ -0,0 +1,13 @@ +public enum ClassDebateAPI { + case debate(classID: Int) + case replies(classID: Int) + case retry(classID: Int) + + public var description: String { + switch self { + case let .debate(id): return "classes/\(id)/battle/debate" + case let .replies(id): return "classes/\(id)/battle/debate/replies" + case let .retry(id): return "classes/\(id)/battle/debate/retry" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassMemberAPI.swift b/Projects/Service/API/Sources/Class/ClassMemberAPI.swift new file mode 100644 index 00000000..9a3365ef --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassMemberAPI.swift @@ -0,0 +1,13 @@ +public enum ClassMemberAPI { + case members(classID: Int) + case me(classID: Int) + case member(classID: Int, memberID: Int) + + public var description: String { + switch self { + case let .members(id): return "classes/\(id)/members" + case let .me(id): return "classes/\(id)/members/me" + case let .member(id, memberID): return "classes/\(id)/members/\(memberID)" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassResultAPI.swift b/Projects/Service/API/Sources/Class/ClassResultAPI.swift new file mode 100644 index 00000000..62b20f2d --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassResultAPI.swift @@ -0,0 +1,15 @@ +public enum ClassResultAPI { + case result(classID: Int) + case myReport(classID: Int) + case insight(classID: Int) + case retryInsight(classID: Int) + + public var description: String { + switch self { + case let .result(id): return "classes/\(id)/result" + case let .myReport(id): return "classes/\(id)/result/my-report" + case let .insight(id): return "classes/\(id)/result/insight" + case let .retryInsight(id): return "classes/\(id)/result/insight/retry" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassTicketAPI.swift b/Projects/Service/API/Sources/Class/ClassTicketAPI.swift new file mode 100644 index 00000000..7d0e908a --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassTicketAPI.swift @@ -0,0 +1,13 @@ +public enum ClassTicketAPI { + case preview + case register + case me + + public var description: String { + switch self { + case .preview: return "class-tickets/preview" + case .register: return "class-tickets/register" + case .me: return "class-tickets/me" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift b/Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift new file mode 100644 index 00000000..c201330d --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift @@ -0,0 +1,17 @@ +public enum ClassTopicDraftAPI { + case drafts + case detail(draftID: Int) + case suggestions(draftID: Int) + case selection(draftID: Int) + case validations(draftID: Int) + + public var description: String { + switch self { + case .drafts: return "class-topic-drafts" + case let .detail(id): return "class-topic-drafts/\(id)" + case let .suggestions(id): return "class-topic-drafts/\(id)/suggestions" + case let .selection(id): return "class-topic-drafts/\(id)/selection" + case let .validations(id): return "class-topic-drafts/\(id)/validations" + } + } +} diff --git a/Projects/Service/API/Sources/Class/ClassroomAPI.swift b/Projects/Service/API/Sources/Class/ClassroomAPI.swift new file mode 100644 index 00000000..746119ef --- /dev/null +++ b/Projects/Service/API/Sources/Class/ClassroomAPI.swift @@ -0,0 +1,17 @@ +public enum ClassroomAPI { + case classes + case detail(classID: Int) + case joinPreview + case join + case leave(classID: Int) + + public var description: String { + switch self { + case .classes: return "classes" + case let .detail(id): return "classes/\(id)" + case .joinPreview: return "classes/join/preview" + case .join: return "classes/join" + case let .leave(id): return "classes/\(id)/members/me" + } + } +} diff --git a/Projects/Service/APIEndpoint/Project.swift b/Projects/Service/APIEndpoint/Project.swift index 4750f95e..df5a4d0d 100644 --- a/Projects/Service/APIEndpoint/Project.swift +++ b/Projects/Service/APIEndpoint/Project.swift @@ -24,7 +24,9 @@ let project = Project.makeModule( .service(.api), // Auth 엔드포인트가 SocialType 을 경로에 쓴다. .domain(.auth, .interface), + .domain(.classroom, .interface), + .core(.coreUtility), .core(.network, .interface), ], hasTests: true -) \ No newline at end of file +) diff --git a/Projects/Domain/ClassDomain/Sources/Service/ClassBattleRecommendationService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassBattleRecommendationService.swift similarity index 56% rename from Projects/Domain/ClassDomain/Sources/Service/ClassBattleRecommendationService.swift rename to Projects/Service/APIEndpoint/Sources/Class/ClassBattleRecommendationService.swift index 2bd9d028..223dd3cb 100644 --- a/Projects/Domain/ClassDomain/Sources/Service/ClassBattleRecommendationService.swift +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassBattleRecommendationService.swift @@ -4,24 +4,24 @@ import ClassDomainInterface import Foundation import PickeNetwork -enum ClassBattleRecommendationService { +public enum ClassBattleRecommendationService { case recommendations(filter: ClassTopicFilter) } extension ClassBattleRecommendationService: PickeDataRequest { - var domain: any PickeDomainType { - ClassBattleRecommendationDomain() + public var domain: any PickeDomainType { + PieckeDomain.classroom } - var path: String { - "class-battle-recommendations" + public var path: String { + ClassBattleRecommendationAPI.recommendations.description } - var method: HTTPMethod { + public var method: HTTPMethod { .get } - var parameters: (any Encodable & Sendable)? { + public var parameters: (any Encodable & Sendable)? { switch self { case let .recommendations(filter): return ClassBattleRecommendationQuery( @@ -33,22 +33,6 @@ extension ClassBattleRecommendationService: PickeDataRequest { } } -private struct ClassBattleRecommendationDomain: PickeDomainType { - var baseURLString: String { - PieckeDomain.battle.baseURLString - } - - var url: String { - "api/v1/" - } -} - -private struct ClassBattleRecommendationQuery: Encodable, Sendable { - let difficulty: String - let categories: String - let keyword: String? -} - private extension ClassAudienceLevel { var apiValue: String { switch self { diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift new file mode 100644 index 00000000..ef3c7455 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift @@ -0,0 +1,54 @@ +import Alamofire +import API +import Foundation +import PickeNetwork + +public enum ClassBattleService { + case detail(classID: Int) + case preVote(classID: Int, optionID: Int) + case ttsComplete(classID: Int) + case postVote(classID: Int, optionID: Int) + case myVotes(classID: Int) + case voteStats(classID: Int) + case publishPerspective(classID: Int, content: String) + case perspectives(classID: Int, cursor: String?, size: Int?, optionID: Int?, sort: String?) + case myPerspectives(classID: Int) +} + +extension ClassBattleService: PickeDataRequest { + public var domain: any PickeDomainType { PieckeDomain.classroom } + + public var path: String { + switch self { + case let .detail(classID): return ClassBattleAPI.detail(classID: classID).description + case let .preVote(classID, _): return ClassBattleAPI.preVote(classID: classID).description + case let .ttsComplete(classID): return ClassBattleAPI.ttsComplete(classID: classID).description + case let .postVote(classID, _): return ClassBattleAPI.postVote(classID: classID).description + case let .myVotes(classID): return ClassBattleAPI.myVotes(classID: classID).description + case let .voteStats(classID): return ClassBattleAPI.voteStats(classID: classID).description + case let .publishPerspective(classID, _), let .perspectives(classID, _, _, _, _): + return ClassBattleAPI.perspectives(classID: classID).description + case let .myPerspectives(classID): return ClassBattleAPI.myPerspectives(classID: classID).description + } + } + + public var method: HTTPMethod { + switch self { + case .detail, .myVotes, .voteStats, .perspectives, .myPerspectives: return .get + case .preVote, .ttsComplete, .postVote, .publishPerspective: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .preVote(_, optionID), let .postVote(_, optionID): + return VoteBody(optionId: optionID) + case let .publishPerspective(_, content): + return PerspectiveBody(content: content) + case let .perspectives(_, cursor, size, optionID, sort): + return PerspectivesQuery(cursor: cursor, size: size, optionId: optionID, sort: sort) + case .detail, .ttsComplete, .myVotes, .voteStats, .myPerspectives: + return nil + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift new file mode 100644 index 00000000..7f6843f0 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift @@ -0,0 +1,41 @@ +import Alamofire +import API +import Foundation +import PickeNetwork + +public enum ClassDebateService { + case fetch(Int) + case start(Int) + case submitReply(classID: Int, round: Int, content: String, requestKey: UUID) + case retry(Int) +} + +extension ClassDebateService: PickeDataRequest { + public var domain: any PickeDomainType { + PieckeDomain.classroom + } + + public var path: String { + switch self { + case let .fetch(id), let .start(id): return ClassDebateAPI.debate(classID: id).description + case let .submitReply(classID, _, _, _): return ClassDebateAPI.replies(classID: classID).description + case let .retry(id): return ClassDebateAPI.retry(classID: id).description + } + } + + public var method: HTTPMethod { + switch self { + case .fetch: return .get + case .start, .submitReply, .retry: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .submitReply(_, round, content, requestKey): + return ReplyBody(roundIndex: round, content: content, requestKey: requestKey.uuidString) + case .fetch, .start, .retry: + return nil + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift new file mode 100644 index 00000000..3b7cb90d --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift @@ -0,0 +1,40 @@ +import Alamofire +import API +import Foundation +import PickeNetwork + +public enum ClassMemberService { + case members(classID: Int) + case updateMyName(classID: Int, name: String) + case remove(classID: Int, memberID: Int) +} + +extension ClassMemberService: PickeDataRequest { + public var domain: any PickeDomainType { + PieckeDomain.classroom + } + + public var path: String { + switch self { + case let .members(classID): return ClassMemberAPI.members(classID: classID).description + case let .updateMyName(classID, _): return ClassMemberAPI.me(classID: classID).description + case let .remove(classID, memberID): + return ClassMemberAPI.member(classID: classID, memberID: memberID).description + } + } + + public var method: HTTPMethod { + switch self { + case .members: return .get + case .updateMyName: return .patch + case .remove: return .delete + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case .members, .remove: return nil + case let .updateMyName(_, name): return ClassMemberNameBody(displayName: name) + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift new file mode 100644 index 00000000..d282d4a7 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift @@ -0,0 +1,36 @@ +import Alamofire +import API +import PickeNetwork + +public enum ClassResultService { + case result(Int) + case myReport(Int) + case insight(Int) + case retryInsight(Int) +} + +extension ClassResultService: PickeDataRequest { + public var domain: any PickeDomainType { + PieckeDomain.classroom + } + + public var path: String { + switch self { + case let .result(id): return ClassResultAPI.result(classID: id).description + case let .myReport(id): return ClassResultAPI.myReport(classID: id).description + case let .insight(id): return ClassResultAPI.insight(classID: id).description + case let .retryInsight(id): return ClassResultAPI.retryInsight(classID: id).description + } + } + + public var method: HTTPMethod { + switch self { + case .result, .myReport, .insight: return .get + case .retryInsight: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + nil + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift new file mode 100644 index 00000000..096fde6e --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift @@ -0,0 +1,37 @@ +import Alamofire +import API +import Foundation +import PickeNetwork + +public enum ClassTicketService { + case preview(code: String) + case register(code: String) + case me +} + +extension ClassTicketService: PickeDataRequest { + public var domain: any PickeDomainType { PieckeDomain.classroom } + + public var path: String { + switch self { + case .preview: return ClassTicketAPI.preview.description + case .register: return ClassTicketAPI.register.description + case .me: return ClassTicketAPI.me.description + } + } + + public var method: HTTPMethod { + switch self { + case .preview, .me: return .get + case .register: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .preview(code): return ClassTicketCodeRequest(code: code) + case let .register(code): return ClassTicketCodeRequest(code: code) + case .me: return nil + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift new file mode 100644 index 00000000..476eaace --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift @@ -0,0 +1,49 @@ +import Alamofire +import API +import ClassDomainInterface +import Foundation +import PickeNetwork + +public enum ClassTopicDraftService { + case create(ClassTopicFilter) + case detail(Int) + case suggest(Int, ClassTopicFilter?) + case select(draftID: Int, batchNo: Int, candidateID: Int) + case validate(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) +} + +extension ClassTopicDraftService: PickeDataRequest { + public var domain: any PickeDomainType { PieckeDomain.classroom } + + public var path: String { + switch self { + case .create: return ClassTopicDraftAPI.drafts.description + case let .detail(id): return ClassTopicDraftAPI.detail(draftID: id).description + case let .suggest(id, _): return ClassTopicDraftAPI.suggestions(draftID: id).description + case let .select(draftID, _, _): return ClassTopicDraftAPI.selection(draftID: draftID).description + case let .validate(draftID, _, _): return ClassTopicDraftAPI.validations(draftID: draftID).description + } + } + + public var method: HTTPMethod { + switch self { + case .detail: return .get + case .create, .suggest, .select, .validate: return .post + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .create(filter): + return TopicFilterBody(filter: filter) + case .detail: + return nil + case let .suggest(_, filter): + return filter.map(TopicFilterBody.init) + case let .select(_, batchNo, candidateID): + return TopicSelectionBody(batchNo: batchNo, candidateId: candidateID) + case let .validate(_, baseVersionID, candidate): + return TopicValidationBody(baseVersionId: baseVersionID, candidate: candidate) + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift b/Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift new file mode 100644 index 00000000..6ab0e499 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift @@ -0,0 +1,60 @@ +import Alamofire +import API +import ClassDomainInterface +import Foundation +import PickeCoreUtility +import PickeNetwork + +public enum ClassroomService { + case myClasses + case create(ClassCreation) + case detail(Int) + case updateDeadline(id: Int, deadline: Date) + case delete(Int) + case joinPreview(String) + case join(code: String, nickname: String) + case leave(Int) +} + +extension ClassroomService: PickeDataRequest { + public var domain: any PickeDomainType { PieckeDomain.classroom } + + public var path: String { + switch self { + case .myClasses, .create: + return ClassroomAPI.classes.description + case let .detail(id), let .delete(id), let .updateDeadline(id, _): + return ClassroomAPI.detail(classID: id).description + case .joinPreview: + return ClassroomAPI.joinPreview.description + case .join: + return ClassroomAPI.join.description + case let .leave(id): + return ClassroomAPI.leave(classID: id).description + } + } + + public var method: HTTPMethod { + switch self { + case .myClasses, .detail, .joinPreview: return .get + case .create, .join: return .post + case .updateDeadline: return .patch + case .delete, .leave: return .delete + } + } + + public var parameters: (any Encodable & Sendable)? { + switch self { + case let .create(creation): + return CreateClassBody(creation: creation) + case let .updateDeadline(_, deadline): + return UpdateClassBody(deadline: deadline.formatted(.serverDateTime)) + case let .joinPreview(code): + return ClassJoinPreviewQuery(inviteCode: code) + case let .join(code, nickname): + return JoinClassBody(inviteCode: code, displayName: nickname) + case .myClasses, .detail, .delete, .leave: + return nil + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift new file mode 100644 index 00000000..492f787d --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift @@ -0,0 +1,8 @@ +import Foundation +import ClassDomainInterface + +struct ClassBattleRecommendationQuery: Encodable, Sendable { + let difficulty: String + let categories: String + let keyword: String? +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift new file mode 100644 index 00000000..d46892c6 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift @@ -0,0 +1,16 @@ +import Foundation + +struct VoteBody: Encodable, Sendable { + let optionId: Int +} + +struct PerspectiveBody: Encodable, Sendable { + let content: String +} + +struct PerspectivesQuery: Encodable, Sendable { + let cursor: String? + let size: Int? + let optionId: Int? + let sort: String? +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift new file mode 100644 index 00000000..d2e9cdfa --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift @@ -0,0 +1,7 @@ +import Foundation + +struct ReplyBody: Encodable, Sendable { + let roundIndex: Int + let content: String + let requestKey: String +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift new file mode 100644 index 00000000..cb71c36e --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift @@ -0,0 +1,5 @@ +import Foundation + +struct ClassMemberNameBody: Encodable, Sendable { + let displayName: String +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift new file mode 100644 index 00000000..f6b6abbf --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift @@ -0,0 +1,5 @@ +import Foundation + +struct ClassTicketCodeRequest: Encodable, Sendable { + let code: String +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift new file mode 100644 index 00000000..890c6938 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift @@ -0,0 +1,56 @@ +import Foundation +import ClassDomainInterface + +struct TopicFilterBody: Encodable, Sendable { + let topicSeed: String + let difficulty: String + let categories: [String] + + init(filter: ClassTopicFilter) { + topicSeed = filter.keyword.trimmingCharacters(in: .whitespacesAndNewlines) + difficulty = filter.level.topicAPIValue + categories = filter.categories.reduce(into: [String]()) { titles, category in + if !titles.contains(category.title) { titles.append(category.title) } + } + } +} + +struct TopicSelectionBody: Encodable, Sendable { + let batchNo: Int + let candidateId: Int +} + +struct TopicValidationBody: Encodable, Sendable { + struct Option: Encodable, Sendable { + let label: String + let rationale: String + let philosopher: String? + } + + let baseVersionId: Int + let question: String + let description: String + let options: [Option] + + init( + baseVersionId: Int, + candidate: ClassTopicCandidate + ) { + self.baseVersionId = baseVersionId + question = candidate.question + description = candidate.description + options = candidate.options.map { + Option(label: $0.label, rationale: $0.rationale, philosopher: $0.philosopher) + } + } +} + +extension ClassAudienceLevel { + var topicAPIValue: String { + switch self { + case .middle: return "BEGINNER" + case .high: return "INTERMEDIATE" + case .adult: return "ADVANCED" + } + } +} diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift new file mode 100644 index 00000000..f6b51a28 --- /dev/null +++ b/Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift @@ -0,0 +1,36 @@ +import Foundation +import ClassDomainInterface +import PickeCoreUtility + +struct CreateClassBody: Encodable, Sendable { + let name: String + let battleId: Int? + let topicVersionId: Int? + let deadline: String? + let commentRequired: Bool + let ownerParticipates: Bool + + init(creation: ClassCreation) { + name = creation.name + battleId = creation.battleId + topicVersionId = creation.topicVersionId + deadline = creation.deadline == .distantFuture + ? nil + : creation.deadline.formatted(.serverDateTime) + commentRequired = creation.requiresComment + ownerParticipates = creation.ownerParticipates + } +} + +struct UpdateClassBody: Encodable, Sendable { + let deadline: String +} + +struct ClassJoinPreviewQuery: Encodable, Sendable { + let inviteCode: String +} + +struct JoinClassBody: Encodable, Sendable { + let inviteCode: String + let displayName: String +} From 980b9804c9a0eae57783642e379e1ba13432797a Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:07:21 +0900 Subject: [PATCH 04/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=9D=84=20=EC=97=AD=ED=95=A0=EB=B3=84=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClassRepositoryImpl과 ClassUseCaseImpl 타입은 각각 하나로 유지하고 메서드를 역할별 파일로 배치합니다. --- .../ClassRepositoryImpl+Battle.swift | 130 +++++ .../ClassRepositoryImpl+Debate.swift | 46 ++ .../ClassRepositoryImpl+LocalAI.swift | 44 ++ .../ClassRepositoryImpl+Member.swift | 51 ++ .../ClassRepositoryImpl+Recommendation.swift | 25 + .../ClassRepositoryImpl+Report.swift | 15 + .../ClassRepositoryImpl+Result.swift | 80 +++ .../Repository/ClassRepositoryImpl+Room.swift | 99 ++++ .../ClassRepositoryImpl+Ticket.swift | 51 ++ .../ClassRepositoryImpl+TopicDraft.swift | 48 ++ .../Repository/ClassRepositoryImpl.swift | 546 +----------------- .../Sources/UseCase/ClassUseCase.swift | 224 ------- .../UseCase/ClassUseCaseImpl+Battle.swift | 52 ++ .../UseCase/ClassUseCaseImpl+Debate.swift | 30 + .../UseCase/ClassUseCaseImpl+LocalAI.swift | 20 + .../UseCase/ClassUseCaseImpl+Member.swift | 16 + .../ClassUseCaseImpl+Recommendation.swift | 8 + .../UseCase/ClassUseCaseImpl+Report.swift | 12 + .../UseCase/ClassUseCaseImpl+Result.swift | 20 + .../UseCase/ClassUseCaseImpl+Room.swift | 40 ++ .../UseCase/ClassUseCaseImpl+Ticket.swift | 16 + .../UseCase/ClassUseCaseImpl+TopicDraft.swift | 32 + .../Sources/UseCase/ClassUseCaseImpl.swift | 17 + 23 files changed, 853 insertions(+), 769 deletions(-) create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift create mode 100644 Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift delete mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift create mode 100644 Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift new file mode 100644 index 00000000..9da08b0a --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift @@ -0,0 +1,130 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchBattle(classID: Int) async throws -> ClassBattleSession { + do { + let payload = try await client.send(ClassBattleService.detail(classID: classID), as: ClassBattleSessionDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func submitPreVote( + classID: Int, + optionID: Int + ) async throws -> ClassVoteReceipt { + do { + let payload = try await client.send( + ClassBattleService.preVote(classID: classID, optionID: optionID), + as: ClassVoteReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func completeTTS(classID: Int) async throws { + do { + _ = try await client.send( + ClassBattleService.ttsComplete(classID: classID), + as: PickeEmptyResponse.self + ) + } catch { throw Self.battleError(error) } + } + + public func submitPostVote( + classID: Int, + optionID: Int + ) async throws -> ClassVoteReceipt { + do { + let payload = try await client.send( + ClassBattleService.postVote(classID: classID, optionID: optionID), + as: ClassVoteReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { + do { + let payload = try await client.send(ClassBattleService.myVotes(classID: classID), as: ClassVoteHistoryDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { + do { + let payload = try await client.send(ClassBattleService.voteStats(classID: classID), as: ClassVoteStatsDTO.self) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func publishPerspective( + classID: Int, + content: String + ) async throws -> ClassPerspectiveReceipt { + do { + let payload = try await client.send( + ClassBattleService.publishPerspective(classID: classID, content: content), + as: ClassPerspectiveReceiptDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage { + do { + let payload = try await client.send( + ClassBattleService.perspectives( + classID: classID, + cursor: cursor, + size: size, + optionID: optionID, + sort: sort + ), + as: ClassPerspectivePageDTO.self + ) + return try payload.toDomain() + } catch { throw Self.battleError(error) } + } + + public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { + do { + let payload = try await client.send( + ClassBattleService.myPerspectives(classID: classID), + as: [ClassPerspectiveItemDTO].self + ) + return try payload.map { try $0.toDomain() } + } catch { throw Self.battleError(error) } + } + + private static func battleError(_ error: Error) -> ClassBattleError { + if let error = error as? ClassBattleError { return error } + if let error = error as? PickeNetworkError, + case let .response(response) = error + { + switch response.code { + case "CLASS_404": return .classNotFound + case "CLASS_403_MEMBER": return .membershipRequired + case "CLASS_409_TICKET_ENDED": return .ticketEnded + case "CLASS_409_CLOSED": return .closed + case "CLASS_409_PRE_VOTE_LOCKED": return .preVoteLocked + case "BATTLE_OPT_404": return .invalidOption + case "CLASS_409_TTS": return .ttsUnavailable + case "VOTE_404": return .voteNotFound + case "VOTE_409_PRE": return .preVoteRequired + case "VOTE_409_SUB": return .alreadySubmitted + case "CLASS_409_DEBATE_INCOMPLETE": return .debateIncomplete + default: return .network(response.message ?? "클래스 배틀 요청을 처리하지 못했어요.") + } + } + return .network(error.localizedDescription) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift new file mode 100644 index 00000000..5ccc3592 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift @@ -0,0 +1,46 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchDebate(classID: Int) async throws -> ClassDebate { + try await debate(.fetch(classID)) + } + + public func startDebate(classID: Int) async throws -> ClassDebate { + try await debate(.start(classID)) + } + + public func retryDebate(classID: Int) async throws -> ClassDebate { + try await debate(.retry(classID)) + } + + public func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate { + try await debate(.submitReply(classID: classID, round: round, content: content, requestKey: requestKey)) + } + + private func debate(_ service: ClassDebateService) async throws -> ClassDebate { + do { + let payload = try await client.send(service, as: ClassDebateDTO.self) + return try payload.toDomain() + } catch let error as PickeNetworkError { + if case let .response(response) = error { + switch response.code { + case "CLASS_404_DEBATE": throw ClassAIChatError.noSession + case "CLASS_400_DEBATE_REPLY": throw ClassAIChatError.contentRejected + case "CLASS_409_DEBATE_STATE", "CLASS_409_DEBATE_ROUND", "CLASS_409_DEBATE_RETRY": + throw ClassAIChatError.temporarilyUnavailable + default: throw ClassAIChatError.unexpectedFailure + } + } + throw ClassAIChatError.temporarilyUnavailable + } + } + +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift new file mode 100644 index 00000000..0bb3487e --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift @@ -0,0 +1,44 @@ +import ClassDomainInterface +import Foundation + +extension ClassRepositoryImpl { + public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + try await ClassAIChatFoundationEngine.shared.start(id: id, context: context) + return + } + #endif + throw ClassAIChatError.unavailable + } + + public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + return try await ClassAIChatFoundationEngine.shared.respond( + id: id, + text: text, + isFinalRound: isFinalRound + ) + } + #endif + throw ClassAIChatError.unavailable + } + + public func endAIChat(_ id: UUID) async { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + await ClassAIChatFoundationEngine.shared.end(id: id) + } + #endif + } + + public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + return try await ClassAIFeedbackFoundationEngine.generate(context: context) + } + #endif + throw ClassAIFeedbackError.deviceNotEligible + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift new file mode 100644 index 00000000..7cec54c4 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift @@ -0,0 +1,51 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchMembers(classID: Int) async throws -> ClassMemberList { + do { + let data = try await client.send( + ClassMemberService.members(classID: classID), + as: [ClassMemberDTO].self + ) + return try ClassMemberDTO.toMemberList(data) + } catch { + throw Self.memberError(error) + } + } + + public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { + do { + let data = try await client.send( + ClassMemberService.updateMyName(classID: classID, name: name), + as: ClassMemberDTO.self + ) + return data.toDomain() + } catch { + throw Self.memberError(error) + } + } + + public func removeMember(classID: Int, memberID: Int) async throws { + do { + _ = try await client.send( + ClassMemberService.remove(classID: classID, memberID: memberID), + as: PickeEmptyResponse.self + ) + } catch { + throw Self.memberError(error) + } + } + + private static func memberError(_ error: Error) -> ClassError { + guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } + guard case let .response(response) = networkError else { return ClassError.from(networkError) } + return ClassErrorMapper.map( + code: response.code, + message: response.message, + fallback: "멤버 요청을 처리하지 못했어요." + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift new file mode 100644 index 00000000..8033af76 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift @@ -0,0 +1,25 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { + guard (1 ... 3).contains(filter.categories.count), filter.keyword.count <= 50 else { + throw ClassError.unknown("추천 조건을 확인해 주세요.") + } + do { + let data = try await client.send( + ClassBattleRecommendationService.recommendations(filter: filter), + as: ClassBattleRecommendationDataDTO.self + ) + return try data.items.map { try $0.toDomain() } + } catch let error as PickeNetworkError { + if case let .response(response) = error { + throw ClassError.network(response.message ?? "배틀 추천을 불러오지 못했습니다.") + } + throw ClassError.from(error) + } + } + +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift new file mode 100644 index 00000000..db38ac30 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift @@ -0,0 +1,15 @@ +import ClassDomainInterface +import Foundation + +extension ClassRepositoryImpl { + private static let unavailableMessage = "클래스 서버 API가 연결되지 않았습니다." + + public func fetchReport(roomID _: Int) async throws -> ClassReport? { + throw ClassError.network(Self.unavailableMessage) + } + + public func fetchOwnerDashboard(roomID _: Int) async throws -> ClassOwnerDashboardData? { + throw ClassError.network(Self.unavailableMessage) + } + +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift new file mode 100644 index 00000000..29879042 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift @@ -0,0 +1,80 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchResult(roomID: Int) async throws -> ClassResult { + do { + let payload = try await client.send(ClassResultService.result(roomID), as: ClassResultDTO.self) + return try payload.toDomain() + } catch let error as PickeNetworkError { + if case let .response(response) = error { + throw Self.resultError(code: response.code, message: response.message) + } + throw ClassError.from(error) + } catch { + throw ClassError.from(error) + } + } + + public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { + do { + let payload = try await client.send(ClassResultService.myReport(roomID), as: ClassPersonalReportDTO.self) + return try payload.toDomain() + } catch { + throw Self.feedbackError(error) + } + } + + public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { + do { + let payload = try await client.send(ClassResultService.insight(roomID), as: ClassInsightDTO.self) + return try payload.toDomain() + } catch { + throw Self.feedbackError(error) + } + } + + public func retryInsight(roomID: Int) async throws { + do { + let payload = try await client.send(ClassResultService.retryInsight(roomID), as: ClassInsightRetryDTO.self) + guard payload.state == "PENDING" else { throw ClassAIFeedbackError.generationFailed } + } catch { + throw Self.feedbackError(error) + } + } + + static func resultError( + code: String?, + message: String? + ) -> ClassError { + switch code { + case "CLASS_409_RESULT_EXPIRED": return .resultExpired + case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied + default: return .network(message ?? "클래스 결과를 불러오지 못했어요.") + } + } + + static func feedbackError(code: String?) -> ClassAIFeedbackError { + switch code { + case "CLASS_409_REPORT_RETRY": return .generationFailed + case "CLASS_409_RESULT_EXPIRED": return .resultExpired + case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied + default: return .temporarilyUnavailable + } + } + + static func feedbackError(_ error: Error) -> ClassAIFeedbackError { + if let mapped = error as? ClassAIFeedbackError { return mapped } + if error is DecodingError { return .invalidResponse } + if let networkError = error as? PickeNetworkError { + switch networkError { + case .decoding: return .invalidResponse + case let .response(response): return feedbackError(code: response.code) + default: break + } + } + return .temporarilyUnavailable + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift new file mode 100644 index 00000000..5ae7e527 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift @@ -0,0 +1,99 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func fetchMyClasses() async throws -> [ClassRoom] { + do { + let data = try await client.send(ClassroomService.myClasses, as: [ClassListItemDTO].self) + return try data.map { try $0.toDomain() } + } catch { + throw Self.classError(error) + } + } + + public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { + do { + let data = try await client.send(ClassroomService.create(creation), as: ClassDetailDataDTO.self) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + + public func fetchClass(id: Int) async throws -> ClassRoom { + do { + let data = try await client.send(ClassroomService.detail(id), as: ClassDetailDataDTO.self) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + + public func fetchClass(joinCode _: String) async throws -> ClassRoom { + throw ClassError.unknown("참여 미리보기는 클래스 상세를 제공하지 않습니다.") + } + + public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { + do { + let data = try await client.send( + ClassroomService.joinPreview(joinCode), + as: ClassJoinPreviewDataDTO.self + ) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + + public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { + do { + let data = try await client.send( + ClassroomService.join(code: joinCode, nickname: nickname), + as: ClassDetailDataDTO.self + ) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + + public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { + do { + _ = try await client.send( + ClassroomService.updateDeadline(id: id, deadline: deadline), + as: PickeEmptyResponse.self + ) + let data = try await client.send(ClassroomService.detail(id), as: ClassDetailDataDTO.self) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + + public func deleteClass(id: Int) async throws { + do { + _ = try await client.send(ClassroomService.delete(id), as: PickeEmptyResponse.self) + } catch { + throw Self.classError(error) + } + } + + public func leaveClass(id: Int) async throws { + do { + _ = try await client.send(ClassroomService.leave(id), as: PickeEmptyResponse.self) + } catch { + throw Self.classError(error) + } + } + + private static func classError(_ error: Error) -> ClassError { + guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } + guard case let .response(response) = networkError else { return ClassError.from(networkError) } + return ClassErrorMapper.map( + code: response.code, + message: response.message, + fallback: "클래스 요청을 처리하지 못했습니다." + ) + }} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift new file mode 100644 index 00000000..9642e6d5 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift @@ -0,0 +1,51 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func previewTicket(code: String) async throws -> ClassTicketPreview { + do { + let payload = try await client.send(ClassTicketService.preview(code: code), as: ClassTicketPreviewDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + public func registerTicket(code: String) async throws -> ClassTicket { + do { + let payload = try await client.send(ClassTicketService.register(code: code), as: ClassTicketDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + public func fetchMyTicket() async throws -> ClassTicket { + do { + let payload = try await client.send(ClassTicketService.me, as: ClassTicketDTO.self) + return try payload.toDomain() + } catch { + throw Self.ticketError(error) + } + } + + static func ticketError(_ error: Error) -> ClassTicketError { + if let error = error as? ClassTicketError { return error } + if let error = error as? PickeNetworkError { + if case let .response(response) = error { + switch response.code { + case "COMMON_400", "TICKET_404_CODE": return .invalidCode + case "TICKET_409_REVOKED": return .revoked + case "TICKET_409_REG_EXP": return .registrationExpired + case "TICKET_409_CLAIMED": return .claimed + case "TICKET_409_ACTIVE": return .activeTicket + case "TICKET_404": return .notFound + default: return .network(response.message ?? "이용권 정보를 불러오지 못했어요.") + } + } + } + return .network(error.localizedDescription) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift new file mode 100644 index 00000000..02c42d0d --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift @@ -0,0 +1,48 @@ +import APIEndpoint +import ClassDomainInterface +import Foundation +import PickeNetwork + +extension ClassRepositoryImpl { + public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.create(filter), as: ClassTopicDraftDTO.self).toDomain() + } + + public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.detail(id), as: ClassTopicDraftDTO.self).toDomain() + } + + public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { + try await sendTopicDraft(ClassTopicDraftService.suggest(id, filter), as: ClassTopicDraftDTO.self).toDomain() + } + + public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { + try await sendTopicDraft( + ClassTopicDraftService.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID), + as: ClassTopicVersionDTO.self + ).toDomain() + } + + public func validateTopicDraft(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) async throws -> ClassTopicValidation { + try await sendTopicDraft( + ClassTopicDraftService.validate(draftID: draftID, baseVersionID: baseVersionID, candidate: candidate), + as: ClassTopicValidationDTO.self + ).toDomain() + } + + private func sendTopicDraft( + _ request: some PickeDataRequest, + as type: T.Type + ) async throws -> T { + do { + return try await client.send(request, as: type) + } catch let error as PickeNetworkError { + if case let .response(response) = error { + throw ClassError.network(response.message ?? "AI 주제 요청을 처리하지 못했어요.") + } + throw ClassError.from(error) + } catch { + throw ClassError.from(error) + } + } +} diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift index 1c9eef1f..e8470a03 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl.swift @@ -12,552 +12,8 @@ import Foundation import PickeNetwork public final class ClassRepositoryImpl: ClassInterface { - private static let unavailableMessage = "클래스 서버 API가 연결되지 않았습니다." - @Dependency(\.networkClient) private var client + @Dependency(\.networkClient) var client public init() {} - public func fetchMyClasses() async throws -> [ClassRoom] { - do { - let data = try await client.send(ClassroomService.myClasses, as: [ClassListItemDTO].self) - return try data.map { try $0.toDomain() } - } catch { - throw Self.classError(error) - } - } - - public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { - guard (1 ... 3).contains(filter.categories.count), filter.keyword.count <= 50 else { - throw ClassError.unknown("추천 조건을 확인해 주세요.") - } - do { - let data = try await client.send( - ClassBattleRecommendationService.recommendations(filter: filter), - as: ClassBattleRecommendationDataDTO.self - ) - return try data.items.map { try $0.toDomain() } - } catch let error as PickeNetworkError { - if case let .response(response) = error { - throw ClassError.network(response.message ?? "배틀 추천을 불러오지 못했습니다.") - } - throw ClassError.from(error) - } - } - - public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { - do { - let data = try await client.send(ClassroomService.create(creation), as: ClassDetailDataDTO.self) - return try data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func fetchClass(id: Int) async throws -> ClassRoom { - do { - let data = try await client.send(ClassroomService.detail(id), as: ClassDetailDataDTO.self) - return try data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func fetchClass(joinCode _: String) async throws -> ClassRoom { - throw ClassError.unknown("참여 미리보기는 클래스 상세를 제공하지 않습니다.") - } - - public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { - do { - let data = try await client.send( - ClassroomService.joinPreview(joinCode), - as: ClassJoinPreviewDataDTO.self - ) - return try data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { - do { - let data = try await client.send( - ClassroomService.join(code: joinCode, nickname: nickname), - as: ClassDetailDataDTO.self - ) - return try data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { - do { - _ = try await client.send( - ClassroomService.updateDeadline(id: id, deadline: deadline), - as: PickeEmptyResponse.self - ) - let data = try await client.send(ClassroomService.detail(id), as: ClassDetailDataDTO.self) - return try data.toDomain() - } catch { - throw Self.classError(error) - } - } - - public func deleteClass(id: Int) async throws { - do { - _ = try await client.send(ClassroomService.delete(id), as: PickeEmptyResponse.self) - } catch { - throw Self.classError(error) - } - } - - public func leaveClass(id: Int) async throws { - do { - _ = try await client.send(ClassroomService.leave(id), as: PickeEmptyResponse.self) - } catch { - throw Self.classError(error) - } - } - - public func fetchReport(roomID _: Int) async throws -> ClassReport? { - throw ClassError.network(Self.unavailableMessage) - } - - public func fetchOwnerDashboard(roomID _: Int) async throws -> ClassOwnerDashboardData? { - throw ClassError.network(Self.unavailableMessage) - } - - private static func classError(_ error: Error) -> ClassError { - guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } - guard case let .response(response) = networkError else { return ClassError.from(networkError) } - return ClassErrorMapper.map( - code: response.code, - message: response.message, - fallback: "클래스 요청을 처리하지 못했습니다." - ) - } -} - -extension ClassRepositoryImpl { - public func fetchBattle(classID: Int) async throws -> ClassBattleSession { - do { - let payload = try await client.send(ClassBattleService.detail(classID: classID), as: ClassBattleSessionDTO.self) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func submitPreVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - do { - let payload = try await client.send( - ClassBattleService.preVote(classID: classID, optionID: optionID), - as: ClassVoteReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func completeTTS(classID: Int) async throws { - do { - _ = try await client.send( - ClassBattleService.ttsComplete(classID: classID), - as: PickeEmptyResponse.self - ) - } catch { throw Self.battleError(error) } - } - - public func submitPostVote( - classID: Int, - optionID: Int - ) async throws -> ClassVoteReceipt { - do { - let payload = try await client.send( - ClassBattleService.postVote(classID: classID, optionID: optionID), - as: ClassVoteReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { - do { - let payload = try await client.send(ClassBattleService.myVotes(classID: classID), as: ClassVoteHistoryDTO.self) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { - do { - let payload = try await client.send(ClassBattleService.voteStats(classID: classID), as: ClassVoteStatsDTO.self) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func publishPerspective( - classID: Int, - content: String - ) async throws -> ClassPerspectiveReceipt { - do { - let payload = try await client.send( - ClassBattleService.publishPerspective(classID: classID, content: content), - as: ClassPerspectiveReceiptDTO.self - ) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage { - do { - let payload = try await client.send( - ClassBattleService.perspectives( - classID: classID, - cursor: cursor, - size: size, - optionID: optionID, - sort: sort - ), - as: ClassPerspectivePageDTO.self - ) - return try payload.toDomain() - } catch { throw Self.battleError(error) } - } - - public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { - do { - let payload = try await client.send( - ClassBattleService.myPerspectives(classID: classID), - as: [ClassPerspectiveItemDTO].self - ) - return try payload.map { try $0.toDomain() } - } catch { throw Self.battleError(error) } - } - - private static func battleError(_ error: Error) -> ClassBattleError { - if let error = error as? ClassBattleError { return error } - if let error = error as? PickeNetworkError, - case let .response(response) = error - { - switch response.code { - case "CLASS_404": return .classNotFound - case "CLASS_403_MEMBER": return .membershipRequired - case "CLASS_409_TICKET_ENDED": return .ticketEnded - case "CLASS_409_CLOSED": return .closed - case "CLASS_409_PRE_VOTE_LOCKED": return .preVoteLocked - case "BATTLE_OPT_404": return .invalidOption - case "CLASS_409_TTS": return .ttsUnavailable - case "VOTE_404": return .voteNotFound - case "VOTE_409_PRE": return .preVoteRequired - case "VOTE_409_SUB": return .alreadySubmitted - case "CLASS_409_DEBATE_INCOMPLETE": return .debateIncomplete - default: return .network(response.message ?? "클래스 배틀 요청을 처리하지 못했어요.") - } - } - return .network(error.localizedDescription) - } -} - -extension ClassRepositoryImpl { - public func fetchMembers(classID: Int) async throws -> ClassMemberList { - do { - let data = try await client.send( - ClassMemberService.members(classID: classID), - as: [ClassMemberDTO].self - ) - return try ClassMemberDTO.toMemberList(data) - } catch { - throw Self.memberError(error) - } - } - - public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { - do { - let data = try await client.send( - ClassMemberService.updateMyName(classID: classID, name: name), - as: ClassMemberDTO.self - ) - return data.toDomain() - } catch { - throw Self.memberError(error) - } - } - - public func removeMember(classID: Int, memberID: Int) async throws { - do { - _ = try await client.send( - ClassMemberService.remove(classID: classID, memberID: memberID), - as: PickeEmptyResponse.self - ) - } catch { - throw Self.memberError(error) - } - } - - private static func memberError(_ error: Error) -> ClassError { - guard let networkError = error as? PickeNetworkError else { return ClassError.from(error) } - guard case let .response(response) = networkError else { return ClassError.from(networkError) } - return ClassErrorMapper.map( - code: response.code, - message: response.message, - fallback: "멤버 요청을 처리하지 못했어요." - ) - } -} - -extension ClassRepositoryImpl { - public func previewTicket(code: String) async throws -> ClassTicketPreview { - do { - let payload = try await client.send(ClassTicketService.preview(code: code), as: ClassTicketPreviewDTO.self) - return try payload.toDomain() - } catch { - throw Self.ticketError(error) - } - } - - public func registerTicket(code: String) async throws -> ClassTicket { - do { - let payload = try await client.send(ClassTicketService.register(code: code), as: ClassTicketDTO.self) - return try payload.toDomain() - } catch { - throw Self.ticketError(error) - } - } - - public func fetchMyTicket() async throws -> ClassTicket { - do { - let payload = try await client.send(ClassTicketService.me, as: ClassTicketDTO.self) - return try payload.toDomain() - } catch { - throw Self.ticketError(error) - } - } - - static func ticketError(_ error: Error) -> ClassTicketError { - if let error = error as? ClassTicketError { return error } - if let error = error as? PickeNetworkError { - if case let .response(response) = error { - switch response.code { - case "COMMON_400", "TICKET_404_CODE": return .invalidCode - case "TICKET_409_REVOKED": return .revoked - case "TICKET_409_REG_EXP": return .registrationExpired - case "TICKET_409_CLAIMED": return .claimed - case "TICKET_409_ACTIVE": return .activeTicket - case "TICKET_404": return .notFound - default: return .network(response.message ?? "이용권 정보를 불러오지 못했어요.") - } - } - } - return .network(error.localizedDescription) - } -} - -extension ClassRepositoryImpl { - public func fetchResult(roomID: Int) async throws -> ClassResult { - do { - let payload = try await client.send(ClassResultService.result(roomID), as: ClassResultDTO.self) - return try payload.toDomain() - } catch let error as PickeNetworkError { - if case let .response(response) = error { - throw Self.resultError(code: response.code, message: response.message) - } - throw ClassError.from(error) - } catch { - throw ClassError.from(error) - } - } - - public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { - do { - let payload = try await client.send(ClassResultService.myReport(roomID), as: ClassPersonalReportDTO.self) - return try payload.toDomain() - } catch { - throw Self.feedbackError(error) - } - } - - public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { - do { - let payload = try await client.send(ClassResultService.insight(roomID), as: ClassInsightDTO.self) - return try payload.toDomain() - } catch { - throw Self.feedbackError(error) - } - } - - public func retryInsight(roomID: Int) async throws { - do { - let payload = try await client.send(ClassResultService.retryInsight(roomID), as: ClassInsightRetryDTO.self) - guard payload.state == "PENDING" else { throw ClassAIFeedbackError.generationFailed } - } catch { - throw Self.feedbackError(error) - } - } - - static func resultError( - code: String?, - message: String? - ) -> ClassError { - switch code { - case "CLASS_409_RESULT_EXPIRED": return .resultExpired - case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied - default: return .network(message ?? "클래스 결과를 불러오지 못했어요.") - } - } - - static func feedbackError(code: String?) -> ClassAIFeedbackError { - switch code { - case "CLASS_409_REPORT_RETRY": return .generationFailed - case "CLASS_409_RESULT_EXPIRED": return .resultExpired - case "CLASS_403_MEMBER", "CLASS_403_OWNER": return .accessDenied - default: return .temporarilyUnavailable - } - } - - static func feedbackError(_ error: Error) -> ClassAIFeedbackError { - if let mapped = error as? ClassAIFeedbackError { return mapped } - if error is DecodingError { return .invalidResponse } - if let networkError = error as? PickeNetworkError { - switch networkError { - case .decoding: return .invalidResponse - case let .response(response): return feedbackError(code: response.code) - default: break - } - } - return .temporarilyUnavailable - } -} - -extension ClassRepositoryImpl { - public func fetchDebate(classID: Int) async throws -> ClassDebate { - try await debate(.fetch(classID)) - } - - public func startDebate(classID: Int) async throws -> ClassDebate { - try await debate(.start(classID)) - } - - public func retryDebate(classID: Int) async throws -> ClassDebate { - try await debate(.retry(classID)) - } - - public func submitDebateReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate { - try await debate(.submitReply(classID: classID, round: round, content: content, requestKey: requestKey)) - } - - private func debate(_ service: ClassDebateService) async throws -> ClassDebate { - do { - let payload = try await client.send(service, as: ClassDebateDTO.self) - return try payload.toDomain() - } catch let error as PickeNetworkError { - if case let .response(response) = error { - switch response.code { - case "CLASS_404_DEBATE": throw ClassAIChatError.noSession - case "CLASS_400_DEBATE_REPLY": throw ClassAIChatError.contentRejected - case "CLASS_409_DEBATE_STATE", "CLASS_409_DEBATE_ROUND", "CLASS_409_DEBATE_RETRY": - throw ClassAIChatError.temporarilyUnavailable - default: throw ClassAIChatError.unexpectedFailure - } - } - throw ClassAIChatError.temporarilyUnavailable - } - } - -} - -extension ClassRepositoryImpl { - public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { - try await sendTopicDraft(ClassTopicDraftService.create(filter), as: ClassTopicDraftDTO.self).toDomain() - } - - public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { - try await sendTopicDraft(ClassTopicDraftService.detail(id), as: ClassTopicDraftDTO.self).toDomain() - } - - public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { - try await sendTopicDraft(ClassTopicDraftService.suggest(id, filter), as: ClassTopicDraftDTO.self).toDomain() - } - - public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { - try await sendTopicDraft( - ClassTopicDraftService.select(draftID: draftID, batchNo: batchNo, candidateID: candidateID), - as: ClassTopicVersionDTO.self - ).toDomain() - } - - public func validateTopicDraft(draftID: Int, baseVersionID: Int, candidate: ClassTopicCandidate) async throws -> ClassTopicValidation { - try await sendTopicDraft( - ClassTopicDraftService.validate(draftID: draftID, baseVersionID: baseVersionID, candidate: candidate), - as: ClassTopicValidationDTO.self - ).toDomain() - } - - private func sendTopicDraft( - _ request: some PickeDataRequest, - as type: T.Type - ) async throws -> T { - do { - return try await client.send(request, as: type) - } catch let error as PickeNetworkError { - if case let .response(response) = error { - throw ClassError.network(response.message ?? "AI 주제 요청을 처리하지 못했어요.") - } - throw ClassError.from(error) - } catch { - throw ClassError.from(error) - } - } -} - -extension ClassRepositoryImpl { - public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - try await ClassAIChatFoundationEngine.shared.start(id: id, context: context) - return - } - #endif - throw ClassAIChatError.unavailable - } - - public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - return try await ClassAIChatFoundationEngine.shared.respond( - id: id, - text: text, - isFinalRound: isFinalRound - ) - } - #endif - throw ClassAIChatError.unavailable - } - - public func endAIChat(_ id: UUID) async { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - await ClassAIChatFoundationEngine.shared.end(id: id) - } - #endif - } - - public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { - #if canImport(FoundationModels) - if #available(iOS 26.0, *) { - return try await ClassAIFeedbackFoundationEngine.generate(context: context) - } - #endif - throw ClassAIFeedbackError.deviceNotEligible - } } diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift deleted file mode 100644 index 7e837974..00000000 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCase.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// ClassUseCase.swift -// ClassDomain -// -// Created by Picke on 10/11/26. -// - -import ClassDomainInterface -import ComposableArchitecture -import Foundation - -public struct ClassUseCaseImpl: ClassInterface { - @Dependency(\.classRepository) private var repository - - public init() {} - - public func fetchMyClasses() async throws -> [ClassRoom] { - try await repository.fetchMyClasses() - } - - public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { - try await repository.fetchRecommendedBattles(filter: filter) - } - - public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { - try await repository.createClass(creation) - } - - public func fetchClass(id: Int) async throws -> ClassRoom { - try await repository.fetchClass(id: id) - } - - public func fetchClass(joinCode: String) async throws -> ClassRoom { - try await repository.fetchClass(joinCode: joinCode) - } - - public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { - try await repository.fetchClassPreview(joinCode: joinCode) - } - - public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { - try await repository.joinClass(joinCode: joinCode, nickname: nickname) - } - - public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { - try await repository.updateDeadline(id: id, deadline: deadline) - } - - public func deleteClass(id: Int) async throws { - try await repository.deleteClass(id: id) - } - - public func leaveClass(id: Int) async throws { - try await repository.leaveClass(id: id) - } - - public func fetchReport(roomID: Int) async throws -> ClassReport? { - try await repository.fetchReport(roomID: roomID) - } - - public func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? { - try await repository.fetchOwnerDashboard(roomID: roomID) - } -} - -extension ClassUseCaseImpl { - public func fetchBattle(classID: Int) async throws -> ClassBattleSession { - try await repository.fetchBattle(classID: classID) - } - - public func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { - try await repository.submitPreVote(classID: classID, optionID: optionID) - } - - public func completeTTS(classID: Int) async throws { - try await repository.completeTTS(classID: classID) - } - - public func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { - try await repository.submitPostVote(classID: classID, optionID: optionID) - } - - public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { - try await repository.fetchMyVotes(classID: classID) - } - - public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { - try await repository.fetchVoteStats(classID: classID) - } - - public func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt { - try await repository.publishPerspective(classID: classID, content: content) - } - - public func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage { - try await repository.fetchPerspectives( - classID: classID, - cursor: cursor, - size: size, - optionID: optionID, - sort: sort - ) - } - - public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { - try await repository.fetchMyPerspectives(classID: classID) - } - - public func fetchMembers(classID: Int) async throws -> ClassMemberList { - try await repository.fetchMembers(classID: classID) - } - - public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { - try await repository.updateMyDisplayName(classID: classID, name: name) - } - - public func removeMember(classID: Int, memberID: Int) async throws { - try await repository.removeMember(classID: classID, memberID: memberID) - } - - public func fetchDebate(classID: Int) async throws -> ClassDebate { - try await repository.fetchDebate(classID: classID) - } - - public func startDebate(classID: Int) async throws -> ClassDebate { - try await repository.startDebate(classID: classID) - } - - public func submitDebateReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate { - try await repository.submitDebateReply( - classID: classID, - round: round, - content: content, - requestKey: requestKey - ) - } - - public func retryDebate(classID: Int) async throws -> ClassDebate { - try await repository.retryDebate(classID: classID) - } - - public func previewTicket(code: String) async throws -> ClassTicketPreview { - try await repository.previewTicket(code: code) - } - - public func registerTicket(code: String) async throws -> ClassTicket { - try await repository.registerTicket(code: code) - } - - public func fetchMyTicket() async throws -> ClassTicket { - try await repository.fetchMyTicket() - } - - public func fetchResult(roomID: Int) async throws -> ClassResult { - try await repository.fetchResult(roomID: roomID) - } - - public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { - try await repository.fetchPersonalResult(roomID: roomID) - } - - public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { - try await repository.fetchInsight(roomID: roomID) - } - - public func retryInsight(roomID: Int) async throws { - try await repository.retryInsight(roomID: roomID) - } - - public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { - try await repository.createTopicDraft(filter: filter) - } - - public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { - try await repository.fetchTopicDraft(id: id) - } - - public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { - try await repository.suggestTopicDraft(id: id, filter: filter) - } - - public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { - try await repository.selectTopicDraft(draftID: draftID, batchNo: batchNo, candidateID: candidateID) - } - - public func validateTopicDraft( - draftID: Int, - baseVersionID: Int, - candidate: ClassTopicCandidate - ) async throws -> ClassTopicValidation { - try await repository.validateTopicDraft( - draftID: draftID, - baseVersionID: baseVersionID, - candidate: candidate - ) - } - - public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { - try await repository.startAIChat(id, context) - } - - public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { - try await repository.respondAIChat(id, text, isFinalRound: isFinalRound) - } - - public func endAIChat(_ id: UUID) async { - await repository.endAIChat(id) - } - - public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { - try await repository.generateAIFeedback(context: context) - } -} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift new file mode 100644 index 00000000..dee2f317 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift @@ -0,0 +1,52 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchBattle(classID: Int) async throws -> ClassBattleSession { + try await repository.fetchBattle(classID: classID) + } + + public func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + try await repository.submitPreVote(classID: classID, optionID: optionID) + } + + public func completeTTS(classID: Int) async throws { + try await repository.completeTTS(classID: classID) + } + + public func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt { + try await repository.submitPostVote(classID: classID, optionID: optionID) + } + + public func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory { + try await repository.fetchMyVotes(classID: classID) + } + + public func fetchVoteStats(classID: Int) async throws -> ClassVoteStats { + try await repository.fetchVoteStats(classID: classID) + } + + public func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt { + try await repository.publishPerspective(classID: classID, content: content) + } + + public func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage { + try await repository.fetchPerspectives( + classID: classID, + cursor: cursor, + size: size, + optionID: optionID, + sort: sort + ) + } + + public func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] { + try await repository.fetchMyPerspectives(classID: classID) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift new file mode 100644 index 00000000..7d0075c7 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift @@ -0,0 +1,30 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchDebate(classID: Int) async throws -> ClassDebate { + try await repository.fetchDebate(classID: classID) + } + + public func startDebate(classID: Int) async throws -> ClassDebate { + try await repository.startDebate(classID: classID) + } + + public func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate { + try await repository.submitDebateReply( + classID: classID, + round: round, + content: content, + requestKey: requestKey + ) + } + + public func retryDebate(classID: Int) async throws -> ClassDebate { + try await repository.retryDebate(classID: classID) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift new file mode 100644 index 00000000..d432fe67 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift @@ -0,0 +1,20 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws { + try await repository.startAIChat(id, context) + } + + public func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String { + try await repository.respondAIChat(id, text, isFinalRound: isFinalRound) + } + + public func endAIChat(_ id: UUID) async { + await repository.endAIChat(id) + } + + public func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult { + try await repository.generateAIFeedback(context: context) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift new file mode 100644 index 00000000..974a84e9 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift @@ -0,0 +1,16 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchMembers(classID: Int) async throws -> ClassMemberList { + try await repository.fetchMembers(classID: classID) + } + + public func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember { + try await repository.updateMyDisplayName(classID: classID, name: name) + } + + public func removeMember(classID: Int, memberID: Int) async throws { + try await repository.removeMember(classID: classID, memberID: memberID) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift new file mode 100644 index 00000000..aa4f5d9e --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift @@ -0,0 +1,8 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] { + try await repository.fetchRecommendedBattles(filter: filter) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift new file mode 100644 index 00000000..a052adc5 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift @@ -0,0 +1,12 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchReport(roomID: Int) async throws -> ClassReport? { + try await repository.fetchReport(roomID: roomID) + } + + public func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? { + try await repository.fetchOwnerDashboard(roomID: roomID) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift new file mode 100644 index 00000000..dd6c38e3 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift @@ -0,0 +1,20 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchResult(roomID: Int) async throws -> ClassResult { + try await repository.fetchResult(roomID: roomID) + } + + public func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback { + try await repository.fetchPersonalResult(roomID: roomID) + } + + public func fetchInsight(roomID: Int) async throws -> ClassAIInsight { + try await repository.fetchInsight(roomID: roomID) + } + + public func retryInsight(roomID: Int) async throws { + try await repository.retryInsight(roomID: roomID) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift new file mode 100644 index 00000000..3601f213 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift @@ -0,0 +1,40 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func fetchMyClasses() async throws -> [ClassRoom] { + try await repository.fetchMyClasses() + } + + public func createClass(_ creation: ClassCreation) async throws -> ClassRoom { + try await repository.createClass(creation) + } + + public func fetchClass(id: Int) async throws -> ClassRoom { + try await repository.fetchClass(id: id) + } + + public func fetchClass(joinCode: String) async throws -> ClassRoom { + try await repository.fetchClass(joinCode: joinCode) + } + + public func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { + try await repository.fetchClassPreview(joinCode: joinCode) + } + + public func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom { + try await repository.joinClass(joinCode: joinCode, nickname: nickname) + } + + public func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom { + try await repository.updateDeadline(id: id, deadline: deadline) + } + + public func deleteClass(id: Int) async throws { + try await repository.deleteClass(id: id) + } + + public func leaveClass(id: Int) async throws { + try await repository.leaveClass(id: id) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift new file mode 100644 index 00000000..3293c5d1 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift @@ -0,0 +1,16 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func previewTicket(code: String) async throws -> ClassTicketPreview { + try await repository.previewTicket(code: code) + } + + public func registerTicket(code: String) async throws -> ClassTicket { + try await repository.registerTicket(code: code) + } + + public func fetchMyTicket() async throws -> ClassTicket { + try await repository.fetchMyTicket() + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift new file mode 100644 index 00000000..9676095c --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift @@ -0,0 +1,32 @@ +import ClassDomainInterface +import Foundation + +extension ClassUseCaseImpl { + public func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft { + try await repository.createTopicDraft(filter: filter) + } + + public func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft { + try await repository.fetchTopicDraft(id: id) + } + + public func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft { + try await repository.suggestTopicDraft(id: id, filter: filter) + } + + public func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion { + try await repository.selectTopicDraft(draftID: draftID, batchNo: batchNo, candidateID: candidateID) + } + + public func validateTopicDraft( + draftID: Int, + baseVersionID: Int, + candidate: ClassTopicCandidate + ) async throws -> ClassTopicValidation { + try await repository.validateTopicDraft( + draftID: draftID, + baseVersionID: baseVersionID, + candidate: candidate + ) + } +} diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift new file mode 100644 index 00000000..30add834 --- /dev/null +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift @@ -0,0 +1,17 @@ +// +// ClassUseCaseImpl.swift +// ClassDomain +// +// Created by Picke on 10/11/26. +// + +import ClassDomainInterface +import ComposableArchitecture +import Foundation + +public struct ClassUseCaseImpl: ClassInterface { + @Dependency(\.classRepository) var repository + + public init() {} + +} From 227a9c46d61f52d402fa7fcc83c9eec2b8826f68 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:08:50 +0900 Subject: [PATCH 05/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20APIEndpoint?= =?UTF-8?q?=EB=A5=BC=20=EC=97=AD=ED=95=A0=EB=B3=84=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class 서비스와 요청 모델을 Room, Battle, Member, Debate, Result, Ticket, TopicDraft 폴더에 배치합니다. --- .../Class/{ => Battle}/ClassBattleRecommendationService.swift | 0 .../Sources/Class/{ => Battle}/ClassBattleService.swift | 0 .../{ => Battle}/Request/ClassBattleRecommendationRequests.swift | 0 .../Sources/Class/{ => Battle}/Request/ClassBattleRequests.swift | 0 .../Sources/Class/{ => Debate}/ClassDebateService.swift | 0 .../Sources/Class/{ => Debate}/Request/ClassDebateRequests.swift | 0 .../Sources/Class/{ => Member}/ClassMemberService.swift | 0 .../Sources/Class/{ => Member}/Request/ClassMemberRequests.swift | 0 .../Sources/Class/{ => Result}/ClassResultService.swift | 0 .../APIEndpoint/Sources/Class/{ => Room}/ClassroomService.swift | 0 .../Sources/Class/{ => Room}/Request/ClassroomRequests.swift | 0 .../Sources/Class/{ => Ticket}/ClassTicketService.swift | 0 .../Sources/Class/{ => Ticket}/Request/ClassTicketRequests.swift | 0 .../Sources/Class/{ => TopicDraft}/ClassTopicDraftService.swift | 0 .../Class/{ => TopicDraft}/Request/ClassTopicDraftRequests.swift | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename Projects/Service/APIEndpoint/Sources/Class/{ => Battle}/ClassBattleRecommendationService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Battle}/ClassBattleService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Battle}/Request/ClassBattleRecommendationRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Battle}/Request/ClassBattleRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Debate}/ClassDebateService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Debate}/Request/ClassDebateRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Member}/ClassMemberService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Member}/Request/ClassMemberRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Result}/ClassResultService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Room}/ClassroomService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Room}/Request/ClassroomRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Ticket}/ClassTicketService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => Ticket}/Request/ClassTicketRequests.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => TopicDraft}/ClassTopicDraftService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{ => TopicDraft}/Request/ClassTopicDraftRequests.swift (100%) diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassBattleRecommendationService.swift b/Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleRecommendationService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassBattleRecommendationService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleRecommendationService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift b/Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassBattleService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRecommendationRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRecommendationRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRecommendationRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassBattleRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift b/Projects/Service/APIEndpoint/Sources/Class/Debate/ClassDebateService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassDebateService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Debate/ClassDebateService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Debate/Request/ClassDebateRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassDebateRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Debate/Request/ClassDebateRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift b/Projects/Service/APIEndpoint/Sources/Class/Member/ClassMemberService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassMemberService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Member/ClassMemberService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Member/Request/ClassMemberRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassMemberRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Member/Request/ClassMemberRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift b/Projects/Service/APIEndpoint/Sources/Class/Result/ClassResultService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassResultService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Result/ClassResultService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift b/Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassroomService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassroomRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift b/Projects/Service/APIEndpoint/Sources/Class/Ticket/ClassTicketService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassTicketService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Ticket/ClassTicketService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Ticket/Request/ClassTicketRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassTicketRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Ticket/Request/ClassTicketRequests.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift b/Projects/Service/APIEndpoint/Sources/Class/TopicDraft/ClassTopicDraftService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/ClassTopicDraftService.swift rename to Projects/Service/APIEndpoint/Sources/Class/TopicDraft/ClassTopicDraftService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/TopicDraft/Request/ClassTopicDraftRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Request/ClassTopicDraftRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/TopicDraft/Request/ClassTopicDraftRequests.swift From b12654c23fdf627b1bd22e012f3b70c69e7c3767 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:09:35 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20API=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=A5=BC=20=EC=97=AD=ED=95=A0=EB=B3=84=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APIEndpoint의 폴더 구조와 맞춰 Class 경로 정의를 역할별로 배치합니다. --- .../Service/API/Sources/Class/{ => Battle}/ClassBattleAPI.swift | 0 .../Sources/Class/{ => Battle}/ClassBattleRecommendationAPI.swift | 0 .../Service/API/Sources/Class/{ => Debate}/ClassDebateAPI.swift | 0 .../Service/API/Sources/Class/{ => Member}/ClassMemberAPI.swift | 0 .../Service/API/Sources/Class/{ => Result}/ClassResultAPI.swift | 0 Projects/Service/API/Sources/Class/{ => Room}/ClassroomAPI.swift | 0 .../Service/API/Sources/Class/{ => Ticket}/ClassTicketAPI.swift | 0 .../API/Sources/Class/{ => TopicDraft}/ClassTopicDraftAPI.swift | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Projects/Service/API/Sources/Class/{ => Battle}/ClassBattleAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Battle}/ClassBattleRecommendationAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Debate}/ClassDebateAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Member}/ClassMemberAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Result}/ClassResultAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Room}/ClassroomAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => Ticket}/ClassTicketAPI.swift (100%) rename Projects/Service/API/Sources/Class/{ => TopicDraft}/ClassTopicDraftAPI.swift (100%) diff --git a/Projects/Service/API/Sources/Class/ClassBattleAPI.swift b/Projects/Service/API/Sources/Class/Battle/ClassBattleAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassBattleAPI.swift rename to Projects/Service/API/Sources/Class/Battle/ClassBattleAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift b/Projects/Service/API/Sources/Class/Battle/ClassBattleRecommendationAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassBattleRecommendationAPI.swift rename to Projects/Service/API/Sources/Class/Battle/ClassBattleRecommendationAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassDebateAPI.swift b/Projects/Service/API/Sources/Class/Debate/ClassDebateAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassDebateAPI.swift rename to Projects/Service/API/Sources/Class/Debate/ClassDebateAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassMemberAPI.swift b/Projects/Service/API/Sources/Class/Member/ClassMemberAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassMemberAPI.swift rename to Projects/Service/API/Sources/Class/Member/ClassMemberAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassResultAPI.swift b/Projects/Service/API/Sources/Class/Result/ClassResultAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassResultAPI.swift rename to Projects/Service/API/Sources/Class/Result/ClassResultAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassroomAPI.swift b/Projects/Service/API/Sources/Class/Room/ClassroomAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassroomAPI.swift rename to Projects/Service/API/Sources/Class/Room/ClassroomAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassTicketAPI.swift b/Projects/Service/API/Sources/Class/Ticket/ClassTicketAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassTicketAPI.swift rename to Projects/Service/API/Sources/Class/Ticket/ClassTicketAPI.swift diff --git a/Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift b/Projects/Service/API/Sources/Class/TopicDraft/ClassTopicDraftAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/ClassTopicDraftAPI.swift rename to Projects/Service/API/Sources/Class/TopicDraft/ClassTopicDraftAPI.swift From af485603523d862d92106090fc7216a35173471a Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:11:35 +0900 Subject: [PATCH 07/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20API=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Battle => Recommendation}/ClassBattleRecommendationAPI.swift | 0 .../ClassBattleRecommendationService.swift | 0 .../Request/ClassBattleRecommendationRequests.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Projects/Service/API/Sources/Class/{Battle => Recommendation}/ClassBattleRecommendationAPI.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{Battle => Recommendation}/ClassBattleRecommendationService.swift (100%) rename Projects/Service/APIEndpoint/Sources/Class/{Battle => Recommendation}/Request/ClassBattleRecommendationRequests.swift (100%) diff --git a/Projects/Service/API/Sources/Class/Battle/ClassBattleRecommendationAPI.swift b/Projects/Service/API/Sources/Class/Recommendation/ClassBattleRecommendationAPI.swift similarity index 100% rename from Projects/Service/API/Sources/Class/Battle/ClassBattleRecommendationAPI.swift rename to Projects/Service/API/Sources/Class/Recommendation/ClassBattleRecommendationAPI.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleRecommendationService.swift b/Projects/Service/APIEndpoint/Sources/Class/Recommendation/ClassBattleRecommendationService.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Battle/ClassBattleRecommendationService.swift rename to Projects/Service/APIEndpoint/Sources/Class/Recommendation/ClassBattleRecommendationService.swift diff --git a/Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRecommendationRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Recommendation/Request/ClassBattleRecommendationRequests.swift similarity index 100% rename from Projects/Service/APIEndpoint/Sources/Class/Battle/Request/ClassBattleRecommendationRequests.swift rename to Projects/Service/APIEndpoint/Sources/Class/Recommendation/Request/ClassBattleRecommendationRequests.swift From 8977b95b1995bf9da8c506b4f9cb9857e55357d8 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:12:18 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=EB=B3=84=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EC=99=80=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=ED=82=A4=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Battle/ClassBattleInterface.swift | 44 ++++++++++ .../Sources/Repository/ClassInterface.swift | 80 +++---------------- .../Debate/ClassDebateInterface.swift | 38 +++++++++ .../LocalAI/ClassLocalAIInterface.swift | 33 ++++++++ .../Member/ClassMemberInterface.swift | 32 ++++++++ .../ClassRecommendationInterface.swift | 30 +++++++ .../Report/ClassReportInterface.swift | 31 +++++++ .../Result/ClassResultInterface.swift | 33 ++++++++ .../Repository/Room/ClassRoomInterface.swift | 45 +++++++++++ .../Ticket/ClassTicketInterface.swift | 32 ++++++++ .../TopicDraft/ClassTopicDraftInterface.swift | 38 +++++++++ .../Sources/ClassLiveDependencies.swift | 80 +++++++++++++++++++ 12 files changed, 447 insertions(+), 69 deletions(-) create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift create mode 100644 Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift new file mode 100644 index 00000000..78fe669e --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift @@ -0,0 +1,44 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassBattleInterface: Sendable { + func fetchBattle(classID: Int) async throws -> ClassBattleSession + func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt + func completeTTS(classID: Int) async throws + func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt + func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory + func fetchVoteStats(classID: Int) async throws -> ClassVoteStats + func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt + func fetchPerspectives( + classID: Int, + cursor: String?, + size: Int?, + optionID: Int?, + sort: String? + ) async throws -> ClassPerspectivePage + func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] +} + +public enum ClassBattleRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassBattleInterface { + MockClassRepository() + } +} + +public enum ClassBattleUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassBattleInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classBattleRepository: any ClassBattleInterface { + get { self[ClassBattleRepositoryDependency.self] } + set { self[ClassBattleRepositoryDependency.self] = newValue } + } + + var classBattleUseCase: any ClassBattleInterface { + get { self[ClassBattleUseCaseDependency.self] } + set { self[ClassBattleUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift index 9ecc201e..61788835 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift @@ -6,76 +6,18 @@ // import ComposableArchitecture -import Foundation -public protocol ClassInterface: Sendable { - func fetchMyClasses() async throws -> [ClassRoom] - func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] - func createClass(_ creation: ClassCreation) async throws -> ClassRoom - func fetchClass(id: Int) async throws -> ClassRoom - func fetchClass(joinCode: String) async throws -> ClassRoom - func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview - func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom - func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom - func deleteClass(id: Int) async throws - func leaveClass(id: Int) async throws - func fetchReport(roomID: Int) async throws -> ClassReport? - func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? - func fetchBattle(classID: Int) async throws -> ClassBattleSession - func submitPreVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt - func completeTTS(classID: Int) async throws - func submitPostVote(classID: Int, optionID: Int) async throws -> ClassVoteReceipt - func fetchMyVotes(classID: Int) async throws -> ClassVoteHistory - func fetchVoteStats(classID: Int) async throws -> ClassVoteStats - func publishPerspective(classID: Int, content: String) async throws -> ClassPerspectiveReceipt - func fetchPerspectives( - classID: Int, - cursor: String?, - size: Int?, - optionID: Int?, - sort: String? - ) async throws -> ClassPerspectivePage - func fetchMyPerspectives(classID: Int) async throws -> [ClassPerspectiveItem] - func fetchMembers(classID: Int) async throws -> ClassMemberList - func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember - func removeMember(classID: Int, memberID: Int) async throws - func fetchDebate(classID: Int) async throws -> ClassDebate - func startDebate(classID: Int) async throws -> ClassDebate - func submitDebateReply( - classID: Int, - round: Int, - content: String, - requestKey: UUID - ) async throws -> ClassDebate - func retryDebate(classID: Int) async throws -> ClassDebate - func previewTicket(code: String) async throws -> ClassTicketPreview - func registerTicket(code: String) async throws -> ClassTicket - func fetchMyTicket() async throws -> ClassTicket - func fetchResult(roomID: Int) async throws -> ClassResult - func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback - func fetchInsight(roomID: Int) async throws -> ClassAIInsight - func retryInsight(roomID: Int) async throws - func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft - func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft - func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft - func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion - func validateTopicDraft( - draftID: Int, - baseVersionID: Int, - candidate: ClassTopicCandidate - ) async throws -> ClassTopicValidation - func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws - func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String - func endAIChat(_ id: UUID) async - func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult -} - -public extension ClassInterface { - func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { - let room = try await fetchClass(joinCode: joinCode) - return ClassJoinPreview(room: room) - } -} +public protocol ClassInterface: + ClassRoomInterface, + ClassRecommendationInterface, + ClassBattleInterface, + ClassMemberInterface, + ClassDebateInterface, + ClassTicketInterface, + ClassResultInterface, + ClassTopicDraftInterface, + ClassReportInterface, + ClassLocalAIInterface {} public enum ClassRepositoryDependency: TestDependencyKey { public static var testValue: any ClassInterface { diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift new file mode 100644 index 00000000..1c283bd7 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassDebateInterface: Sendable { + func fetchDebate(classID: Int) async throws -> ClassDebate + func startDebate(classID: Int) async throws -> ClassDebate + func submitDebateReply( + classID: Int, + round: Int, + content: String, + requestKey: UUID + ) async throws -> ClassDebate + func retryDebate(classID: Int) async throws -> ClassDebate +} + +public enum ClassDebateRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassDebateInterface { + MockClassRepository() + } +} + +public enum ClassDebateUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassDebateInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classDebateRepository: any ClassDebateInterface { + get { self[ClassDebateRepositoryDependency.self] } + set { self[ClassDebateRepositoryDependency.self] = newValue } + } + + var classDebateUseCase: any ClassDebateInterface { + get { self[ClassDebateUseCaseDependency.self] } + set { self[ClassDebateUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift new file mode 100644 index 00000000..1f3070b8 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift @@ -0,0 +1,33 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassLocalAIInterface: Sendable { + func startAIChat(_ id: UUID, _ context: ClassAIChatContext) async throws + func respondAIChat(_ id: UUID, _ text: String, isFinalRound: Bool) async throws -> String + func endAIChat(_ id: UUID) async + func generateAIFeedback(context: ClassAIFeedbackContext) async throws -> ClassAIFeedbackResult +} + +public enum ClassLocalAIRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassLocalAIInterface { + MockClassRepository() + } +} + +public enum ClassLocalAIUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassLocalAIInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classLocalAIRepository: any ClassLocalAIInterface { + get { self[ClassLocalAIRepositoryDependency.self] } + set { self[ClassLocalAIRepositoryDependency.self] = newValue } + } + + var classLocalAIUseCase: any ClassLocalAIInterface { + get { self[ClassLocalAIUseCaseDependency.self] } + set { self[ClassLocalAIUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift new file mode 100644 index 00000000..ce3f6ea3 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift @@ -0,0 +1,32 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassMemberInterface: Sendable { + func fetchMembers(classID: Int) async throws -> ClassMemberList + func updateMyDisplayName(classID: Int, name: String) async throws -> ClassMember + func removeMember(classID: Int, memberID: Int) async throws +} + +public enum ClassMemberRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassMemberInterface { + MockClassRepository() + } +} + +public enum ClassMemberUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassMemberInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classMemberRepository: any ClassMemberInterface { + get { self[ClassMemberRepositoryDependency.self] } + set { self[ClassMemberRepositoryDependency.self] = newValue } + } + + var classMemberUseCase: any ClassMemberInterface { + get { self[ClassMemberUseCaseDependency.self] } + set { self[ClassMemberUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift new file mode 100644 index 00000000..ee4d3410 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift @@ -0,0 +1,30 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassRecommendationInterface: Sendable { + func fetchRecommendedBattles(filter: ClassTopicFilter) async throws -> [ClassBattleSummary] +} + +public enum ClassRecommendationRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassRecommendationInterface { + MockClassRepository() + } +} + +public enum ClassRecommendationUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassRecommendationInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classRecommendationRepository: any ClassRecommendationInterface { + get { self[ClassRecommendationRepositoryDependency.self] } + set { self[ClassRecommendationRepositoryDependency.self] = newValue } + } + + var classRecommendationUseCase: any ClassRecommendationInterface { + get { self[ClassRecommendationUseCaseDependency.self] } + set { self[ClassRecommendationUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift new file mode 100644 index 00000000..d6ffa317 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift @@ -0,0 +1,31 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassReportInterface: Sendable { + func fetchReport(roomID: Int) async throws -> ClassReport? + func fetchOwnerDashboard(roomID: Int) async throws -> ClassOwnerDashboardData? +} + +public enum ClassReportRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassReportInterface { + MockClassRepository() + } +} + +public enum ClassReportUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassReportInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classReportRepository: any ClassReportInterface { + get { self[ClassReportRepositoryDependency.self] } + set { self[ClassReportRepositoryDependency.self] = newValue } + } + + var classReportUseCase: any ClassReportInterface { + get { self[ClassReportUseCaseDependency.self] } + set { self[ClassReportUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift new file mode 100644 index 00000000..f5a890d0 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift @@ -0,0 +1,33 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassResultInterface: Sendable { + func fetchResult(roomID: Int) async throws -> ClassResult + func fetchPersonalResult(roomID: Int) async throws -> ClassAIPersonalFeedback + func fetchInsight(roomID: Int) async throws -> ClassAIInsight + func retryInsight(roomID: Int) async throws +} + +public enum ClassResultRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassResultInterface { + MockClassRepository() + } +} + +public enum ClassResultUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassResultInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classResultRepository: any ClassResultInterface { + get { self[ClassResultRepositoryDependency.self] } + set { self[ClassResultRepositoryDependency.self] = newValue } + } + + var classResultUseCase: any ClassResultInterface { + get { self[ClassResultUseCaseDependency.self] } + set { self[ClassResultUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift new file mode 100644 index 00000000..ec2a7371 --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift @@ -0,0 +1,45 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassRoomInterface: Sendable { + func fetchMyClasses() async throws -> [ClassRoom] + func createClass(_ creation: ClassCreation) async throws -> ClassRoom + func fetchClass(id: Int) async throws -> ClassRoom + func fetchClass(joinCode: String) async throws -> ClassRoom + func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview + func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom + func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom + func deleteClass(id: Int) async throws + func leaveClass(id: Int) async throws +} + +public extension ClassRoomInterface { + func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { + let room = try await fetchClass(joinCode: joinCode) + return ClassJoinPreview(room: room) + } +} + +public enum ClassRoomRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassRoomInterface { + MockClassRepository() + } +} + +public enum ClassRoomUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassRoomInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classRoomRepository: any ClassRoomInterface { + get { self[ClassRoomRepositoryDependency.self] } + set { self[ClassRoomRepositoryDependency.self] = newValue } + } + + var classRoomUseCase: any ClassRoomInterface { + get { self[ClassRoomUseCaseDependency.self] } + set { self[ClassRoomUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift new file mode 100644 index 00000000..d1bae0ef --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift @@ -0,0 +1,32 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassTicketInterface: Sendable { + func previewTicket(code: String) async throws -> ClassTicketPreview + func registerTicket(code: String) async throws -> ClassTicket + func fetchMyTicket() async throws -> ClassTicket +} + +public enum ClassTicketRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassTicketInterface { + MockClassRepository() + } +} + +public enum ClassTicketUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassTicketInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classTicketRepository: any ClassTicketInterface { + get { self[ClassTicketRepositoryDependency.self] } + set { self[ClassTicketRepositoryDependency.self] = newValue } + } + + var classTicketUseCase: any ClassTicketInterface { + get { self[ClassTicketUseCaseDependency.self] } + set { self[ClassTicketUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift new file mode 100644 index 00000000..da033aec --- /dev/null +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +import Foundation + +public protocol ClassTopicDraftInterface: Sendable { + func createTopicDraft(filter: ClassTopicFilter) async throws -> ClassTopicDraft + func fetchTopicDraft(id: Int) async throws -> ClassTopicDraft + func suggestTopicDraft(id: Int, filter: ClassTopicFilter?) async throws -> ClassTopicDraft + func selectTopicDraft(draftID: Int, batchNo: Int, candidateID: Int) async throws -> ClassTopicVersion + func validateTopicDraft( + draftID: Int, + baseVersionID: Int, + candidate: ClassTopicCandidate + ) async throws -> ClassTopicValidation +} + +public enum ClassTopicDraftRepositoryDependency: TestDependencyKey { + public static var testValue: any ClassTopicDraftInterface { + MockClassRepository() + } +} + +public enum ClassTopicDraftUseCaseDependency: TestDependencyKey { + public static var testValue: any ClassTopicDraftInterface { + MockClassRepository() + } +} + +public extension DependencyValues { + var classTopicDraftRepository: any ClassTopicDraftInterface { + get { self[ClassTopicDraftRepositoryDependency.self] } + set { self[ClassTopicDraftRepositoryDependency.self] = newValue } + } + + var classTopicDraftUseCase: any ClassTopicDraftInterface { + get { self[ClassTopicDraftUseCaseDependency.self] } + set { self[ClassTopicDraftUseCaseDependency.self] = newValue } + } +} diff --git a/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift b/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift index 78608474..15a3cad4 100644 --- a/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift +++ b/Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift @@ -8,3 +8,83 @@ extension ClassUseCaseDependency: DependencyKey { extension ClassRepositoryDependency: DependencyKey { public static var liveValue: any ClassInterface { ClassRepositoryImpl() } } + +extension ClassRoomUseCaseDependency: DependencyKey { + public static var liveValue: any ClassRoomInterface { ClassUseCaseImpl() } +} + +extension ClassRoomRepositoryDependency: DependencyKey { + public static var liveValue: any ClassRoomInterface { ClassRepositoryImpl() } +} + +extension ClassRecommendationUseCaseDependency: DependencyKey { + public static var liveValue: any ClassRecommendationInterface { ClassUseCaseImpl() } +} + +extension ClassRecommendationRepositoryDependency: DependencyKey { + public static var liveValue: any ClassRecommendationInterface { ClassRepositoryImpl() } +} + +extension ClassBattleUseCaseDependency: DependencyKey { + public static var liveValue: any ClassBattleInterface { ClassUseCaseImpl() } +} + +extension ClassBattleRepositoryDependency: DependencyKey { + public static var liveValue: any ClassBattleInterface { ClassRepositoryImpl() } +} + +extension ClassMemberUseCaseDependency: DependencyKey { + public static var liveValue: any ClassMemberInterface { ClassUseCaseImpl() } +} + +extension ClassMemberRepositoryDependency: DependencyKey { + public static var liveValue: any ClassMemberInterface { ClassRepositoryImpl() } +} + +extension ClassDebateUseCaseDependency: DependencyKey { + public static var liveValue: any ClassDebateInterface { ClassUseCaseImpl() } +} + +extension ClassDebateRepositoryDependency: DependencyKey { + public static var liveValue: any ClassDebateInterface { ClassRepositoryImpl() } +} + +extension ClassTicketUseCaseDependency: DependencyKey { + public static var liveValue: any ClassTicketInterface { ClassUseCaseImpl() } +} + +extension ClassTicketRepositoryDependency: DependencyKey { + public static var liveValue: any ClassTicketInterface { ClassRepositoryImpl() } +} + +extension ClassResultUseCaseDependency: DependencyKey { + public static var liveValue: any ClassResultInterface { ClassUseCaseImpl() } +} + +extension ClassResultRepositoryDependency: DependencyKey { + public static var liveValue: any ClassResultInterface { ClassRepositoryImpl() } +} + +extension ClassTopicDraftUseCaseDependency: DependencyKey { + public static var liveValue: any ClassTopicDraftInterface { ClassUseCaseImpl() } +} + +extension ClassTopicDraftRepositoryDependency: DependencyKey { + public static var liveValue: any ClassTopicDraftInterface { ClassRepositoryImpl() } +} + +extension ClassReportUseCaseDependency: DependencyKey { + public static var liveValue: any ClassReportInterface { ClassUseCaseImpl() } +} + +extension ClassReportRepositoryDependency: DependencyKey { + public static var liveValue: any ClassReportInterface { ClassRepositoryImpl() } +} + +extension ClassLocalAIUseCaseDependency: DependencyKey { + public static var liveValue: any ClassLocalAIInterface { ClassUseCaseImpl() } +} + +extension ClassLocalAIRepositoryDependency: DependencyKey { + public static var liveValue: any ClassLocalAIInterface { ClassRepositoryImpl() } +} From c3ce89668b20cd219dab117cb1dc17eb0601d354 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:20:10 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20Repository?= =?UTF-8?q?=EC=99=80=20UseCase=20=ED=99=95=EC=9E=A5=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Repository/ClassRepositoryImpl+Battle.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Debate.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+LocalAI.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Member.swift | 7 +++++++ .../Repository/ClassRepositoryImpl+Recommendation.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Report.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Result.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Room.swift | 7 +++++++ .../Sources/Repository/ClassRepositoryImpl+Ticket.swift | 7 +++++++ .../Repository/ClassRepositoryImpl+TopicDraft.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Battle.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Debate.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+LocalAI.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Member.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Recommendation.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Report.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Result.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Room.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+Ticket.swift | 7 +++++++ .../Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift | 7 +++++++ 20 files changed, 140 insertions(+) diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift index 9da08b0a..cbd1ae18 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Battle.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Battle.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift index 5ccc3592..0e0ad010 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Debate.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Debate.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift index 0bb3487e..3d67e335 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+LocalAI.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+LocalAI.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift index 7cec54c4..98eda7e0 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Member.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Member.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift index 8033af76..040b152f 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Recommendation.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Recommendation.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift index db38ac30..2e9da22e 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Report.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Report.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift index 29879042..6f7c8789 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Result.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Result.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift index 5ae7e527..a79df96e 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Room.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift index 9642e6d5..60820b94 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Ticket.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+Ticket.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift index 02c42d0d..bc17a59c 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+TopicDraft.swift @@ -1,3 +1,10 @@ +// +// ClassRepositoryImpl+TopicDraft.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import APIEndpoint import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift index dee2f317..9434363c 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Battle.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Battle.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift index 7d0075c7..7ebd1a8c 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Debate.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Debate.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift index d432fe67..a4296451 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+LocalAI.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+LocalAI.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift index 974a84e9..8018d456 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Member.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Member.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift index aa4f5d9e..68b196d1 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Recommendation.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Recommendation.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift index a052adc5..39328de6 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Report.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Report.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift index dd6c38e3..6b6d188b 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Result.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Result.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift index 3601f213..60bde223 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Room.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift index 3293c5d1..b7fe8dff 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Ticket.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+Ticket.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift index 9676095c..2bfeda59 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+TopicDraft.swift @@ -1,3 +1,10 @@ +// +// ClassUseCaseImpl+TopicDraft.swift +// ClassDomain +// +// Created by Picke on 10/26/26. +// + import ClassDomainInterface import Foundation From 6372dc108eca9aa03adbd70bcb37e004e82a3128 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:22:22 +0900 Subject: [PATCH 10/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EA=B3=BC=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9D=84=20API=20=EB=AA=85=EC=84=B8=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/Room/ClassCreation.swift | 9 ++++- .../Default/MockClassRepository.swift | 40 ++++++++++++++++++- .../Repository/Room/ClassRoomInterface.swift | 12 ++++++ .../Repository/ClassRepositoryImpl+Room.swift | 13 ++++++ .../UseCase/ClassUseCaseImpl+Room.swift | 4 ++ .../Sources/Class/Room/ClassroomService.swift | 9 +++-- .../Room/Request/ClassroomRequests.swift | 5 ++- 7 files changed, 85 insertions(+), 7 deletions(-) diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift index 0f9cfbe6..c026d7e0 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Entity/Room/ClassCreation.swift @@ -15,6 +15,7 @@ public struct ClassCreation: Equatable, Sendable { public var allowsAnonymousOpinion: Bool public var requiresComment: Bool public var ownerParticipates: Bool + public var ownerDisplayName: String? public init( name: String, @@ -22,7 +23,8 @@ public struct ClassCreation: Equatable, Sendable { battleId: Int, allowsAnonymousOpinion: Bool, requiresComment: Bool, - ownerParticipates: Bool = false + ownerParticipates: Bool = false, + ownerDisplayName: String? = nil ) { self.name = name self.deadline = deadline @@ -31,6 +33,7 @@ public struct ClassCreation: Equatable, Sendable { self.allowsAnonymousOpinion = allowsAnonymousOpinion self.requiresComment = requiresComment self.ownerParticipates = ownerParticipates + self.ownerDisplayName = ownerDisplayName } public init( @@ -39,7 +42,8 @@ public struct ClassCreation: Equatable, Sendable { topicVersionId: Int, allowsAnonymousOpinion: Bool, requiresComment: Bool, - ownerParticipates: Bool = false + ownerParticipates: Bool = false, + ownerDisplayName: String? = nil ) { self.name = name self.deadline = deadline @@ -48,5 +52,6 @@ public struct ClassCreation: Equatable, Sendable { self.allowsAnonymousOpinion = allowsAnonymousOpinion self.requiresComment = requiresComment self.ownerParticipates = ownerParticipates + self.ownerDisplayName = ownerDisplayName } } diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift index 8739f0cc..2e7f95ef 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Default/MockClassRepository.swift @@ -249,7 +249,11 @@ public actor MockClassRepository: ClassInterface { rooms.append(room) reports[id] = nil dashboards[id] = nil - membersByRoom[id] = [ClassMember(id: 1, name: "운영자", isOwner: true)] + membersByRoom[id] = [ClassMember( + id: 1, + name: creation.ownerParticipates ? (creation.ownerDisplayName ?? "운영자") : "운영자", + isOwner: true + )] currentMemberIDs[id] = 1 nextMemberIDs[id] = 2 return room @@ -343,6 +347,40 @@ public actor MockClassRepository: ClassInterface { return updated } + public func updateName(id: Int, name: String) async throws -> ClassRoom { + guard let index = rooms.firstIndex(where: { $0.id == id }) else { + throw ClassError.invalidCode + } + let room = rooms[index] + guard room.role == .owner else { + throw ClassError.unknown("클래스 관리자만 이름을 변경할 수 있습니다.") + } + guard room.status == .open else { throw ClassError.closed } + let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, trimmed.count <= 20 else { + throw ClassError.unknown("클래스 이름은 1~20자여야 합니다.") + } + let updated = ClassRoom( + id: room.id, + name: trimmed, + joinCode: room.joinCode, + battle: room.battle, + deadline: room.deadline, + closeAt: room.closeAt, + memberCount: room.memberCount, + role: room.role, + status: room.status, + allowsAnonymousOpinion: room.allowsAnonymousOpinion, + requiresComment: room.requiresComment, + ownerParticipates: room.ownerParticipates, + memberCapacity: room.memberCapacity, + battleType: room.battleType, + ticketExpiresAt: room.ticketExpiresAt + ) + rooms[index] = updated + return updated + } + public func deleteClass(id: Int) async throws { guard let index = rooms.firstIndex(where: { $0.id == id }) else { throw ClassError.invalidCode diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift index ec2a7371..748b4666 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Room/ClassRoomInterface.swift @@ -1,3 +1,10 @@ +// +// ClassRoomInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation @@ -9,11 +16,16 @@ public protocol ClassRoomInterface: Sendable { func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview func joinClass(joinCode: String, nickname: String) async throws -> ClassRoom func updateDeadline(id: Int, deadline: Date) async throws -> ClassRoom + func updateName(id: Int, name: String) async throws -> ClassRoom func deleteClass(id: Int) async throws func leaveClass(id: Int) async throws } public extension ClassRoomInterface { + func updateName(id _: Int, name _: String) async throws -> ClassRoom { + throw ClassError.unknown("클래스 이름 변경을 지원하지 않습니다.") + } + func fetchClassPreview(joinCode: String) async throws -> ClassJoinPreview { let room = try await fetchClass(joinCode: joinCode) return ClassJoinPreview(room: room) diff --git a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift index a79df96e..51d3848d 100644 --- a/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift +++ b/Projects/Domain/ClassDomain/Sources/Repository/ClassRepositoryImpl+Room.swift @@ -79,6 +79,19 @@ extension ClassRepositoryImpl { } } + public func updateName(id: Int, name: String) async throws -> ClassRoom { + do { + _ = try await client.send( + ClassroomService.updateName(id: id, name: name), + as: PickeEmptyResponse.self + ) + let data = try await client.send(ClassroomService.detail(id), as: ClassDetailDataDTO.self) + return try data.toDomain() + } catch { + throw Self.classError(error) + } + } + public func deleteClass(id: Int) async throws { do { _ = try await client.send(ClassroomService.delete(id), as: PickeEmptyResponse.self) diff --git a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift index 60bde223..6af986d7 100644 --- a/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift +++ b/Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl+Room.swift @@ -37,6 +37,10 @@ extension ClassUseCaseImpl { try await repository.updateDeadline(id: id, deadline: deadline) } + public func updateName(id: Int, name: String) async throws -> ClassRoom { + try await repository.updateName(id: id, name: name) + } + public func deleteClass(id: Int) async throws { try await repository.deleteClass(id: id) } diff --git a/Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift b/Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift index 6ab0e499..74b634d4 100644 --- a/Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift +++ b/Projects/Service/APIEndpoint/Sources/Class/Room/ClassroomService.swift @@ -10,6 +10,7 @@ public enum ClassroomService { case create(ClassCreation) case detail(Int) case updateDeadline(id: Int, deadline: Date) + case updateName(id: Int, name: String) case delete(Int) case joinPreview(String) case join(code: String, nickname: String) @@ -23,7 +24,7 @@ extension ClassroomService: PickeDataRequest { switch self { case .myClasses, .create: return ClassroomAPI.classes.description - case let .detail(id), let .delete(id), let .updateDeadline(id, _): + case let .detail(id), let .delete(id), let .updateDeadline(id, _), let .updateName(id, _): return ClassroomAPI.detail(classID: id).description case .joinPreview: return ClassroomAPI.joinPreview.description @@ -38,7 +39,7 @@ extension ClassroomService: PickeDataRequest { switch self { case .myClasses, .detail, .joinPreview: return .get case .create, .join: return .post - case .updateDeadline: return .patch + case .updateDeadline, .updateName: return .patch case .delete, .leave: return .delete } } @@ -48,7 +49,9 @@ extension ClassroomService: PickeDataRequest { case let .create(creation): return CreateClassBody(creation: creation) case let .updateDeadline(_, deadline): - return UpdateClassBody(deadline: deadline.formatted(.serverDateTime)) + return UpdateClassBody(name: nil, deadline: deadline.formatted(.serverDateTime)) + case let .updateName(_, name): + return UpdateClassBody(name: name, deadline: nil) case let .joinPreview(code): return ClassJoinPreviewQuery(inviteCode: code) case let .join(code, nickname): diff --git a/Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift b/Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift index f6b51a28..095b46d8 100644 --- a/Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift +++ b/Projects/Service/APIEndpoint/Sources/Class/Room/Request/ClassroomRequests.swift @@ -9,6 +9,7 @@ struct CreateClassBody: Encodable, Sendable { let deadline: String? let commentRequired: Bool let ownerParticipates: Bool + let ownerDisplayName: String? init(creation: ClassCreation) { name = creation.name @@ -19,11 +20,13 @@ struct CreateClassBody: Encodable, Sendable { : creation.deadline.formatted(.serverDateTime) commentRequired = creation.requiresComment ownerParticipates = creation.ownerParticipates + ownerDisplayName = creation.ownerDisplayName } } struct UpdateClassBody: Encodable, Sendable { - let deadline: String + let name: String? + let deadline: String? } struct ClassJoinPreviewQuery: Encodable, Sendable { From 9fbd4c89a49e9bc348320e3a1dd7c9d5496de57e Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:22:27 +0900 Subject: [PATCH 11/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=EB=B3=84=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=ED=8C=8C=EC=9D=BC=20=ED=97=A4=EB=8D=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Repository/Battle/ClassBattleInterface.swift | 7 +++++++ .../Sources/Repository/Debate/ClassDebateInterface.swift | 7 +++++++ .../Sources/Repository/LocalAI/ClassLocalAIInterface.swift | 7 +++++++ .../Sources/Repository/Member/ClassMemberInterface.swift | 7 +++++++ .../Recommendation/ClassRecommendationInterface.swift | 7 +++++++ .../Sources/Repository/Report/ClassReportInterface.swift | 7 +++++++ .../Sources/Repository/Result/ClassResultInterface.swift | 7 +++++++ .../Sources/Repository/Ticket/ClassTicketInterface.swift | 7 +++++++ .../Repository/TopicDraft/ClassTopicDraftInterface.swift | 7 +++++++ 9 files changed, 63 insertions(+) diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift index 78fe669e..c78b435e 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Battle/ClassBattleInterface.swift @@ -1,3 +1,10 @@ +// +// ClassBattleInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift index 1c283bd7..976657b6 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Debate/ClassDebateInterface.swift @@ -1,3 +1,10 @@ +// +// ClassDebateInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift index 1f3070b8..e01e5d1b 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/LocalAI/ClassLocalAIInterface.swift @@ -1,3 +1,10 @@ +// +// ClassLocalAIInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift index ce3f6ea3..21c1c53c 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Member/ClassMemberInterface.swift @@ -1,3 +1,10 @@ +// +// ClassMemberInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift index ee4d3410..34818387 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Recommendation/ClassRecommendationInterface.swift @@ -1,3 +1,10 @@ +// +// ClassRecommendationInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift index d6ffa317..585ff0ab 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Report/ClassReportInterface.swift @@ -1,3 +1,10 @@ +// +// ClassReportInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift index f5a890d0..dac4b067 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Result/ClassResultInterface.swift @@ -1,3 +1,10 @@ +// +// ClassResultInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift index d1bae0ef..0af86942 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/Ticket/ClassTicketInterface.swift @@ -1,3 +1,10 @@ +// +// ClassTicketInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation diff --git a/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift b/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift index da033aec..f392eda3 100644 --- a/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift +++ b/Projects/Domain/ClassDomain/Interface/Sources/Repository/TopicDraft/ClassTopicDraftInterface.swift @@ -1,3 +1,10 @@ +// +// ClassTopicDraftInterface.swift +// ClassDomainInterface +// +// Created by Picke on 10/26/26. +// + import ComposableArchitecture import Foundation From 180bf6288e6fc035098c46b7ab7541e6e5d01d31 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:34:20 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=ED=99=95=EC=A0=95=20=EC=8B=9C=EC=95=88=EA=B3=BC=20?= =?UTF-8?q?AI=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 참여 화면 배경과 입력 칸 상태 및 키보드 초점을 확정 시안에 맞춥니다. AI 피드백 DTO의 중첩 투표 선택지 타입을 명시합니다. --- .../Sources/Model/Mapper/ClassAIFeedbackDTO+.swift | 4 ++-- .../Class/Sources/ClassJoin/View/ClassJoinView.swift | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift index bee45cba..ffae479f 100644 --- a/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift +++ b/Projects/Domain/ClassDomain/Sources/Model/Mapper/ClassAIFeedbackDTO+.swift @@ -20,8 +20,8 @@ extension ClassPersonalReportDTO.Feedback.Evidence { extension ClassPersonalReportDTO.Feedback { func toDomain( - preOption: VoteOption?, - postOption: VoteOption?, + preOption: ClassPersonalReportDTO.VoteOption?, + postOption: ClassPersonalReportDTO.VoteOption?, perspectiveCount: Int, commentCount: Int ) -> ClassAIPersonalFeedback { diff --git a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift index 6e616f7d..13c6b38e 100644 --- a/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift +++ b/Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift @@ -15,6 +15,7 @@ import SwiftUI public struct ClassJoinView: View { @Bindable public var store: StoreOf @FocusState private var isCodeFocused: Bool + @FocusState private var isNicknameFocused: Bool public init(store: StoreOf) { self.store = store @@ -52,8 +53,12 @@ public struct ClassJoinView: View { } .frame(maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .ignoresSafeArea(.container, edges: store.mode == .code ? .bottom : []) - .screenBackground(.surfaceBeigeDefault) + .screenBackground(.white) .hidesSystemBars() + .onAppear { + isCodeFocused = store.mode == .code + isNicknameFocused = store.mode != .code + } } } @@ -86,7 +91,7 @@ private extension ClassJoinView { .foregroundStyle(.gray800) .frame(maxWidth: .infinity) .frame(height: 50) - .background(.bgSubtle) + .background(index < store.joinCode.count ? .bgSubtle : .white) .overlay { RoundedRectangle(cornerRadius: 2) .strokeBorder(codeBorderColor(at: index), lineWidth: 1) @@ -145,6 +150,7 @@ private extension ClassJoinView { TextField("", text: $store.nickname, prompt: Text(store.nicknamePlaceholder).foregroundStyle(.gray300)) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .focused($isNicknameFocused) .accessibilityLabel("클래스에서 사용할 이름") .pickeTextField(height: 52) .overlay { From 7e5c7de9a8d9fad89ca2dffa3e6d5fb7208a4f06 Mon Sep 17 00:00:00 2001 From: Roy Date: Mon, 26 Oct 2026 11:38:41 +0900 Subject: [PATCH 13/13] =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20SwiftUI=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=20=ED=91=9C=EC=8B=9C=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 숨겨둔 TabView 탭바를 다시 표시하고 별도 커스텀 탭바를 제거합니다. 키보드 및 상세 화면에서 탭바를 숨기는 기존 상태 조건을 유지합니다. --- .../Class/ClassCoordinatorView.swift | 1 - .../Navigation/MainTab/MainTabBar.swift | 69 ------------------- .../Navigation/MainTab/MainTabView.swift | 17 +---- 3 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 Projects/App/Sources/Navigation/MainTab/MainTabBar.swift diff --git a/Projects/App/Sources/Navigation/Class/ClassCoordinatorView.swift b/Projects/App/Sources/Navigation/Class/ClassCoordinatorView.swift index aeb9bea9..2601e820 100644 --- a/Projects/App/Sources/Navigation/Class/ClassCoordinatorView.swift +++ b/Projects/App/Sources/Navigation/Class/ClassCoordinatorView.swift @@ -114,7 +114,6 @@ public struct ClassCoordinatorView: View { } } } - .toolbar(.hidden, for: .tabBar) } } diff --git a/Projects/App/Sources/Navigation/MainTab/MainTabBar.swift b/Projects/App/Sources/Navigation/MainTab/MainTabBar.swift deleted file mode 100644 index faa9d18c..00000000 --- a/Projects/App/Sources/Navigation/MainTab/MainTabBar.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// MainTabBar.swift -// Picke -// -// Created by Picke on 10/17/26. -// - -import ComposableArchitecture -import PickeDesignKit -import SwiftUI - -struct MainTabBar: View { - @Bindable var store: StoreOf - - var body: some View { - tabButtons() - .padding(.horizontal, 21) - .background { - Color.white - .ignoresSafeArea(.container, edges: .bottom) - .shadow(color: .black.opacity(0.12), radius: 6, x: 0, y: -2) - } - } -} - -extension MainTabBar { - @ViewBuilder - private func tabButtons() -> some View { - HStack(spacing: 0) { - ForEach(MainTabCoordinator.Tab.allCases, id: \.rawValue) { tab in - tabButton(tab) - } - } - } - - private func tabButton(_ tab: MainTabCoordinator.Tab) -> some View { - Button { - store.send(.tabTapped(tab.rawValue)) - } label: { - tabLabel(tab) - } - .buttonStyle(.plain) - .accessibilityLabel(tab.title) - .accessibilityAddTraits(store.state.isSelected(tab) ? .isSelected : []) - .accessibilityIdentifier("tab.\(tab.rawValue)") - } - - @ViewBuilder - private func tabLabel(_ tab: MainTabCoordinator.Tab) -> some View { - VStack(spacing: 8) { - Image(asset: tab.iconAsset(isSelected: store.state.isSelected(tab))) - .renderingMode(.original) - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - .accessibilityHidden(true) - - Text(tab.title) - .pretendardFont(.labelSmall) - .foregroundStyle(store.state.isSelected(tab) ? .neutral900 : .gray200) - .lineLimit(1) - .frame(height: 16) - } - .padding(.top, 12) - .padding(.bottom, 1) - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) - } -} diff --git a/Projects/App/Sources/Navigation/MainTab/MainTabView.swift b/Projects/App/Sources/Navigation/MainTab/MainTabView.swift index 932270bc..a8aca3d5 100644 --- a/Projects/App/Sources/Navigation/MainTab/MainTabView.swift +++ b/Projects/App/Sources/Navigation/MainTab/MainTabView.swift @@ -15,7 +15,6 @@ import TCAFlow public struct MainTabView: View { @Bindable private var store: StoreOf - @State private var hasMeasuredKeyboard = false public init(store: StoreOf) { PickeTabBarAppearance.configure() @@ -24,29 +23,18 @@ public struct MainTabView: View { public var body: some View { ZStack { - tabLayout() + tabRouter MainTabKeyboardObserver { isVisible in if store.isKeyboardVisible != isVisible { store.send(.keyboardVisibilityChanged(isVisible)) } - hasMeasuredKeyboard = true } } } } private extension MainTabView { - @ViewBuilder - func tabLayout() -> some View { - VStack(spacing: 0) { - tabRouter - if hasMeasuredKeyboard, !store.shouldHideTabBar { - MainTabBar(store: store) - } - } - } - var tabRouter: some View { TCAFlowTabRouter( selectedTab: $store.selectedTab.sending(\.selectTab), @@ -65,10 +53,9 @@ private extension MainTabView { } ) { tabContent(for: $0) - .toolbar(.hidden, for: .tabBar) + .toolbar(store.shouldHideTabBar ? .hidden : .visible, for: .tabBar) } .tint(.neutral900) - .toolbar(.hidden, for: .tabBar) } @ViewBuilder