Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Projects/App/Sources/Navigation/MainTab/MainTabCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public struct MainTabCoordinator {
case .classroom:
return !classState.showsTabBar
case .myPage:
if case .ticket? = myPageState.routes.last?.screen { return false }
return myPageState.routes.count > 1
case .none:
return false
Expand Down Expand Up @@ -174,6 +175,17 @@ public struct MainTabCoordinator {
state.selectedTab = state.backDestinationTab
return .none

case .classroom(.router(.routeAction(0, action: .intro(.delegate(.ticket))))):
state.classState.routes.goBackToRoot()
state.myPageState.routes.goBackToRoot()
state.myPageState.routes.push(.ticket(.init()))
if state.selectedTab != Tab.myPage.rawValue {
state.previousTab = state.selectedTab
trackTabSelection(Tab.myPage.rawValue)
}
state.selectedTab = Tab.myPage.rawValue
return .none

case .myPage(.router(.routeAction(_, action: .settings(.delegate(.sessionEnded))))):
return .send(.delegate(.sessionEnded))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct ProfileTicketCoordinatorTests {

#expect(store.state.selectedTab == myPageTab)
#expect(store.state.myPageState.routes.count == 2)
#expect(store.state.shouldHideTabBar)
#expect(!store.state.shouldHideTabBar)
guard case let .ticket(ticket)? = store.state.myPageState.routes.last?.screen else {
Issue.record("이용권 등록 화면이 열리지 않았습니다")
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct ClassPersonalReportDTO: Decodable, Sendable {

}

let state: String
let state: ClassAIGenerationState

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

ClassAIFeedbackDTO 및 해당 DTO에서 사용되는 ClassAIGenerationState가 Domain 모듈에 위치하는 것은 클린 아키텍처의 계층 의존성 규칙(Presentation → Domain ← Data)을 위반합니다. Domain 레이어는 데이터 전송 객체(DTO)를 직접 알아서는 안 되며, DTO와 DTO를 위한 타입 정의는 Data 레이어에 속해야 합니다.

Suggested change
let state: ClassAIGenerationState
// Projects/Data/ClassData/Sources/Model/DTO/ClassAIFeedbackDTO.swift
import Foundation
import DomainClassDomainInterface
struct ClassPersonalReportDTO: Decodable, Sendable {
// ...
let state: ClassAIGenerationState // ClassAIGenerationState도 Data 모듈로 이동
// ...
}
// Projects/Data/ClassData/Sources/Model/DTO/ClassAIGenerationState.swift
import Foundation
enum ClassAIGenerationState: Decodable, Equatable, Sendable {
// ... (기존 코드 유지)
}

let feedback: Feedback?
let preOption: VoteOption?
let postOption: VoteOption?
Expand All @@ -52,7 +52,7 @@ struct ClassInsightDTO: Decodable, Sendable {

}

let state: String
let state: ClassAIGenerationState

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

ClassInsightDTO가 Domain 모듈에 위치하는 것은 클린 아키텍처의 계층 의존성 규칙을 위반합니다. DTO는 Data 레이어에 속해야 합니다. 이에 대한 자세한 내용은 LINE 37의 코멘트를 참고해 주세요.

let retryable: Bool
let insightText: String?
let evidence: [Evidence]?
Expand All @@ -62,5 +62,5 @@ struct ClassInsightDTO: Decodable, Sendable {
}

struct ClassInsightRetryDTO: Decodable, Sendable {
let state: String
let state: ClassAIGenerationState

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

ClassInsightRetryDTO가 Domain 모듈에 위치하는 것은 클린 아키텍처의 계층 의존성 규칙을 위반합니다. DTO는 Data 레이어에 속해야 합니다. 이에 대한 자세한 내용은 LINE 37의 코멘트를 참고해 주세요.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

enum ClassAIGenerationState: Decodable, Equatable, Sendable {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

ClassAIGenerationState는 DTO의 상태를 표현하는 타입이므로 Data 레이어에 정의되어야 합니다. 현재 Domain 모듈에 위치하는 것은 계층 의존성 원칙에 위배됩니다. 이에 대한 자세한 내용은 ClassAIFeedbackDTO.swift LINE 37의 코멘트를 참고하여 Data 레이어로 이동시키는 것을 권고합니다.

case beforeClose
case pending
case generating
case succeeded
case limited
case notEligible
case failed
case unknown

init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self)
switch value {
case "BEFORE_CLOSE": self = .beforeClose
case "PENDING": self = .pending
case "GENERATING": self = .generating
case "SUCCEEDED": self = .succeeded
case "LIMITED": self = .limited
case "NOT_ELIGIBLE": self = .notEligible
case "FAILED": self = .failed
default: self = .unknown
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,21 @@ extension ClassPersonalReportDTO.Feedback {
extension ClassPersonalReportDTO {
func toDomain() throws -> ClassAIPersonalFeedback {
switch state {
case "SUCCEEDED", "LIMITED":
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":
case .beforeClose, .pending, .generating:
throw ClassAIFeedbackError.modelNotReady
case "NOT_ELIGIBLE":
case .notEligible:
throw ClassAIFeedbackError.insufficientInput
case "FAILED":
case .failed:
throw ClassAIFeedbackError.generationFailed
default:
case .unknown:
throw ClassAIFeedbackError.generationFailed
}
}
Expand All @@ -82,7 +82,7 @@ extension ClassInsightDTO.Evidence {
extension ClassInsightDTO {
func toDomain() throws -> ClassAIInsight {
switch state {
case "SUCCEEDED", "LIMITED":
case .succeeded, .limited:
guard let insightText, !insightText.isEmpty else { throw ClassAIFeedbackError.generationFailed }
return ClassAIInsight(
text: insightText,
Expand All @@ -91,11 +91,11 @@ extension ClassInsightDTO {
coverage: coverage,
excludedCount: excludedCount ?? 0
)
case "BEFORE_CLOSE", "PENDING", "GENERATING":
case .beforeClose, .pending, .generating:
throw ClassAIFeedbackError.modelNotReady
case "FAILED":
case .failed:
throw retryable ? ClassAIFeedbackError.serverRetryAvailable : .generationFailed
default:
case .notEligible, .unknown:
throw ClassAIFeedbackError.generationFailed
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension ClassRepositoryImpl {
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 }
guard payload.state == .pending else { throw ClassAIFeedbackError.generationFailed }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [P2] Major

ClassRepositoryImpl과 같은 Repository의 구현체는 Data 레이어에 속해야 합니다. 현재 Projects/Domain/ClassDomain/Sources/Repository 경로에 위치하는 것은 클린 아키텍처의 계층 의존성 규칙을 위반합니다. Domain 레이어는 DomainInterface에 정의된 Repository 프로토콜에만 의존해야 합니다.

Suggested change
guard payload.state == .pending else { throw ClassAIFeedbackError.generationFailed }
// Projects/Data/ClassData/Sources/Repository/ClassRepositoryImpl.swift
import DomainClassDomain
import Network
final class ClassRepositoryImpl: ClassRepository {
// ...
}

} catch {
throw Self.feedbackError(error)
}
Expand Down
16 changes: 14 additions & 2 deletions Projects/Feature/Class/Sources/ClassJoin/View/ClassJoinView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public struct ClassJoinView: View {
public var body: some View {
VStack(spacing: 0) {
PickeNavigationBar(
onBack: { send(.backTapped) },
onBack: {
resignInputFocus()
send(.backTapped)
},
centerTitle: store.navigationCenterTitle,
leadingTitle: store.navigationLeadingTitle,
leadingSpacing: 12
Expand Down Expand Up @@ -59,6 +62,7 @@ public struct ClassJoinView: View {
isCodeFocused = store.mode == .code
isNicknameFocused = store.mode != .code
}
.onDisappear(perform: resignInputFocus)
}
}

Expand Down Expand Up @@ -169,10 +173,18 @@ private extension ClassJoinView {
}

var bottomAction: some View {
Button { send(store.mode == .code ? .findTapped : .joinTapped) } label: {
Button {
resignInputFocus()
send(store.mode == .code ? .findTapped : .joinTapped)
} label: {
Text(store.primaryButtonTitle)
}
.ctaButtonStyle(.primary, size: .large, height: 52)
.disabled(store.mode == .code ? !store.canFindClass : !store.canJoin)
}

func resignInputFocus() {
isCodeFocused = false
isNicknameFocused = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public struct ClassTicketView: View {
}
.frame(maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.screenBackground(.surfaceBeigeDefault)
.hidesSystemBars()
.toolbar(.hidden, for: .navigationBar)
.onAppear { send(.onAppear) }
}
}
Expand Down
2 changes: 0 additions & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@ platform :ios do
begin
# picke/PICKE 그룹에 배포한다.
upload_to_testflight_with_retry(
api_key_path: ENV.fetch("APP_STORE_CONNECT_API_KEY_PATH"),
ipa: ipa_path,
changelog: testflight_changelog(new_build_number),
groups: ["picke", "PICKE"],
Expand Down Expand Up @@ -613,7 +612,6 @@ platform :ios do
# skip_waiting_for_build_processing: false 로 App Store Connect 처리 완료까지 기다려야
# 심사 제출 단계에서 해당 빌드를 선택할 수 있다.
upload_to_testflight_with_retry(
api_key_path: ENV.fetch("APP_STORE_CONNECT_API_KEY_PATH"),
ipa: ipa_path,
app_identifier: BUNDLE_ID,
skip_submission: true,
Expand Down
Loading