Skip to content

클래스 화면과 도메인, 내비게이션을 추가한다 - #45

Open
Roy-wonji wants to merge 40 commits into
developfrom
feature/class
Open

Roy-wonji wants to merge 40 commits into
developfrom
feature/class

Conversation

@Roy-wonji

@Roy-wonji Roy-wonji commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

요약

  • 클래스 도메인과 화면, 내비게이션 및 운영자 대시보드·상세 화면을 추가했습니다.
  • Figma 노드를 기준으로 리포트, 멤버 목록, 이름 수정·필터 모달과 공용 바텀시트를 보완했습니다.
  • ChatRoomFeature의 메시지 타임라인 계산을 UseCase로 분리하고 출력 순서를 회귀 테스트로 확인했습니다.

PR 리뷰 반영

  • 기존 마이 탭의 tab.3 식별자를 유지했습니다.
  • 운영 경로에서 목 클래스 저장소를 제거했습니다. 서버 API 계약이 없는 요청은 오류로 처리하며, 클래스 화면에 계정 데이터처럼 예시 목록을 보여주지 않습니다.
  • ClassUseCaseImpl을 의존성 키로 사용하고 테스트도 UseCase를 거쳐 저장소를 주입합니다.
  • 서버 저장 없이 멤버를 제거하거나 서로 다른 클래스에 동일한 투표·댓글·피드백을 표시하던 동작을 제거했습니다. 데이터가 없는 항목에는 빈 상태를 표시합니다.
  • 사용 불가능한 멤버 필터·미리듣기는 비활성화하고, 이용권 등록 버튼에는 준비 중 안내를 연결했습니다.
  • 운영자 화면의 공개 모델을 ClassDomainInterface의 타입별 파일로 분리했습니다.

hidesSystemBars()는 PickeDesignKit/Sources/Extension/View/View+Background.swift에 정의돼 있으며 Stage 앱 빌드가 통과했습니다. 탭바 toolbar 지정은 클래스 모달·멤버·리포트에서 숨김과 복원을 요청한 화면별 동작을 유지했습니다. isDatePickerPresented는 Reducer에서 처리할 수 있는 상태를 Reducer에 두라는 요청에 따라 유지했습니다.

검증

  • Class 및 ClassDomain 테스트 통과 (iPhone 18 Pro, iOS 27.0 시뮬레이터)
  • Chat 테스트 5개 통과
  • Picke-Stage 시뮬레이터 빌드 통과
  • git diff --check 통과. SwiftFormat은 변경된 Swift 파일 24개 중 22개 통과 (MainTabCoordinator의 기존 import 순서, ClassLiveDependencies의 기존 한 줄 DI 형식은 포맷 규칙과 충돌)
  • Fastlane ios QA 종료 코드 0. 빌드 1.1.2 (2610012307)의 아카이브·IPA 내보내기·TestFlight 업로드·처리 완료
  • 테스터 그룹 배포는 Fastlane에서 심사/그룹 단계 보류로 기록됨. 바이너리 업로드 완료와 그룹 배포 완료는 구분함

현재 데이터 범위

  • 클래스 서버 API 계약이 아직 없어 클래스의 운영 데이터 조회·생성·수정·삭제를 실제 서버에 연결하지 않았습니다.
  • 운영자 지표와 의견 화면은 예시 데이터임을 화면에 표시합니다. 피드백 등록도 서버 전송 대신 준비 중 안내를 표시합니다.
  • Figma API 상세 노드 조회가 제한되어 전 화면의 픽셀 단위 일치는 검증되지 않았습니다.

Auth·Battle·Hifi·Home·Profile·MainTab 코디네이터와 뷰, Screen 이름을 접두사 없이 맞추고 파일명과 참조를 함께 변경한다.
Rejected: 확인되지 않은 API 계약을 추측해 연결하기 | 서버 계약이 바뀌면 재작업이 발생한다.
- GNB 클래스 탭 아이콘(선택/비선택) SVG 추가
- 클래스 인트로 배경·카드 이미지 PNG 추가 (Figma 래스터 이미지)
- ImageAsset 에 클래스 케이스, AnalyticsButton 에 tabClass 추가
- Feature/Class 모듈 신설, ClassIntroFeature·ClassIntroView·ClassMenuCard 추가
- ClassCoordinator(TCAFlow) 추가, 인트로를 루트 화면으로 연결
- MainTab 에 클래스 탭(빠른 배틀과 마이 사이) 추가, 분석 이벤트·루트 리셋 포함
- FeatureAssembly 에 Class 모듈 조립
기존 Domain과 같은 의존성 등록 방식을 유지하고, 생성 후 다른 저장소 인스턴스에서 조회되는 동작을 검증한다.
- PickeSharedUI 에 PickeTabBarAppearance, PickeTabItemLabel 추가
- MainTabView 는 공용 컴포넌트를 호출하도록 정리
Figma 화면의 수준과 카테고리, 익명 의견 설정을 모델에 반영하고 공개 타입을 파일별로 분리한다.
- PickeTextField: beige50 배경 + beige600 테두리 한 줄 입력
- pickeChoiceChip: 그리드 단일 선택 칩
- PickeBoxSegment: 회색 트랙 위 박스형 세그먼트
- 찾고 싶은 주제, 대상 수준, 카테고리를 골라 추천 조건을 만든다
- 인트로의 새 클래스 만들기에서 조건 선택 화면으로 이동
- 카테고리 토글과 검색 조건 전달 테스트 추가
- PickeTextField 구조체 → TextField(prompt:) + .pickeTextField()
- PickeBoxSegment 구조체 → .pickeBoxSegment(isSelected:) + .pickeBoxSegmentTrack()
- 클래스 주제 화면 호출부 교체
- 자식 2개 이상 stack 은 @ViewBuilder func, 단일 뷰는 var 로 AGENTS.md 규칙 변경
- 포맷터가 @ViewBuilder 를 지우지 않도록 redundantViewBuilder 비활성화
- 클래스 인트로·주제 화면에 적용
- pickeCheckbox(isChecked:) 는 ComponentToken.Checkbox 토큰을 따른다
- pickePill() 은 primary50 캡슐 + primary500 텍스트
- 선택한 조건으로 추천 배틀을 불러오고 한 개를 선택한다
- 미리듣기 버튼은 두되 동작은 넣지 않는다
- 주제 화면의 검색 델리게이트를 추천 배틀 화면 push 로 연결한다
- Toggle(...).toggleStyle(.picke) 로 쓰고 toggle 토큰을 따른다
- 클래스 명, 참여 마감일, 선택한 배틀, 댓글 필수 여부를 설정한다
- 추천 배틀 선택 델리게이트를 설정 화면 push 로 연결한다
참여 코드 조회와 이름 입력, 내 클래스 필터, 클래스 상세와 멤버 목록 흐름을 추가한다. 기존 Feature Reducer의 view/async/inner/delegate 구분을 따른다.
- 클래스 생성 후 참여 코드와 선택한 배틀을 보여주고 ShareLink 로 코드를 공유한다
- 선택한 배틀 카드를 설정 / 공유 화면 공용 컴포넌트로 분리한다
기존 Class 테스트와 같은 케이스 경로 수신 형식으로 바꾸고 목록 필터 열거형을 Hashable로 선언한다.
참여 코드, 내 클래스, 상세, 멤버 목록을 ClassCoordinator에 연결하고 배틀 참여는 기존 Chat 흐름으로 이동한다. 상세의 코드 공유는 시스템 공유 기능을 사용한다.
추천 배틀 조회와 클래스 생성의 사용자 입력, 비동기 실행, 응답 반영을 각각 분리하고 관련 테스트를 갱신한다.
클래스 상세 화면에서 저장소 응답을 확인한 뒤 마감일과 삭제 경로를 갱신한다.
목 저장소의 변경 결과를 도메인과 화면 테스트로 검증한다.
클래스 경로의 모달 상태에 맞춰 탭바를 전환하고 닫힌 뒤 다시 표시한다.
앱 버전과 빌드 번호를 1.1.2와 2610012046으로 올리고 한국어 릴리스 노트의 표시 버전을 1.1.1로 갱신한다.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T13:52:25.698929Z 7881f6f New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot left a comment

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.

클래스 기능의 도메인, 화면, 내비게이션 추가에 대한 코드 리뷰입니다. 새 기능의 TCA 구조 및 Tuist 모듈 설정은 전반적으로 모범 사례를 따르고 있습니다. 특히, FlowCoordinator를 활용한 내비게이션 로직과 ViewAction 매크로, TestClock 활용은 좋은 구현 사례로 보입니다. 또한, PickeDesignKit에 추가된 SwiftUI View 확장들은 재사용성을 높이고 디자인 시스템을 잘 반영하고 있습니다.

다만, ClassRepositoryImpl에서 MockClassRepository를 직접 사용하는 부분은 클린 아키텍처의 Data 계층 원칙을 위반할 소지가 있어 개선이 필요합니다. 테스트 코드와 라이브 코드의 의존성을 명확히 분리해야 합니다.

import Foundation

public struct ClassRepositoryImpl: ClassInterface {
private static let mock = MockClassRepository()

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은 실제 서버 API 연동을 담당해야 하는 Data 계층의 구현체입니다. 그러나 현재 private static let mock = MockClassRepository()를 사용하여 MockClassRepository에 대한 의존성을 직접 가지고 있습니다. 이는 Data 계층이 Network 계층 대신 목업 데이터를 사용하도록 강제하여 클린 아키텍처의 계층 간 의존성 방향을 위반합니다.

MockClassRepository는 ClassRepositoryDependency의 testValue에 주입되어 테스트 환경에서만 사용되어야 합니다. ClassRepositoryImpl은 실제 네트워크 서비스를 의존성 주입받아 사용해야 합니다.

Suggested change
private static let mock = MockClassRepository()
@Dependency(\.classNetworkService) private var networkService // 예시: Network 계층의 서비스를 의존성 주입
public init() {}
public func fetchMyClasses() async throws -> [ClassRoom] {
// 실제 networkService를 통해 API 호출
// try await networkService.fetchMyClasses()
fatalError("ClassRepositoryImpl.fetchMyClasses() should use a real network service")
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6815262f4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

case home
case explore
case quickBattle
case classroom

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the existing My tab identifier

Inserting classroom before myPage changes the My tab's raw value and accessibility identifier from tab.3 to tab.4. The existing profile, withdrawal, and smoke Maestro workflows still tap tab.3 (scripts/maestro/profile.yaml:10, withdraw.yaml:9, and smoke.yaml:63), so they now open the Class tab and time out waiting for Profile content. Assign explicit stable raw values or update every dependent workflow in the same change.

Useful? React with 👍 / 👎.

switch alertAction {
case .presented(.confirmTapped):
guard let member = state.selectedMember else { return .none }
state.members.removeAll(where: { $0.id == member.id })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist member removals through the class domain

Confirming the destructive “내보내기” action only removes the member from this screen's transient array; no use-case or repository operation runs, and reopening the screen reconstructs all members from the unchanged room.memberCount, so the removed student immediately returns. Add a domain member model and removal operation, persist the change through classUseCase, and update state from its response rather than mutating the synthetic list locally.

AGENTS.md reference: AGENTS.md:L427-L433

Useful? React with 👍 / 👎.

Comment on lines +20 to +23
public enum ClassUseCaseDependency: TestDependencyKey {
public static var testValue: any ClassInterface {
MockClassRepository()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make ClassUseCaseImpl the dependency key

The separate existential ClassUseCaseDependency supplies MockClassRepository directly in tests, so feature tests bypass ClassUseCaseImpl and any use-case behavior added there will remain untested while test/preview composition differs from live. Make ClassUseCaseImpl itself the DependencyKey, define all live/test/preview values on it, and expose classUseCase with the concrete implementation type as required.

AGENTS.md reference: AGENTS.md:L623-L625

Useful? React with 👍 / 👎.

Comment on lines +111 to +112
case .ticketTapped:
return .send(.delegate(.ticket))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle the ticket-registration delegate

Tapping the visible “이용권 등록하기” button emits .delegate(.ticket), but a repo-wide search finds no handler for that delegate and ClassCoordinator.routerAction falls through to default, leaving the button with no observable effect. Route this delegate to the registration destination or remove/disable the control until that flow exists.

Useful? React with 👍 / 👎.

Comment on lines +129 to +130
case .previewTapped:
return .none

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Implement or remove the preview control

Every recommendation card exposes an enabled “미리듣기” button that sends previewTapped, but this reducer branch always returns .none; consequently tapping the control never starts playback or navigates anywhere. Wire the selected battle into the audio preview flow, or hide/disable the button so users are not presented with a nonfunctional action.

Useful? React with 👍 / 👎.

리포트 진입 시 탭바를 숨기고 페이지 스와이프를 Reducer의 탭 선택 액션에 연결한다. 클래스 결과 막대의 그리기 상태를 Reducer에서 관리하고 동작 테스트를 보강한다.
메시지 노출 시점과 화자 매핑을 순수 계산으로 분리하고 기존 재생 상태 처리는 유지한다.

시나리오 로드 전, 문장 노출 시점, 대사 시작 시각에 대한 회귀 테스트를 추가한다.
운영자 대시보드와 상세 화면을 연결하고 Figma 기반 클래스 UI를 보완한다.
서버 계약이 없는 예시 데이터는 표시하고 피드백 전송은 안내로 제한한다.
클래스 회귀 테스트 38개와 Picke-Stage 빌드를 확인한다.

@github-actions github-actions Bot left a comment

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.

클래스 화면과 관련된 도메인, 내비게이션, UI 컴포넌트 추가 및 기존 App 프리픽스 제거 리팩토링이 잘 수행되었습니다. 전반적으로 TCA 컨벤션과 클린 아키텍처 원칙을 준수하고 있습니다.

주요 개선점은 다음과 같습니다:

  • 미정의된 hidesSystemBars() 뷰 수정자 사용은 컴파일 오류를 일으킬 수 있는 심각한 문제입니다.
  • ClassCoordinator에서 탭바 가시성을 통합 관리하고 있으므로, 개별 뷰에서 중복으로 toolbar(for: .tabBar) 수정자를 사용하는 것은 불필요하며 제거하는 것이 좋습니다.
  • ClassSettingFeature의 isDatePickerPresented와 같은 임시 UI 상태는 SwiftUI @State로 관리하는 것이 더 적절합니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(store.modal == nil ? .visible : .hidden, for: .tabBar)

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.

🔵 [P4] Readability

ClassDetailFeature 내 모달이 표시될 때 탭바를 숨기는 로직은 이미 ClassCoordinator의 showsTabBar 계산 속성에 포함되어 있습니다. 따라서 이 toolbar 수정자는 불필요하며, ClassCoordinator의 탭바 가시성 제어 로직을 따르도록 제거하는 것이 좋습니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(.hidden, for: .tabBar)

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.

🔵 [P4] Readability

ClassMemberView는 ClassCoordinator 내에서 탐색 스택에 푸시되는 화면입니다. ClassCoordinator의 showsTabBar 로직이 ClassMemberFeature 화면에서 탭바를 숨기도록 이미 정의되어 있으므로, 이 toolbar 수정자는 불필요합니다. 탭바 가시성 제어는 코디네이터에서 일관되게 관리하는 것이 좋습니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(.visible, for: .tabBar)

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.

🔵 [P4] Readability

ClassOwnerDashboardView는 ClassCoordinator 내에서 탭바가 보이는 화면입니다. ClassCoordinator의 showsTabBar 로직이 이 화면에서 탭바를 표시하도록 이미 정의되어 있으므로, 이 toolbar 수정자는 불필요합니다. 탭바 가시성 제어는 코디네이터에서 일관되게 관리하는 것이 좋습니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(.hidden, for: .tabBar)

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.

🔵 [P4] Readability

ClassFeedbackComposeView는 ClassCoordinator 내에서 탐색 스택에 푸시되는 화면입니다. ClassCoordinator의 showsTabBar 로직이 이 화면에서 탭바를 숨기도록 이미 정의되어 있으므로, 이 toolbar 수정자는 불필요합니다. 탭바 가시성 제어는 코디네이터에서 일관되게 관리하는 것이 좋습니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(.hidden, for: .tabBar)

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.

🔵 [P4] Readability

ClassMemberDetailView는 ClassCoordinator 내에서 탐색 스택에 푸시되는 화면입니다. ClassCoordinator의 showsTabBar 로직이 이 화면에서 탭바를 숨기도록 이미 정의되어 있으므로, 이 toolbar 수정자는 불필요합니다. 탭바 가시성 제어는 코디네이터에서 일관되게 관리하는 것이 좋습니다.

public var name = ""
public var deadline: Date
public var isDeadlineEnabled = true
public var isDatePickerPresented = false

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.

🔵 [P4] Readability

isDatePickerPresented는 날짜 선택기 UI의 단순한 가시성 상태를 제어합니다. 이러한 임시적인 UI 상태(transient UI state)는 SwiftUI 뷰의 @State로 관리하는 것이 더 적절합니다. 이 상태 변경이 복잡한 비즈니스 로직이나 사이드 이펙트를 유발하지 않으므로 TCA State에 두어 테스트 커버리지를 높일 필요는 없습니다.

.padding(.bottom, 16)
}
.screenBackground()
.hidesSystemBars()

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.

🔴 [P1] Critical

.hidesSystemBars() 뷰 수정자는 이 프로젝트에서 정의되지 않았습니다. 의도한 동작에 따라 ignoresSafeArea() 또는 다른 적절한 시스템 바 숨김 API를 사용하거나, 해당 기능을 제공하는 사용자 정의 뷰 수정자를 확인해야 합니다. 정의되지 않은 수정자를 사용하면 컴파일 오류 또는 예상치 못한 런타임 동작이 발생할 수 있습니다.

.padding(.bottom, 16)
}
.screenBackground()
.hidesSystemBars()

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.

🔴 [P1] Critical

.hidesSystemBars() 뷰 수정자는 이 프로젝트에서 정의되지 않았습니다. 의도한 동작에 따라 ignoresSafeArea() 또는 다른 적절한 시스템 바 숨김 API를 사용하거나, 해당 기능을 제공하는 사용자 정의 뷰 수정자를 확인해야 합니다. 정의되지 않은 수정자를 사용하면 컴파일 오류 또는 예상치 못한 런타임 동작이 발생할 수 있습니다.

.padding(.bottom, 16)
}
.screenBackground()
.hidesSystemBars()

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.

🔴 [P1] Critical

.hidesSystemBars() 뷰 수정자는 이 프로젝트에서 정의되지 않았습니다. 의도한 동작에 따라 ignoresSafeArea() 또는 다른 적절한 시스템 바 숨김 API를 사용하거나, 해당 기능을 제공하는 사용자 정의 뷰 수정자를 확인해야 합니다. 정의되지 않은 수정자를 사용하면 컴파일 오류 또는 예상치 못한 런타임 동작이 발생할 수 있습니다.

}
.screenBackground()
.toolbar(.hidden, for: .navigationBar)
.toolbar(.visible, for: .tabBar)

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.

🔵 [P4] Readability

MyClassView는 ClassCoordinator 내에서 탭바가 보이는 화면입니다. ClassCoordinator의 showsTabBar 로직이 이 화면에서 탭바를 표시하도록 이미 정의되어 있으므로, 이 toolbar 수정자는 불필요합니다. 탭바 가시성 제어는 코디네이터에서 일관되게 관리하는 것이 좋습니다.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e72ae1a9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import Foundation

public struct ClassRepositoryImpl: ClassInterface {
private static let mock = MockClassRepository()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop using the mock repository for live class data

When the Class tab runs with live dependencies, ClassLiveDependencies.swift resolves ClassRepositoryImpl, but every operation here delegates to this process-local MockClassRepository. Production users therefore see seeded sample rooms instead of account data, and created, joined, updated, or deleted classes reset after relaunch. Implement the live provider-backed repository or keep the Class tab gated until the API is available.

Useful? React with 👍 / 👎.

Comment on lines +207 to +208
voteBar("사전 투표 결과", primary: "낮춰야 한다", secondary: "교정이 우선이다", fraction: 0.67, color: .secondary500)
voteBar("사후 투표 결과", primary: "낮춰야 한다", secondary: "교정이 우선이다", fraction: 0.49, color: .primary500)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace hard-coded report results with room data

For any room other than the juvenile-justice sample, the heading uses store.room.battle while these vote labels and percentages remain fixed. For example, opening the report for the included art-class room displays results about lowering the juvenile age threshold, presenting unrelated fabricated results as that class's report. Load report values through the class domain or clearly disable this report until data exists.

Useful? React with 👍 / 👎.

Comment on lines +40 to +42
public var visibleMembers: [Member] {
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
return query.isEmpty ? members : members.filter { $0.name.localizedCaseInsensitiveContains(query) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the selected member filters

After the user selects participation, completion, attendance, or sort options and taps “적용하기”, the reducer stores them in appliedFilter, but visibleMembers consults only searchText. Consequently every filter and sort control closes successfully without changing the member list. Apply appliedFilter here using corresponding member attributes, or remove the controls until that data is available.

Useful? React with 👍 / 👎.

Comment on lines +9 to +13
public enum Tab: String, CaseIterable, Hashable {
case home = "홈"
case members = "멤버"
case opinions = "의견"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move dashboard screen models into the entity layer

This feature declares Tab, OpinionFilter, MemberSort, Member, and Opinion directly in the reducer file. The repository architecture requires screen models and UI-branching enums to live in the domain entity module, with one public type per file; move these types into separate ClassDomainInterface/Sources/Entity files and reference them from the feature.

AGENTS.md reference: AGENTS.md:L427-L431

Useful? React with 👍 / 👎.

클래스 데이터의 목 응답을 운영 경로에서 제거하고 UseCase 의존성 및 화면 모델을 정리합니다.
멤버·리포트의 가짜 상태 변경을 제거하고 탭 식별자와 준비 중인 버튼 동작을 보완합니다.

@github-actions github-actions Bot left a comment

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.

클래스 도메인 유스케이스의 아키텍처 의존성 노출 문제가 발견되었습니다. 유스케이스 구현체(ClassUseCaseImpl)가 인터페이스 모듈에 위치하여, 해당 모듈을 사용하는 상위 레이어에서 구체 타입에 직접 의존하게 됩니다. 이는 추상화 계층 분리 원칙을 위반하므로, 구현체를 구현 모듈로 이동하고 의존성 주입 시에는 추상화된 프로토콜 타입을 사용하도록 수정해야 합니다.

import ComposableArchitecture
import Foundation

public struct ClassUseCaseImpl: ClassInterface, DependencyKey {

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

ClassUseCaseImpl는 ClassInterface의 구체적인 구현체이므로, ClassDomain 모듈의 Interface 하위 디렉토리가 아닌 Sources 하위 디렉토리에 위치해야 합니다. 현재 위치에서는 ClassDomainInterface를 의존하는 다른 모듈들이 유스케이스의 구체 구현체에 직접 의존하게 되어 도메인 레이어의 추상화 원칙을 위반합니다.

구현체(ClassUseCaseImpl)는 Projects/Domain/ClassDomain/Sources/UseCase 경로로 이동하고, ClassUseCaseImpl을 DependencyKey로 등록하는 코드는 Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift에서 이루어져야 합니다. 또한, Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift 파일의 DependencyValues 익스텐션에서 classUseCase는 ClassUseCaseImpl 타입이 아닌 any ClassInterface 타입으로 정의되어야 합니다.

Suggested change
public struct ClassUseCaseImpl: ClassInterface, DependencyKey {
// Projects/Domain/ClassDomain/Sources/UseCase/ClassUseCaseImpl.swift
public struct ClassUseCaseImpl: ClassInterface {
// ...
}
// Projects/Domain/ClassDomain/Sources/ClassLiveDependencies.swift
import ClassDomainInterface
import ComposableArchitecture
extension ClassRepositoryDependency: DependencyKey {
public static var liveValue: any ClassInterface { ClassRepositoryImpl() }
}
enum ClassUseCaseKey: DependencyKey {
static var liveValue: any ClassInterface { ClassUseCaseImpl() }
static var testValue: any ClassInterface { MockClassRepository() } // 테스트용 mock or stub
}
// Projects/Domain/ClassDomain/Interface/Sources/Repository/ClassInterface.swift
public extension DependencyValues {
var classRepository: any ClassInterface {
get { self[ClassRepositoryDependency.self] }
set { self[ClassRepositoryDependency.self] = newValue }
}
var classUseCase: any ClassInterface { // ClassUseCaseImpl이 아닌 추상화된 프로토콜 타입으로 변경
get { self[ClassUseCaseKey.self] }
set { self[ClassUseCaseKey.self] = newValue }
}
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb61493e88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +9 to +10
public func fetchMyClasses() async throws -> [ClassRoom] {
throw ClassError.network(Self.unavailableMessage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate the Class tab until its live backend exists

With live dependencies, every data-backed path exposed by the new Class tab reaches this implementation, but all repository methods unconditionally throw 클래스 서버 API가 연결되지 않았습니다.; users therefore cannot list or join classes, load recommendations, create a class, or update one. Fresh evidence since the earlier review is that the live repository now throws instead of returning mock data while the tab remains enabled, so either connect the provider-backed implementation or gate the tab until it is available.

Useful? React with 👍 / 👎.

Comment on lines +162 to +164
case .routeAction(_, action: .ownerDashboard(.delegate(.openReplyDetail))):
guard case let .ownerDashboard(dashboard)? = state.routes.last?.screen else { return .none }
state.routes.push(.ownerReplyDetail(.init(room: dashboard.room)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the selected opinion when opening replies

When an owner taps any opinion row, the dashboard delegate carries the selected ClassOwnerOpinion, but this route pattern discards it and initializes the reply screen with only the room. Since ClassReplyDetailFeature.State then supplies the same default replies, every opinion opens an identical detail screen; pass the opinion or its identifier into the destination state so the selected thread can be displayed.

Useful? React with 👍 / 👎.

Comment on lines +64 to +71
public var visibleOpinions: [ClassOwnerOpinion] {
switch opinionFilter {
case .all, .inProgress:
return opinions
case .recommended:
return opinions.sorted { $0.replyCount > $1.replyCount }
case .replies:
return opinions.sorted { $0.replyCount > $1.replyCount }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Implement the distinct opinion filters

On the owner Opinions tab, selecting “추천순” and “대댓글 많은순” produces exactly the same replyCount ordering, while “진행 중” returns the same collection as “전체”. Because all four chips are enabled, three visibly distinct controls currently provide only two behaviors; add the missing recommendation/status fields and predicates or disable the unsupported filters.

Useful? React with 👍 / 👎.


@ObservableState
public struct State: Equatable {
public struct Member: Equatable, Identifiable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the remaining member models into entity files

Member remains a screen model nested inside ClassMemberFeature.State, and the same reducer file also declares the participation, completion, attendance, and sort enums. These types must live in the domain entity module as separate public type files rather than inside a Feature reducer, matching the repository's required model boundary.

AGENTS.md reference: AGENTS.md:L427-L431

Useful? React with 👍 / 👎.


@ViewAction(for: ClassReportFeature.self)
public struct ClassReportView: View {
public let store: StoreOf<ClassReportFeature>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Declare every TCA view store as Bindable

ClassReportView stores its TCA store as a plain let; ClassMemberDetailView and ClassReplyDetailView do the same. The project requires @Bindable even for read-only stores so future bindings do not require changing the view's storage contract; update these declarations consistently.

AGENTS.md reference: AGENTS.md:L299-L302

Useful? React with 👍 / 👎.

클래스 멤버와 운영자 화면의 상태 및 이동 동작을 정리하고 피그마 예시는 미리보기 데이터로 분리한다.

도메인 유스케이스 의존성과 클래스 화면 테스트를 기존 모듈 방식에 맞춘다.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

새로운 클래스 기능 추가 및 관련 리팩토링은 Clean Architecture, SwiftUI, TCA 컨벤션을 잘 준수하고 있습니다. 특히 ChatRoomFeature의 메시지 타임라인 계산 로직을 ChatRoomTimelineUseCase로 분리한 점은 Reducer에서 복잡한 비즈니스 로직을 분리하는 좋은 예시이며, 모달 및 탭바 UI 처리에 대한 프로젝트 규칙(hidesSystemBars(), isDatePickerPresented 등)도 잘 반영되었습니다. ClassDomain 모듈 분리, 의존성 주입(ClassUseCaseImpl, ClassRepositoryImpl), #if DEBUG를 활용한 미리보기 데이터 관리, 그리고 미구현 기능에 대한 명확한 UI 처리(disabled, alert, placeholder 메시지) 등 전반적으로 높은 수준의 코드 품질과 아키텍처 준수를 보여줍니다.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7881f6f22e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +117
Button { send(.deadlineTapped) } label: {
informationRow(
"참여 마감일",
detail: Self.deadlineFormatter.string(from: store.deadline) + "까지",
actionTitle: "수정"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide the sentinel date for classes without a deadline

When an owner disables “참여 마감일”, ClassSettingFeature.State.creation stores .distantFuture, but this row always formats that value. Entering the newly created class therefore displays a nonsensical date around year 4001, unlike the share and class-list screens that explicitly omit the sentinel; render a no-deadline label or hide the row when deadline == .distantFuture.

Useful? React with 👍 / 👎.

Comment on lines +162 to +165
switch action {
case let .created(result):
state.isLoading = false
guard case let .success(room) = result else { return .none }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface class creation failures

Whenever createClass fails—for example because of a transient network or validation error—this guard silently discards the ClassError, merely re-enabling the button with no alert or inline message. Preserve the failure in state and present it so users can distinguish a failed request from an ignored tap.

AGENTS.md reference: AGENTS.md:L538-L545

Useful? React with 👍 / 👎.

Comment on lines +10 to +14
case participation = "내 참여"
case classResult = "클래스 결과"
case feedback = "피드백"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the report tab enum into the entity layer

Tab is a screen-level UI branching enum declared directly in a Feature reducer, while the repository requires these enums to be separate domain entity types and reserves Feature files for TCA components. Move it to its own ClassDomainInterface/Sources/Entity file and reference that type from the reducer and view.

AGENTS.md reference: AGENTS.md:L427-L431

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant