Skip to content

feat: 홈 투표 UI 및 내비게이션 바 구현 (#16)#17

Merged
rafa-e1 merged 61 commits intodevelopfrom
feat/#16
Jul 1, 2025
Merged

feat: 홈 투표 UI 및 내비게이션 바 구현 (#16)#17
rafa-e1 merged 61 commits intodevelopfrom
feat/#16

Conversation

@rafa-e1
Copy link
Contributor

@rafa-e1 rafa-e1 commented Jun 17, 2025

🚀 Pull Request

No merge conflict? Where’s the fun in that.


📌 요약

  • 커스텀 내비게이션 바 UI 구현
  • HomeSection 기반의 Compositional Layout 분리
  • SectionController 기반 UI 아키텍처 도입
  • “전체보기” 버튼 동작 ViewModel로 위임 처리

📄 상세 설명

  • 각 섹션별 레이아웃을 HomeLayoutFactory를 통해 추상화하여 구성 유연성을 확보함
  • VoteSectionLayout, DefaultSectionLayout을 구현하여 섹션별 디자인 차이를 명확히 분리
  • 셀, 헤더, 푸터 바인딩 책임을 SectionController 프로토콜로 위임하여 ViewController 로직 단순화
  • HomeSectionFooterView의 “전체보기” 버튼 탭 시 해당 섹션 타이틀을 ViewModel로 전달하도록 로직 연결
  • 디렉토리 구조는 다음과 같음
Home/
├── Component/
│   ├── Cell/
│   ├── Footer/
│   └── Header/
├── Enum/
├── Layout/
│   ├── Factory/
│   ├── Protocol/
│   └── Section/
│       ├── Controller/
│       ├── SectionLayout/
│       └── Supplementary/
├── View/
├── ViewModel/

폴더별 설명

Component/

UI 컴포넌트 단위로 구성된 View 계층입니다.

  • Cell/ : 각 섹션에 대응되는 UICollectionViewCell
  • Header/, Footer/ : UICollectionReusableView 형태로 구성되는 Supplementary View

Enum/

  • HomeSection.swift: 섹션 타입 정의와 타이틀/버튼 텍스트/identifier 등 섹션별 메타데이터 캡슐화

Layout/

Layout/Factory/

  • HomeLayoutFactory.swift: HomeSection에 따라 알맞은 레이아웃을 반환하는 팩토리

Layout/Protocol/

  • HomeSectionLayoutFactory, SectionLayoutProviding: 레이아웃 생성을 추상화하는 인터페이스

Layout/Section/

Layout/Section/Controller/
  • SectionController.swift: 셀 및 SupplementaryView의 구성 책임을 갖는 인터페이스
  • DefaultSectionController.swift, VoteSectionController.swift: 각 섹션별 구성 로직 구현
Layout/Section/SectionLayout/
  • DefaultSectionLayout.swift, VoteSectionLayout.swift: 섹션별 NSCollectionLayoutSection 생성 클래스
Layout/Section/Supplementary/
  • HomeSupplementaryItemFactory.swift: Header/Footer SupplementaryItem 레이아웃 제공 유틸

View/

  • HomeViewController.swift: 컬렉션 뷰 셋업, 데이터 바인딩, 섹션 컨트롤러 호출 등 화면 전체를 조립

ViewModel/

  • HomeViewModel.swift: UI 이벤트 처리 및 화면 상태에 필요한 데이터를 가공

설계 전략 요약

요소 전략
UI 구성 책임 분리 SectionController 패턴으로 셀/헤더/푸터 구성 분리
레이아웃 구성 책임 분리 Factory 및 LayoutProvider 추상화 적용
확장성 섹션별 로직/레이아웃 추가 시 새로운 SectionController + Layout만 정의하면 됨
MVVM 적용 View ↔ ViewModel 간 이벤트 처리 책임 분리
재사용성 SupplementaryItem, Layout 구조는 다른 화면에서도 쉽게 재사용 가능

주요 코드 설명

HomeSectionLayoutFactory

protocol HomeSectionLayoutFactory {
    func layout(for section: HomeSection) -> NSCollectionLayoutSection
}
  • 역할: HomeSection에 해당하는 레이아웃을 생성해주는 팩토리의 인터페이스입니다.
  • 사용 이유: 섹션에 따라 레이아웃이 달라지기 때문에 이를 동적으로 선택하고 추상화하여 깔끔한 분기 구조를 유지하기 위해 사용합니다.

SectionLayoutProviding

protocol SectionLayoutProviding {
    func layout() -> NSCollectionLayoutSection
}
  • 역할: 레이아웃을 제공하는 책임만 가진 인터페이스입니다.
  • 사용 이유: 공통된 레이아웃 반환 방식(layout())을 통해 다양한 레이아웃을 하나의 타입으로 묶기 위해 사용합니다.

DefaultSectionLayout

final class DefaultSectionLayout: SectionLayoutProviding {
    func layout() -> NSCollectionLayoutSection { ... }
}
  • 역할: 주변 맛집, 추천, 찐 맛집 등과 같은 일반적인 섹션들의 레이아웃을 구성합니다.
  • 특징: 셀 크기를 동일하게 유지하고 헤더와 푸터를 함께 포함하는 NSCollectionLayoutSection을 반환합니다.

VoteSectionLayout

final class VoteSectionLayout: SectionLayoutProviding {
    func layout() -> NSCollectionLayoutSection { ... }
}
  • 역할: 투표 섹션 전용 레이아웃을 구성합니다.
  • 특징: SupplementaryView를 포함하지 않습니다.

HomeSupplementaryItemFactory

enum HeaderSupplementaryItemFactory { static func create() -> NSCollectionLayoutBoundarySupplementaryItem }
enum FooterSupplementaryItemFactory { static func create() -> NSCollectionLayoutBoundarySupplementaryItem }
  • 역할: 헤더와 푸터용 SupplementaryItem 레이아웃을 생성합니다.
  • 사용 이유: 레이아웃 코드 중복을 줄이고 헤더/푸터의 높이와 정렬을 통일성 있게 유지하기 위함입니다.

SectionController

protocol SectionController {
    func numberOfItems() -> Int
    func cellForItem(at:in:) -> UICollectionViewCell
    func header(in:at:) -> UICollectionReusableView?
    func footer(in:at:) -> UICollectionReusableView?
}
  • 역할: 각 섹션의 셀, 헤더, 푸터를 구성하는 책임을 가진 인터페이스입니다.
  • 사용 이유: UICollectionViewDataSource 구현을 분산시켜 ViewController를 단순화하고 섹션별 책임을 분리하기 위해 사용합니다.

DefaultSectionController

final class DefaultSectionController: SectionController {
    // 생략
}
  • 역할: 주변 맛집, 추천, 찐 맛집과 같은 일반 섹션의 셀/헤더/푸터를 구성하는 클래스입니다.
  • 특징: HomeSection을 기반으로 셀 identifier, 타이틀, SupplementaryView 여부 등을 참조하여 UI를 구성합니다.
  • 사용 이유: 하나의 클래스로 다양한 섹션에 공통된 UI 패턴을 재사용할 수 있습니다.

VoteSectionController

final class VoteSectionController: SectionController {
    // 생략
}
  • 역할: 투표 섹션 전용 셀을 구성하는 클래스로, SupplementaryView는 사용하지 않습니다.
  • 특징: UICollectionViewCell 하나만 구성하며 가장 간결한 섹션 컨트롤러의 예시입니다.
  • 사용 이유: 레이아웃뿐 아니라 UI 구성 책임도 전용 클래스로 분리하여 역할을 명확히 하기 위함입니다.

HomeLayoutFactory

final class HomeLayoutFactory: HomeSectionLayoutFactory {
    func layout(for section: HomeSection) -> NSCollectionLayoutSection { ... }
}
  • 역할: 각 HomeSection에 대해 적절한 SectionLayoutProviding 구현체를 선택하여 NSCollectionLayoutSection을 반환하는 팩토리입니다.
  • 사용 이유: UICollectionViewCompositionalLayout의 섹션별 구성을 switch 문으로 깔끔하게 위임하고 변경에 유연하게 대응할 수 있도록 하기 위함입니다.

📸 구현 화면 (GIF / Screenshot)

구현 내용 SE 13 mini 15 pro
홈 투표 UI

🙋 리뷰 가이드

  • SectionController 설계가 과도하게 추상화되지 않았는지 확인 부탁드립니다.

🔗 관련 이슈 (Issue)


✅ PR Check List

  • Merge 대상 브랜치가 올바른가?
  • 최종 코드가 에러 없이 잘 동작하는가?
  • 전체 변경사항이 500줄을 넘지 않는가?

Summary by CodeRabbit

  • 신규 기능

    • 홈 화면이 새롭게 도입되어 투표, 근처 랭킹, 개인 추천, 숨은 맛집 등 다양한 섹션이 추가되었습니다.
    • 홈 화면 내 각 섹션별로 커스텀 셀, 헤더/푸터 뷰, 내비게이션 바 등이 적용되었습니다.
    • 투표, 추천, 랭킹 등 다양한 데이터 모델과 뷰모델이 추가되어 정보를 효과적으로 표시합니다.
    • 커스텀 폰트, 색상, 아이콘 및 이미지 리소스가 추가되어 디자인 시스템이 확장되었습니다.
    • StackView 및 UIView 등 UI 편의 확장 기능이 도입되었습니다.
  • 스타일

    • UI 요소별 배경색, 폰트, 레이아웃 등 일관된 스타일이 적용되었습니다.
  • 기타

    • 불필요한 기존 HomeViewController 파일이 제거되었습니다.
    • Info.plist에 커스텀 폰트 리소스가 추가되었습니다.

rafa-e1 added 30 commits June 9, 2025 18:02
- Common/Extension -> UI/Extension
- 주변 맛집 랭킹 셀
- 맞춤형 추천 셀
- 숨은 찐 맛집 셀
@rafa-e1 rafa-e1 requested a review from alstjr7437 June 17, 2025 13:18
@rafa-e1 rafa-e1 self-assigned this Jun 17, 2025
@rafa-e1 rafa-e1 added 🛠️ Refactor Refactoring ✨ Feature 기능 구현 🎸 Chore 코드 기능과 무관한 프로젝트 설정, 도구 추가, 유지보수 등 환경 개선 작업 labels Jun 17, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jun 17, 2025

Walkthrough

이번 변경사항은 홈 화면의 투표 UI와 커스텀 내비게이션 바를 포함한 홈 화면 전체 구조를 구현하고, 관련된 데이터 모델, 뷰모델, 셀, 섹션 컨트롤러, 레이아웃, 디자인 시스템(폰트, 컬러, 이미지 등)과 확장 유틸리티, 베이스 클래스 등을 추가하는 대규모 초기 개발입니다. 기존 HomeViewController 파일은 제거되고, 새로운 구조로 대체됩니다.

Changes

파일/경로 그룹 변경 요약
.../project.pbxproj, Info.plist Xcode 프로젝트 설정 및 Info.plist에 폰트 리소스 추가
.../Core/Common/Extension/ReusableIdentifier+.swift 재사용 식별자 프로토콜 및 익스텐션 추가
.../Core/DesignSystem/Foundation/* 아이콘, 이미지, 텍스트, 컬러, 폰트 등 디자인 시스템 리소스 및 유틸리티 추가
.../Core/DesignSystem/Resources/Assets.xcassets/* SVG/PNG 아이콘, 이미지 에셋 및 메타데이터 JSON 추가
.../Core/UI/Extension/UIStackView+.swift UIStackView 레이아웃 설정 메서드 추가
.../Core/UI/Extension/UIView+.swift UIView에 addSubviews 유틸리티 추가
.../Presentation/Common/Base/* CollectionView Cell/ReusableView 베이스 클래스 추가
.../Presentation/Home/Component/Cell/* 투표, 추천, 랭킹, 히든젬 셀 클래스 추가
.../Presentation/Home/Component/Footer/HomeSectionFooterView.swift 홈 섹션 푸터 뷰 추가
.../Presentation/Home/Component/Header/HomeSectionHeaderView.swift 홈 섹션 헤더 뷰 추가
.../Presentation/Home/Component/HomeNavigationBarView.swift 커스텀 홈 내비게이션 바 뷰 추가
.../Presentation/Home/HomeSection/Controller/* 섹션별 컨트롤러(투표, 랭킹, 추천, 히든젬) 추가
.../Presentation/Home/HomeSection/Enum/HomeSectionType.swift 홈 섹션 타입 Enum 및 레이아웃 반환 메서드 추가
.../Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift 홈 섹션 팩토리(섹션 생성 및 구성) 추가
.../Presentation/Home/HomeSection/Layout/* 섹션별 컴포지셔널 레이아웃 및 SupplementaryItem 레이아웃 유틸 추가
.../Presentation/Home/HomeSection/Protocol/* Section/레이아웃/헤더푸터 디스플레이 프로토콜 추가
.../Presentation/Home/Model/* Vote, Restaurant, NearbyRanking, RecommendedRestaurant 등 데이터 모델 추가
.../Presentation/Home/ViewModel/* Vote, 랭킹, 추천, 히든젬 셀 뷰모델 클래스 추가
.../Presentation/Home/View/HomeViewController.swift 홈 화면 컨트롤러 전체 신규 구현
.../Presentation/Main/MainTab.swift 홈 탭에서 HomeViewModel을 생성해 HomeViewController에 주입하도록 변경
.../Presentation/Home/HomeViewController.swift 기존 HomeViewController 파일 삭제

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeViewController
    participant HomeViewModel
    participant CollectionView
    participant SectionControllers
    participant CustomViews

    User->>HomeViewController: 앱 실행/홈 진입
    HomeViewController->>HomeViewModel: ViewModel 초기화
    HomeViewModel->>HomeSectionFactory: 섹션 생성 요청
    HomeSectionFactory-->>HomeViewModel: SectionDisplayable 배열 반환
    HomeViewController->>CustomViews: 내비게이션 바, 컬렉션뷰 등 UI 설정
    HomeViewController->>CollectionView: 데이터소스/레이아웃 등록
    CollectionView->>SectionControllers: 셀/헤더/푸터 요청
    SectionControllers-->>CollectionView: 셀/헤더/푸터 반환
    User->>CustomViews: 내비게이션 바 검색 버튼/푸터 버튼 등 클릭
    CustomViews->>HomeViewController: 액션 전달
    HomeViewController->>HomeViewModel: 액션 위임
Loading

Assessment against linked issues

Objective Addressed Explanation
내비게이션 바 구현 (#16)
홈 투표 UI 구현 (#16)

Assessment against linked issues: Out-of-scope changes

(해당 변경사항에서는 모든 코드 변경이 #16 이슈의 "홈 투표 UI 및 내비게이션 바 구현" 목적에 부합하므로, 범위를 벗어난 변경사항이 없습니다.)

Poem

🐰
홈 화면이 새로 태어나
투표와 내비게이션 바
셀과 섹션, 뷰모델까지
깔끔하게 정돈했지
토끼도 놀란 이 변화
코드밭에 봄이 왔나봐!
🌸

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🔭 Outside diff range comments (2)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift (2)

12-26: HEX 파싱 실패 시 검은색으로 떨어지는 문제

Scanner.scanHexInt64가 실패해도 hexNumber는 기본값 0이어서 결과가 항상 #000000(검정)이 됩니다. 잘못된 문자열을 조기에 감지해 assertionFailure 또는 기본 색상 반환으로 처리해 주세요.

-        scanner.scanHexInt64(&hexNumber)
+        guard scanner.scanHexInt64(&hexNumber) else {
+            assertionFailure("❌ Invalid hex string: \(hex)")
+            self.init(white: 0, alpha: 0) // 투명색 반환
+            return
+        }

18-24: 알파 포함 8자리 지원 로직 오동작

finalAlpha 계산이 hex.count > 7 로 분기하지만 #AARRGGBB 형식의 문자열 길이는 9(해시 포함) 또는 8(해시 제외)입니다. 현재 구현은 해시를 뺀 8자리만 정상 동작하며, 9자리 입력 시 alpha 계산이 잘못됩니다.

🧹 Nitpick comments (27)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipText.swift (1)

10-15: 문자열 하드코딩 대신 Localizable.strings 사용 권장

UI 텍스트를 코드에 직접 하드코딩하면 다국어 지원 및 A/B 테스트가 어려워집니다.
NSLocalizedString(또는 SwiftGen) 기반으로 옮기면 번역·관리 용이성이 향상됩니다.

-        static let title: String = "당신의 선택은?"
-        static let subtitle: String = "오늘 당신의 맛집을 투표해주세요!"
+        static let title = NSLocalizedString("vote.title", comment: "투표 섹션 타이틀")
+        static let subtitle = NSLocalizedString("vote.subtitle", comment: "투표 섹션 서브타이틀")
MatzipBook/MatzipBook/Presentation/Home/Layout/Protocol/HomeSectionLayoutFactory.swift (1)

10-12: 프로토콜을 AnyObject로 제한하여 참조타입 전제 명시

레이아웃 팩토리는 대체로 클래스 기반(상태·DI 필요)으로 구현되므로, 프로토콜에 AnyObject 제약을 추가하면 reference-type 임을 명확히 드러낼 수 있습니다.

-protocol HomeSectionLayoutFactory {
+protocol HomeSectionLayoutFactory: AnyObject {
MatzipBook/MatzipBook/Core/Common/Extension/ReusableIdentifier+.swift (1)

10-19: 재사용 식별자 네이밍 일관성 및 범위 확장 제안

  1. UIKit 기본 명칭( reuseIdentifier )과 달리 identifier 로 정의되어 혼동을 줄 수 있습니다. 동일 단어 사용이 가독성을 높입니다.
  2. UITableViewCell 도 동일 패턴을 사용하므로 프로토콜 채택 범위를 확장하면 재사용성 향상됩니다.
-    static var identifier: String {
+    static var reuseIdentifier: String {
         return String(describing: self)
     }
@@
-extension UICollectionReusableView: ReusableIdentifier {}
+extension UICollectionReusableView: ReusableIdentifier {}
+extension UITableViewCell: ReusableIdentifier {}

기존 호출부에서도 .identifier 대신 .reuseIdentifier 로 일괄 변경 요구됩니다.

MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (2)

18-19: 상수 프로퍼티를 저장 프로퍼티로 변환하여 반복 연산 제거

universityName 이 불변 값이라면 계산 프로퍼티 대신 let 저장 프로퍼티가 간결합니다.

-    var universityName: String { return "부경대학교" }
+    let universityName: String = "부경대학교"

20-26: print 대신 OSLog 등 로깅 유틸 사용 권장

print 는 릴리스 빌드에서 제거되지 않아 성능 및 보안 이슈를 야기할 수 있습니다.
os.Logger(iOS 14+) 또는 자체 Logger 로 교체해 주세요.

-        print("DEBUG: Search button tapped")
+        logger.debug("Search button tapped")
@@
-        print("DEBUG: See all button tapped for section >>> \(section.cellIdentifier)")
+        logger.debug("See all button tapped for section: \(section.cellIdentifier, privacy: .public)")

(로거 인스턴스는 private let logger = Logger(subsystem:"com.matzipbook.home", category:"viewModel") 등으로 선언)

MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift (2)

10-12: 불필요한 의존성 import 제거 권장

SnapKit, Then 모듈을 현재 파일에서 직접 사용하지 않고 있습니다. 사용되지 않는 import 는 경고를 유발하므로 제거하거나, 앞으로의 확장을 위해 반드시 필요하다면 최소한 // swiftlint:disable unused_import 등의 주석으로 의도를 명시해 주세요.

-import SnapKit
-import Then
+// 필요 시 개별 구현 파일에서 import 해주세요.

23-25: init(coder:) 사용 불가를 컴파일 타임에 명확히 표시하세요

fatalError 로 런타임에 죽이기보다는, 컴파일 타임에 사용할 수 없음을 선언하면 의도를 더 명확히 전달할 수 있습니다.

-  required init?(coder: NSCoder) {
-      fatalError("init(coder:) has not been implemented")
-  }
+  @available(*, unavailable, message: "Storyboard/XIB 초기화를 지원하지 않습니다.")
+  required init?(coder: NSCoder) {
+      fatalError("init(coder:) has not been implemented")
+  }
MatzipBook/MatzipBook/Presentation/Home/Layout/Protocol/SectionLayoutProviding.swift (1)

10-12: 접근 제어자를 명시해 주세요

해당 프로토콜이 모듈 외부(예: Feature / DesignSystem 모듈 등)에서도 사용될 가능성이 있다면 public 키워드를 명시해 주는 편이 좋습니다. 기본값(internal)이 의도된 것인지 확인 부탁드립니다.

-protocol SectionLayoutProviding {
+public protocol SectionLayoutProviding {
     func layout() -> NSCollectionLayoutSection
 }
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipImage.swift (1)

8-14: UIImage 헬퍼 프로퍼티 추가 고려

자주 사용되는 자산 enum이라면 매번 UIImage(named:)를 호출하기보다, 아래와 같이 image 계산 프로퍼티를 추가하면 호출부 코드가 간결해집니다.

 enum MatzipImage: String {
     case imgLogo   = "img_logo"
     case imgBubble = "img_bubble"
     case imgVS     = "img_vs"
+
+    var image: UIImage? {
+        return UIImage(named: self.rawValue)
+    }
 }

(이 경우 UIKit import 필요)
선택 사항이니 팀 코딩 컨벤션에 맞춰 판단해 주세요.

MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1)

10-14: super.setupStyles() 호출 여부 확인

BaseCollectionViewCellsetupStyles()가 UI 초기화 로직을 담고 있다면, 오버라이드 시에도 super.setupStyles()를 호출해야 예상치 못한 스타일 누락을 방지할 수 있습니다.

 override func setupStyles() {
-    contentView.backgroundColor = .systemGreen
+    super.setupStyles()
+    contentView.backgroundColor = .systemGreen
 }

베이스 클래스 구현이 비어있다면 현 상태로도 무관하지만, 향후 베이스에 공통 스타일이 추가될 때 대비해 두는 편을 권장드립니다.

MatzipBook/MatzipBook/Presentation/Main/MainTab.swift (1)

33-37: viewController 프로퍼티 다중 호출 시 중복 인스턴스 생성 가능성

viewController가 계산 프로퍼티라서 매번 접근할 때마다 새로운 HomeViewModel/HomeViewController 인스턴스를 생성합니다.
TabBarController에서 케이스별 한 번만 호출한다면 문제없지만, 외부에서 반복 접근할 경우 메모리·상태 불일치가 발생할 수 있습니다.

필요하다면 캐싱 방식으로 개선을 고려해 주세요.

// 예시
private static var homeVC: UIViewController = {
    let vm = HomeViewModel()
    let vc = HomeViewController(viewModel: vm)
    return UINavigationController(rootViewController: vc)
}()

case .home:
    return MainTab.homeVC
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (1)

12-14: setupStyles()에서 super.setupStyles() 호출 누락

BaseCollectionViewCell이 기본 스타일(코너 라운딩, 셀 공통 배경 등)을 설정하고 있을 가능성이 높습니다.
오버라이드 시 super.setupStyles()를 먼저 호출하지 않으면 상위 클래스의 설정이 무시될 수 있습니다.

override func setupStyles() {
-    contentView.backgroundColor = .systemIndigo
+    super.setupStyles()
+    contentView.backgroundColor = .systemIndigo
}
MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift (1)

12-22: 반환값 추가로 체이닝 가능성 확보

configureStackView를 사용한 뒤 바로 다른 설정을 체이닝하고 싶을 때 반환값이 없어서 제약이 생깁니다.
@discardableResultSelf를 반환하도록 수정하면 다음과 같이 유연하게 사용할 수 있습니다.

-func configureStackView(
+@discardableResult
+func configureStackView(
     axis: NSLayoutConstraint.Axis = .horizontal,
     alignment: UIStackView.Alignment = .fill,
     distribution: UIStackView.Distribution = .fillEqually,
     spacing: CGFloat = 0
-) {
+) -> Self {
     self.axis = axis
     self.alignment = alignment
     self.distribution = distribution
     self.spacing = spacing
+    return self
 }
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/RecommendationCollectionViewCell.swift (1)

12-14: super.setupStyles() 호출로 공통 스타일 보존

HiddenGemCollectionViewCell와 동일한 이유로, 상위 클래스의 기본 스타일을 유지하기 위해 먼저 super.setupStyles()를 호출하는 편이 안전합니다.

override func setupStyles() {
-    contentView.backgroundColor = .systemBlue
+    super.setupStyles()
+    contentView.backgroundColor = .systemBlue
}
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)

17-19: 폰트 로딩 실패 상황에 대한 명시적 로깅 추가 제안

폰트 이름 오타나 Info.plist 등록 누락 시 시스템 폰트로 조용히 폴백하지만, 원인을 찾기 힘듭니다. 실패 시 assertionFailure 또는 os_log로 경고를 남겨 두면 디버깅이 수월합니다.

-        return UIFont(name: name.rawValue, size: size) ?? .systemFont(ofSize: size)
+        if let font = UIFont(name: name.rawValue, size: size) {
+            return font
+        } else {
+            assertionFailure("❌ Custom font \(name.rawValue) not found. Falling back to system font.")
+            return .systemFont(ofSize: size)
+        }
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/VoteSectionLayout.swift (1)

24-30: 콘텐츠 인셋 비일관성

top: 25, bottom: 0로 설정되어 있어 시각적으로 다른 섹션과 높이 차이가 큽니다. 디자인 가이드에 따른 값인지 한 번 더 체크 부탁드립니다.

MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Supplementary/HomeSupplementaryItemFactory.swift (2)

10-20: 중복 객체 생성 방지

create() 호출마다 새 NSCollectionLayoutBoundarySupplementaryItem 인스턴스를 생성합니다. 동일 레이아웃을 여러 섹션에서 공유한다면 static 상수로 캐시해도 무방합니다.


23-33: 헤더·푸터 식별자 상수화 제안

elementKind를 직접 문자열로 사용하면 오타 위험이 있습니다. UICollectionView.elementKindSectionHeader 등의 시스템 상수를 그대로 쓰셨지만, 커스텀 kind를 추가할 경우를 대비해 별도 enum/struct로 관리하면 유지보수성이 높아집니다.

MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/DefaultSectionLayout.swift (1)

24-34: 보조 뷰 인서트 간격 불일치 주의

헤더 높이가 .estimated(50)라 실제 렌더링 후 오토사이징 결과에 따라 섹션 간 간격이 들쭉날쭉할 수 있습니다. 필요 시 section.interGroupSpacing 또는 boundarySupplementaryItems[...].contentInsets를 활용해 균일 간격을 맞춰 주세요.

MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift (1)

24-29: Dynamic Type를 지원하도록 폰트 설정 개선 제안
고정 크기(20pt) 폰트는 사용자의 ‘텍스트 크기’ 접근성 설정을 무시합니다. UIFont.preferredFont(forTextStyle:)adjustsFontForContentSizeCategory를 활용해 접근성을 높이는 것을 권장드립니다.

-            $0.font = .applyFont(.bold, ofSize: 20)
+            $0.font = .preferredFont(forTextStyle: .title3)
+            $0.adjustsFontForContentSizeCategory = true
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/SectionController.swift (1)

10-27: AnyObject 제약 및 기본 구현 추가로 메모리 관리·보일러플레이트 감소
프로토콜을 AnyObject로 제한하면 컨트롤러를 weak으로 보관할 수 있고, header/footer가 불필요한 섹션을 위해 기본 구현을 제공하면 중복이 줄어듭니다.

-protocol SectionController {
+protocol SectionController: AnyObject {
@@
-    func header(
+    func header(
@@
-    func footer(
+    func footer(
extension SectionController {
    func header(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionReusableView? { nil }
    func footer(in collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionReusableView? { nil }
}
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (1)

121-169: prepareForReuse에서 이미지·텍스트 초기화 누락
재사용 시 이전 이미지·텍스트가 남아 플리커 현상이 발생할 수 있습니다.

override func prepareForReuse() {
    super.prepareForReuse()
    [leftNameLabel, rightNameLabel].forEach { $0.text = nil }
    [leftNameImageView, rightNameImageView].forEach { $0.image = nil }
}
MatzipBook/MatzipBook/Presentation/Home/Enum/HomeSection.swift (1)

42-49: 새 섹션 추가 시 누락 위험 – 컴파일 타임 안전성 강화 제안
cellIdentifier switch 구문은 케이스 추가 시 누락되어도 컴파일 오류가 발생하지 않습니다. @unknown default 를 사용하거나 셀 타입을 연관 값으로 보관하는 방식으로 안전성을 높일 수 있습니다.

MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/VoteSectionController.swift (1)

12-26: 셀 설정 로직 부재

numberOfItems()가 1을 반환하므로 현 시점에서는 상관없지만, cellForItem에서 VoteCollectionViewCell을 반환한 뒤 아무 설정도 하지 않습니다.
후속 요구사항(뷰 모델 바인딩, delegate 지정 등)이 생기면 셀 내부에서 처리하더라도 configure(with:) 같은 메서드를 호출하는 패턴을 미리 마련해 두면 추후 유지보수성이 높아집니다.

MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift (2)

10-12: 불필요한 Then 임포트

이 파일 내부에서는 Then을 사용하지 않습니다. 임포트를 제거하여 불필요한 의존성을 줄이세요.

-import Then

38-68: open 접근 수준 고려

해당 메서드들은 서브클래스 전용 확장 지점입니다.
외부 모듈에서 상속할 가능성이 있으면 open으로, 아닐 경우 internal (기본값)로 두어도 충분합니다. 현재 공개 필요성이 없으므로 의도적으로 final 클래스 선언으로 막거나, 주석으로 범위를 명시해 두면 좋겠습니다.

MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (1)

70-80: 셀 등록 누락 위험

cellTypes 배열에 새로운 섹션 셀이 추가될 때마다 수동으로 업데이트해야 합니다.
반복을 줄이기 위해 HomeSection.allCases.map(\.cellClass) 같이 메타데이터 기반 자동 등록을 고려하세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c1c751 and 3ef492e.

⛔ Files ignored due to path filters (15)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/ic_clock.svg is excluded by !**/*.svg
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/ic_search.svg is excluded by !**/*.svg
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/img_bubble.svg is excluded by !**/*.svg
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@2x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@3x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@2x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@3x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@2x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@3x.png is excluded by !**/*.png
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/img_vs.svg is excluded by !**/*.svg
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Bold.otf is excluded by !**/*.otf
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Regular.otf is excluded by !**/*.otf
📒 Files selected for processing (41)
  • MatzipBook/MatzipBook.xcodeproj/project.pbxproj (1 hunks)
  • MatzipBook/MatzipBook/Core/Common/Extension/ReusableIdentifier+.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipImage.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipText.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/Contents.json (1 hunks)
  • MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift (1 hunks)
  • MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift (1 hunks)
  • MatzipBook/MatzipBook/Info.plist (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/RecommendationCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Enum/HomeSection.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeViewController.swift (0 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Factory/HomeLayoutFactory.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Protocol/HomeSectionLayoutFactory.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Protocol/SectionLayoutProviding.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/DefaultSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/SectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/VoteSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/DefaultSectionLayout.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/VoteSectionLayout.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Supplementary/HomeSupplementaryItemFactory.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Main/MainTab.swift (1 hunks)
💤 Files with no reviewable changes (1)
  • MatzipBook/MatzipBook/Presentation/Home/HomeViewController.swift
🧰 Additional context used
🧬 Code Graph Analysis (12)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/RecommendationCollectionViewCell.swift (2)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (2)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/RecommendationCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (2)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/RecommendationCollectionViewCell.swift (1)
  • setupStyles (12-14)
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/VoteSectionLayout.swift (1)
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/DefaultSectionLayout.swift (1)
  • layout (12-37)
MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift (1)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)
  • applyFont (17-19)
MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift (1)
MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift (3)
  • setupStyles (38-40)
  • setupLayouts (53-53)
  • setupConstraints (67-67)
MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift (3)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)
  • applyFont (17-19)
MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift (1)
  • configureStackView (12-22)
MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift (1)
  • addSubviews (12-14)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (3)
MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift (1)
  • configureStackView (12-22)
MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift (1)
  • addSubviews (12-14)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)
  • applyFont (17-19)
MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift (1)
MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift (3)
  • setupStyles (38-40)
  • setupLayouts (53-53)
  • setupConstraints (67-67)
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/DefaultSectionLayout.swift (1)
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/VoteSectionLayout.swift (1)
  • layout (12-33)
MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift (2)
MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift (4)
  • configure (18-20)
  • setupStyles (24-29)
  • setupLayouts (31-33)
  • setupConstraints (35-40)
MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)
  • applyFont (17-19)
MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (3)
MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (2)
  • didTapSearchButton (20-22)
  • didTapSeeAllButton (24-26)
MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift (2)
  • setTitle (39-41)
  • setRightBarButtonAction (43-45)
MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift (1)
  • addSubviews (12-14)
🔇 Additional comments (19)
MatzipBook/MatzipBook/Info.plist (1)

5-9: 커스텀 폰트 파일 번들 포함 여부 확인 필요

Info.plist에 UIAppFonts 항목을 등록한 것만으로는 앱에서 폰트를 사용할 수 없습니다.
‣ 프로젝트 → Target → Build Phases → Copy Bundle Resources 단계에 Pretendard-Bold.otf, Pretendard-Regular.otf 두 파일이 실제로 추가돼 있는지, 그리고 파일명이 정확히 일치하는지 검증해 주세요.
‣ 또한 내부 PostScript 이름이 코드에서 지정하는 이름과 일치하지 않으면 런타임에서 폰트가 로드되지 않을 수 있으니 확인이 필요합니다.

MatzipBook/MatzipBook.xcodeproj/project.pbxproj (1)

125-126: 프로젝트 업그레이드 체크 값 업데이트 확인

LastUpgradeCheck 값이 1640 으로 변경되었습니다. Xcode 버전 호환성만 변경된 사항이므로 다른 설정 충돌이 없는지 한번 빌드 확인만 해주시면 될 것 같습니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/Contents.json (1)

1-23: 리소스 파일 – 검토할 사항 없음

이미지 세트 정의가 정상적으로 추가되었습니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/Contents.json (1)

1-6: 리소스 파일 – 검토할 사항 없음

에셋 카탈로그 메타데이터가 정상입니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/Contents.json (1)

1-15: 리소스 파일 – 검토할 사항 없음

벡터 보존 옵션이 활성화되어 있어 SVG 품질 유지가 가능합니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/Contents.json (1)

1-15: 벡터 자산 설정이 적절합니다.
SVG 아이콘에 대해 idiom: universalpreserves-vector-representation: true가 올바르게 설정되어 있어 다양한 해상도에서 선명도를 유지합니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json (1)

1-15: 검색 아이콘 자산 설정이 적절합니다.
SVG 파일이 universal idiom으로 등록되었고 벡터 표현이 유지되어 확장성에 문제가 없습니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json (1)

1-23: 더미 이미지 스케일 구성 확인 완료.
1x, 2x, 3x 스케일이 모두 universal idiom으로 올바르게 선언되어 있어 다양한 기기에서 정상 표시됩니다.

MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift (1)

19-20: 새 아이콘 enum 추가가 적절합니다.
icSearchicClock 케이스가 올바른 rawValue로 선언되어 있어 디자인 시스템과 일관성이 유지됩니다.

MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift (1)

10-15: UIView 다중 subview 추가 메서드 도입이 유용합니다.
가변 인자를 이용해 여러 서브뷰를 한 번에 추가할 수 있어 코드 가독성과 생산성이 향상됩니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/Contents.json (1)

1-23: 리소스 파일 확인 완료

이미지 세트 메타데이터 구조(Xcode v1) 및 스케일별 파일명이 정상적으로 설정되어 있습니다.

MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/Contents.json (1)

1-15: 에셋 정의 이상 무

벡터 보존 옵션까지 잘 설정되어 있습니다. 별다른 문제 없어 보입니다.

MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift (1)

10-13: 열거형 스코프 및 접근 제어 고려 필요

FontName 열거형이 기본 internal 접근 수준으로 노출되어 있는데, 디자인 시스템 외부에서 사용하도록 의도한 타입이라면 public / open 지정이 필요합니다. 반대로 내부 전용이라면 private(file-private)로 한정해 외부 노출을 최소화하는 편이 좋습니다.

MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/VoteSectionLayout.swift (2)

13-18: 추정(Estimated) 높이값 하드코딩 위험

heightDimension: .estimated(200)는 셀 내부 오토레이아웃이 정확히 잡히지 않을 경우 예상치 못한 잘림/공백이 발생할 수 있습니다.
셀 컨텐츠가 고정 높이라면 .absolute 사용, 동적 높이라면 셀 내부 제약 충족 여부를 재검증해 주세요.


24-30: 보조 뷰(Header/Footer) 누락 여부 확인

다른 섹션(DefaultSectionLayout)에서는 헤더·푸터 Supplementary Item을 붙이는데, 투표 섹션에는 정의가 없습니다.
의도적으로 빼신 것인지, 아니면 "전체보기" 버튼이 헤더에 위치해야 하는데 빠진 것인지 확인 바랍니다.

MatzipBook/MatzipBook/Presentation/Home/Layout/Section/SectionLayout/DefaultSectionLayout.swift (1)

13-18: 정사각형 셀 강제 여부 확인

heightDimension: .fractionalWidth(1.0)로 설정되어 모든 셀이 정사각형입니다. 이미지 비율이 4:3 등 다른 경우 비율 왜곡이 일어나니 콘텐츠 타입을 다시 확인해 주세요.

MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift (1)

84-88: footer button 폭이 섹션 전체를 차지함 – 의도 확인 필요
horizontalEdges.equalToSuperview()로 버튼이 footer 뷰의 좌우를 모두 채우도록 설정되어 있습니다. 디자인이 ‘캡슐형 가운데 정렬’이라면 centerX + width 제약으로 조정하는 것이 자연스러울 수 있습니다. 의도된 레이아웃인지 확인 바랍니다.

MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (2)

140-148: SectionController 아키텍처 미적용

PR 설명에서는 SectionController 패턴 도입을 강조했으나, 실제 HomeViewController 데이터 소스는 HomeSection 열거형만 사용하고 있습니다.
섹션별 로직을 SectionController로 분리하려면 collectionView(_:cellForItemAt:) 등에서 sectionControllers[sectionType]?.cellForItem(...)을 호출하도록 변경해야 의도한 구조가 완성됩니다.

Likely an incorrect or invalid review comment.


52-55: 뷰 모델-컨트롤러 강한 참조 사이클 가능성

footer.configure { self.viewModel.didTapSeeAllButton(...) } 패턴과 동일하게, didTapSearch에서도 self가 클로저에 캡처됩니다.
HomeSectionFooterView가 클로저를 프로퍼티로 강하게 보유하면 retain cycle이 생길 수 있습니다. [weak self]를 사용해 안전하게 해제하세요.

footer.configure(title: sectionType.footerButtonTitle) { [weak self] in
    self?.viewModel.didTapSeeAllButton(for: sectionType)
}

Likely an incorrect or invalid review comment.

Comment on lines 12 to 16
func layout(for section: HomeSection) -> NSCollectionLayoutSection {
switch section {
case .vote: VoteSectionLayout().layout()
default: DefaultSectionLayout().layout()
}
Copy link

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

NSCollectionLayoutSection 반환 누락으로 컴파일 오류 발생

switch 문 안에서 return을 명시하지 않아 함수가 Void를 반환하게 됩니다.
컴파일 자체가 되지 않으므로 반드시 수정이 필요합니다.

switch section {
-case .vote: VoteSectionLayout().layout()
-default: DefaultSectionLayout().layout()
+case .vote:
+    return VoteSectionLayout().layout()
+default:
+    return DefaultSectionLayout().layout()
}

추가로, 매 호출마다 레이아웃 객체를 새로 생성하면 성능·메모리 측면에서 손해가 있을 수 있습니다.
섹션별로 캐싱하는 방식도 고려해 보세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func layout(for section: HomeSection) -> NSCollectionLayoutSection {
switch section {
case .vote: VoteSectionLayout().layout()
default: DefaultSectionLayout().layout()
}
func layout(for section: HomeSection) -> NSCollectionLayoutSection {
switch section {
case .vote:
return VoteSectionLayout().layout()
default:
return DefaultSectionLayout().layout()
}
}
🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/Layout/Factory/HomeLayoutFactory.swift
around lines 12 to 16, the function layout(for:) lacks explicit return
statements inside the switch, causing a compile error because it returns Void
instead of NSCollectionLayoutSection. Fix this by adding return before each
layout() call in the switch cases. Additionally, consider caching the layout
objects per section instead of creating new instances on every call to improve
performance and memory usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

switch 문에서 return 키워드를 작성하지 않아 Void를 반환하게 된 점은 놓친 부분이 맞습니다. 리뷰 주신 대로 return 구문을 작성하여 컴파일 오류를 해결하겠습니다.

현재는 섹션의 수가 적고 레이아웃의 복잡도가 높지 않아 초기 단계에서는 가독성과 구현 단순성을 우선시했습니다. 이후 섹션 수가 많아지고 레이아웃 생성 비용이 높아질 경우를 대비해서 static 프로퍼티를 활용하여 리팩터리;ㅇ하겠습니다.

Copy link

Choose a reason for hiding this comment

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

@rafa-e1 네, 정확히 핵심적인 문제를 파악하고 계시네요! 컴파일 오류는 반드시 수정되어야 하는 부분이고, 현재 단계에서는 말씀하신 접근이 합리적입니다.

초기 구현에서 가독성과 단순성을 우선시하는 것은 좋은 판단이라고 생각합니다. 섹션이 4개 정도이고 레이아웃 생성 비용이 크지 않은 상황에서는 과도한 최적화보다는 코드의 명확성이 더 가치 있죠.

향후 섹션이 늘어나거나 레이아웃이 복잡해질 때 static 프로퍼티나 lazy 초기화를 통한 캐싱을 고려하시는 것도 적절한 계획입니다.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +16 to +34
var onTap: (() -> Void)?

// MARK: - Initializer

override init(frame: CGRect) {
super.init(frame: frame)

setAddTargets()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Actions

@objc private func didTapButton() {
onTap?()
}
Copy link

@coderabbitai coderabbitai bot Jun 17, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

재사용 시 클로저 메모리 릭 방지를 위해 prepareForReuse 구현 필요
onTap 클로저가 재사용 시 이전 섹션을 참조해 잘못된 동작이나 메모리 릭을 유발할 수 있습니다. 재사용 준비 단계에서 초기화하세요.

+    override func prepareForReuse() {
+        super.prepareForReuse()
+        onTap = nil
+        seeAllButton.setAttributedTitle(nil, for: .normal)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var onTap: (() -> Void)?
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
setAddTargets()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Actions
@objc private func didTapButton() {
onTap?()
}
var onTap: (() -> Void)?
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
setAddTargets()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Actions
@objc private func didTapButton() {
onTap?()
}
override func prepareForReuse() {
super.prepareForReuse()
onTap = nil
seeAllButton.setAttributedTitle(nil, for: .normal)
}
🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift
between lines 16 and 34, the onTap closure property is not reset during cell
reuse, which can cause memory leaks or incorrect behavior by retaining
references to previous sections. Implement the prepareForReuse method and reset
onTap to nil inside it to prevent these issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

감사합니다. 제안해주신 대로 prepareForReuse()를 구현하여 onTap 클로저를 해제하고 버튼 상태를 초기화하도록 수정하겠습니다.

Copy link

Choose a reason for hiding this comment

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

@rafa-e1 네, 좋습니다! prepareForReuse() 구현으로 재사용 시 발생할 수 있는 메모리 릭과 잘못된 동작을 방지할 수 있을 것입니다. 코드 품질 향상에 적극적으로 대응해 주셔서 감사합니다!


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines +49 to +70
private func setupStyles() {
backgroundColor = .mainBackgroundColor

logoImageView.do {
$0.image = .imgLogo
$0.contentMode = .scaleAspectFit
}

universityNameLabel.do {
$0.font = .applyFont(.bold, ofSize: 16)
$0.textColor = .sub1
}

leftStackView.configureStackView(distribution: .fill, spacing: 4)

searchButton.setImage(
.icSearch.withRenderingMode(.alwaysOriginal),
for: .normal
)

separatorView.backgroundColor = .separatorColor
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

오토레이아웃 제약 불안정 – 높이 결정 문제

searchButtoncenterY에 맞추고, separatorView.topsearchButton.bottom + 8 로 잡은 뒤 separatorView.bottom을 슈퍼뷰에 고정했습니다.
하지만 HomeNavigationBarView 자체 높이에 대한 명시적 제약이 없어서, 런타임에 “Ambiguous layout” 경고가 발생할 가능성이 있습니다.

간단히 고정 높이를 주거나(예: 56), logoImageView/leftStackView의 top·bottom 제약을 추가해 해결할 수 있습니다.

separatorView.snp.makeConstraints {
     $0.top.equalTo(searchButton.snp.bottom).offset(8)
     $0.horizontalEdges.bottom.equalToSuperview()
     $0.height.equalTo(1)
 }
+
+// 예시: 내비게이션 바 고정 높이
+self.snp.makeConstraints {
+    $0.height.equalTo(56)
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift
around lines 49 to 70, the layout constraints cause ambiguity because
HomeNavigationBarView lacks an explicit height constraint. To fix this, add a
fixed height constraint (e.g., 56 points) to HomeNavigationBarView or add top
and bottom constraints to logoImageView and leftStackView to fully define the
vertical layout and eliminate ambiguity warnings.

Comment on lines 33 to 39
let cell: UICollectionViewCell = collectionView.dequeueReusableCell(
withReuseIdentifier: section.cellIdentifier,
for: indexPath
)

return cell
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

셀 타입 캐스팅 누락으로 인한 설정 불가

섹션별 셀을 커스텀하기 위해서는 타입 캐스팅이 필요합니다. 현재는 UICollectionViewCell 그대로 반환되어 이후 구성 로직을 넣을 수 없습니다.

-let cell: UICollectionViewCell = collectionView.dequeueReusableCell(
-    withReuseIdentifier: section.cellIdentifier,
-    for: indexPath
-)
-
-return cell
+switch section {
+case .nearbyRanking:
+    guard let cell = collectionView.dequeueReusableCell(
+        withReuseIdentifier: section.cellIdentifier,
+        for: indexPath
+    ) as? NearbyRankingCollectionViewCell else { return UICollectionViewCell() }
+    // cell.configure(...)
+    return cell
+// 다른 섹션도 동일 패턴
+default:
+    return UICollectionViewCell()
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let cell: UICollectionViewCell = collectionView.dequeueReusableCell(
withReuseIdentifier: section.cellIdentifier,
for: indexPath
)
return cell
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch section {
case .nearbyRanking:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: section.cellIdentifier,
for: indexPath
) as? NearbyRankingCollectionViewCell else {
return UICollectionViewCell()
}
// cell.configure(...)
return cell
// 다른 섹션도 동일 패턴
default:
return UICollectionViewCell()
}
}
🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/DefaultSectionController.swift
around lines 33 to 39, the dequeued cell is returned as a generic
UICollectionViewCell without casting to the specific custom cell type required
for each section. To fix this, cast the dequeued cell to the appropriate custom
cell class based on the section before returning it, enabling further
configuration of the cell.

Copy link
Member

@alstjr7437 alstjr7437 left a comment

Choose a reason for hiding this comment

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

코드 줄을 보니 정말 많더라구요!!
그래도 어느정도 다 읽어보면서 리뷰 남겼습니다!!

따로 궁금한점 질문이나 사용했던걸 구경했어요 사실 ㅋㅋ...
너무 많이 들어오다보니 아직 자세하게 이해는 못했지만 사용해보면서 같이 맞춰나가요!!

혹시 다음 PR부터는 만약

홈 투표 UI 및 내비를 만든다고 하면 중간에 작업이 뻗어 나가는걸 PR로 나눠주실 수 있을까요!

  • 홈 투표 UI 및 내비게이션 바 구현
    • UI 구현에 필요한 Foundation 구현
    • BaseView 등 기본적인 필요한 class 구현
    • Footer View 구현
    • Header View 구현
    • ...

이런식으로 뻗어나가는게 좋을 것 같은데 어떻게 생각하시나요!?

위와 같이 하면 각 부분에 맞는 코드들만 볼 수 있어서 리뷰하기 좋고 기능 분리도 좋을 것 같습니다!

Comment on lines +10 to +14
enum MatzipImage: String {
case imgLogo = "img_logo"
case imgBubble = "img_bubble"
case imgVS = "img_vs"
}
Copy link
Member

Choose a reason for hiding this comment

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

사용되는 image, Text 들을 enum으로 관리해서 좋네요!!

혹시 따로 궁금한점인데 img_logo나 ic_clock등 snake case를 쓰신 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Asset Catalog에서는 이름 구분할 때 대소문자를 구분하지 않아서 이미지 등록할 때 스네이크 케이스로 등록하는 것이 충돌 가능성을 방지하는걸로 알고 있습니다!

Comment on lines +15 to +20
extension UIFont {

static func applyFont(_ name: FontName, ofSize size: CGFloat) -> UIFont {
return UIFont(name: name.rawValue, size: size) ?? .systemFont(ofSize: size)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

오 폰트를 이렇게 쓰는 방법도 있군요
저는 폰트를 쓸때 SwiftUI긴 하지만

static var pretendardBold14: Font { .pretend(type: .bold, size: 14) }
static var pretendardBold16: Font { .pretend(type: .bold, size: 16) }

이런식으로 썻는데 범수님 같이 쓰는게 더 확장성이 좋아보이네요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

말씀해주신 방식도 충분히 좋은 접근이고 상황에 따라 더 적합할 수도 있다고 생각합니다!
결국 코드에는 정답은 없고 프로젝트의 성격과 팀의 방향성에 따라 유연하게 선택하는게 가장 중요하다고 생각해요

Comment on lines +10 to +15
extension UIView {

func addSubviews(_ views: UIView...) {
views.forEach { addSubview($0) }
}
}
Copy link
Member

Choose a reason for hiding this comment

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

이부분은 addSubview를 여러번 말고 한번에 다 추가하는 방식이네요!! 배워갑니다!! 🙏

...으로 list가 아닌 방식으로 하는 방식도 좋네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

감사합니다! 🙏
개인적으로 ... 을 사용하면 줄바꿈했을 때 더 깔끔한 것 같더라고요:)

Comment on lines 62 to 65
configureLabel(
leftNameLabel,
text: "그집짬뽕0927",
fontName: .bold,
Copy link
Member

Choose a reason for hiding this comment

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

이 Text 같은 경우는 매개변수로 바꾸실 예정인거죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재는 UI 구성 상태만 확인하기 위해 임시로 텍스트를 넣어둔 상태이고 실제 가게명은 API 응답 데이터를 통해 받아와 뷰모델에서 바인딩할 예정입니다! 그래서 이 부분은 이후 네트워크 연결 및 뷰모델 설계 시 동적으로 처리될 계획입니다

Comment on lines +85 to +88
seeAllButton.snp.makeConstraints {
$0.centerY.horizontalEdges.equalToSuperview()
$0.height.equalTo(50)
}
Copy link
Member

Choose a reason for hiding this comment

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

아직 snapkit에 익숙하지 않아서 열심히 배우겠습니다!

Comment on lines +52 to +55
logoImageView.do {
$0.image = .imgLogo
$0.contentMode = .scaleAspectFit
}
Copy link
Member

Choose a reason for hiding this comment

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

Then을 쓰면 이렇게 간결해지는군요

Comment on lines 9 to 16

enum HomeSection: Int, CaseIterable {
case vote, nearbyRanking, recommendation, hiddenGem

var headerTitle: String? {
switch self {
case .vote: return nil
case .nearbyRanking: return "주변 맛집 랭킹"
Copy link
Member

Choose a reason for hiding this comment

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

상수값 들도 관리하는 것 같은데 HomeSection이라는 변수명으로 지으신 이유가 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

혹시 질문 주신 부분이 headerTitle이나 footerButtonTitle처럼 HomeSection 내부에 정의된 텍스트 관련 프로퍼티를 말씀하신 걸까요?

만약 그 부분을 말씀하신 거라면 HomeSection은 단순히 텍스트 상수를 모아놓은 enum이라기보다는, 홈 화면 내 각 섹션의 성격과 역할을 표현하고 관련된 뷰 구성 요소들(헤더, 푸터, 셀 ID, supplementary 여부 등)을 응집시킨 모델 타입으로 구성했습니다

혹시 제가 질문 의도를 잘못 이해했다면 다시 한번 말씀 부탁드립니다!

  • 지금 다시 코드를 보니 MVVM 패턴의 확장성 측면에서는 구조적으로 제한적일 수 있다는 것을 깨달았네요. 현재는 UI 구성 중심으로 빠르게 작업하면서 뷰모델 분리까지는 고려하지 못했는데 실제 데이터를 바인딩하거나 뷰 로직이 복잡해지는 시점에는 각 섹션을 뷰모델 단위로 분리하고 역할을 재정의하는 방향으로 구조를 개선해야겠어요,

Copy link
Member

Choose a reason for hiding this comment

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

아하 각 세션의 성격을 표현하는 부분이였군요

저가 물었던 부분은 이 부분이 상수값들을 모아둔 enum이랑 분리되는게 좋지 않을까 했는데

섹션의 성격이 저장된 부분이면 지금처럼 상수값이 들어있어도 괜찮을 것 같아요

Comment on lines 10 to 27
protocol SectionController {
func numberOfItems() -> Int

func cellForItem(
at indexPath: IndexPath,
in collectionView: UICollectionView
) -> UICollectionViewCell

func header(
in collectionView: UICollectionView,
at indexPath: IndexPath
) -> UICollectionReusableView?

func footer(
in collectionView: UICollectionView,
at indexPath: IndexPath
) -> UICollectionReusableView?
}
Copy link
Member

Choose a reason for hiding this comment

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

이부분이 그럼 나중에 다른 화면도 섹션을 사용하면 이렇게 사용하면 될까요?
protocol 명칭으로는 다른 섹션 부분에서도 protocol을 채택하면 될까 궁금해요!

만약 섹션을 쓰는 다른곳에서도 하게 된다면 파일 위치를 이동해도 좋을 것 같습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋은 질문 및 의견 감사합니다!
SectionController는 UICollectionView 기반의 섹션 UI를 분리하고 모듈화하기 위한 프로토콜로 설계된 구조이기 때문에 다른 화면에서도 재사용이 가능합니다!
말씀주신 것처럼 이 프로토콜이 특정 화면에 종속되지 않고 공통 역할을 수행한다면 Presentation/Common/Section 또는 Common/Protocol과 같은 경로로 파일 위치를 옮기는데 더 좋은 방향이네요! 반영하도록 하겠습니다:)


final class VoteSectionController: SectionController {

func numberOfItems() -> Int { 1 }
Copy link
Member

@alstjr7437 alstjr7437 Jun 18, 2025

Choose a reason for hiding this comment

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

    func numberOfItems() -> Int {
        section.numberOfItems
    }

이부분은 이렇게 안하신 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

앗 이 부분은 제가 놓친 부분이네요. 꼼꼼하게 봐주셔서 감사합니다!

@rafa-e1
Copy link
Contributor Author

rafa-e1 commented Jun 18, 2025

코드 줄을 보니 정말 많더라구요!! 그래도 어느정도 다 읽어보면서 리뷰 남겼습니다!!

따로 궁금한점 질문이나 사용했던걸 구경했어요 사실 ㅋㅋ... 너무 많이 들어오다보니 아직 자세하게 이해는 못했지만 사용해보면서 같이 맞춰나가요!!

혹시 다음 PR부터는 만약

홈 투표 UI 및 내비를 만든다고 하면 중간에 작업이 뻗어 나가는걸 PR로 나눠주실 수 있을까요!

  • 홈 투표 UI 및 내비게이션 바 구현

    • UI 구현에 필요한 Foundation 구현
    • BaseView 등 기본적인 필요한 class 구현
    • Footer View 구현
    • Header View 구현
    • ...

이런식으로 뻗어나가는게 좋을 것 같은데 어떻게 생각하시나요!?

위와 같이 하면 각 부분에 맞는 코드들만 볼 수 있어서 리뷰하기 좋고 기능 분리도 좋을 것 같습니다!

좋은 제안 감사합니다! 다음 PR부터는 기능 단위로 더 명확하게 나눠서 작업을 진행하겠습니다:)

rafa-e1 added 17 commits June 29, 2025 18:55
- HiddenGemCellViewModel, NearbyRankingCellViewModel, RecommendationCellViewModel, VoteCellViewModel 추가
- 도메인 모델을 뷰 표현 데이터로 변환하도록 설계
- 향후 API 연동 시 확장 가능하도록 초기 구조 마련
- HomeViewModelProtocol이 불필요한 추상화로 판단되어 제거
- ViewModel을 교체하거나 다형성을 활용할 요구사항이 없고
  구체 타입으로 충분히 역할을 수행 가능하므로 단순화
- 코드 가독성과 유지보수성을 위해 인터페이스 중복을 제거
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (1)

36-44: ViewModel 바인딩이 잘 구현되었습니다!

이전 리뷰에서 지적된 하드코딩 문제가 해결되고, VoteCellViewModel을 통한 데이터 바인딩이 적절하게 구현되었습니다.

🧹 Nitpick comments (11)
MatzipBook/MatzipBook/Presentation/Home/ViewModel/VoteCellViewModel.swift (1)

18-26: 하드코딩된 더미 이미지를 개선해 주세요.

computed property들이 MVVM 패턴을 잘 따르고 있지만, 좌우 레스토랑의 더미 이미지가 하드코딩되어 있습니다. 이는 유지보수성을 떨어뜨릴 수 있습니다.

다음과 같이 개선을 고려해보세요:

var leftRestaurantImage: UIImage? {
-    vote.leftRestaurant.thumbnail ?? .imgDummy0
+    vote.leftRestaurant.thumbnail ?? UIImage.defaultRestaurantPlaceholder
}
var rightRestaurantImage: UIImage? {
-    vote.rightRestaurant.thumbnail ?? .imgDummy1
+    vote.rightRestaurant.thumbnail ?? UIImage.defaultRestaurantPlaceholder
}

또는 이미지 플레이스홀더를 관리하는 별도의 매니저나 상수를 도입하는 것을 고려해보세요.

MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (1)

12-12: 하드코딩된 대학교 이름을 설정 가능하게 만들어 주세요.

universityName이 "부경대학교"로 하드코딩되어 있습니다. 이후 다른 대학교 지원이나 동적 설정을 위해 초기화 시점에 주입받도록 개선을 고려해보세요.

-var universityName: String = "부경대학교"
+var universityName: String

-init() {
+init(universityName: String = "부경대학교") {
+    self.universityName = universityName
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (1)

41-45: 배열 인덱스 접근 시 안전성을 고려해주세요.

items[indexPath.item] 접근 시 bounds 체킹이 없습니다. UICollectionView가 올바른 인덱스를 제공해야 하지만, 방어적 프로그래밍을 위해 guard 문을 추가하는 것을 고려해보세요.

-let cellData: NearbyRanking = items[indexPath.item]
+guard indexPath.item < items.count else {
+    return UICollectionViewCell()
+}
+let cellData: NearbyRanking = items[indexPath.item]
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (1)

33-35: 배열 인덱스 접근 시 안전성을 고려해주세요.

다른 섹션 컨트롤러와 마찬가지로 items[indexPath.item] 접근 시 bounds 체킹을 추가하는 것을 고려해보세요.

-let cellData: Vote = items[indexPath.item]
+guard indexPath.item < items.count else {
+    return UICollectionViewCell()
+}
+let cellData: Vote = items[indexPath.item]
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (2)

10-83: 섹션 컨트롤러들 간의 코드 중복을 고려해보세요.

HiddenGemSectionController, NearbyRankingSectionController, RecommendationSectionController가 거의 동일한 구조를 가지고 있습니다. 향후 유지보수성을 위해 제네릭 베이스 클래스나 프로토콜 확장을 고려해볼 수 있습니다.

현재 구현도 명확하고 이해하기 쉬우므로 즉시 변경이 필요한 것은 아니지만, 추후 리팩토링 시 참고해주세요.


41-45: 배열 인덱스 접근 시 안전성을 고려해주세요.

다른 섹션 컨트롤러와 마찬가지로 items[indexPath.item] 접근 시 bounds 체킹을 추가하는 것을 고려해보세요.

MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (1)

41-45: 배열 인덱스 접근 시 안전성을 고려해주세요.

다른 섹션 컨트롤러와 마찬가지로 items[indexPath.item] 접근 시 bounds 체킹을 추가하는 것을 고려해보세요.

MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (2)

146-180: supplementary view 반환 로직 개선 필요

현재 headerfooter가 nil일 경우 빈 UICollectionReusableView()를 반환하고 있습니다. 이는 예상치 못한 빈 공간을 만들 수 있습니다.

더 나은 접근 방법:

  1. 섹션 레이아웃에서 해당 supplementary view의 크기를 0으로 설정
  2. 또는 섹션별로 header/footer 필요 여부를 명시적으로 관리

103-103: 매직 넘버를 상수로 분리하세요

contentInset.bottom = 60의 60이 어떤 의미인지 명확하지 않습니다. 탭바 높이나 다른 UI 요소를 고려한 값이라면 명확한 상수로 분리하는 것이 좋습니다.

-        collectionView.contentInset.bottom = 60
+        collectionView.contentInset.bottom = Constants.collectionViewBottomInset

Constants 구조체에 다음과 같이 정의:

private enum Constants {
    static let collectionViewBottomInset: CGFloat = 60  // 탭바 공간 확보
}
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift (1)

12-22: 하드코딩된 값들을 TODO로 표시하세요

현재 userName: "범수", generation: "19" 등이 하드코딩되어 있습니다. 이는 개발 초기 단계에서 이해할 수 있지만, 실제 데이터로 교체해야 함을 명확히 하기 위해 TODO 주석을 추가하는 것이 좋습니다.

또한 universityName 파라미터가 사용되지 않고 있습니다.

    static func makeSections(
        for universityName: String,
        onSeeAll: @escaping (HomeSectionType) -> Void
    ) -> [SectionDisplayable] {
+       // TODO: 실제 사용자 데이터로 교체 필요
        return [
            makeVoteSection(),
            makeNearbyRankingSection(onSeeAll),
            makePersonalRecommendationSection(userName: "범수", onSeeAll),
            makeHiddenGemSection(generation: "19", onSeeAll)
        ]
    }
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (1)

204-213: 그림자 설정 최적화 필요

shadowOpacity = 1은 매우 진한 그림자를 만듭니다. 또한 그림자 렌더링은 성능에 영향을 줄 수 있으므로 최적화가 필요합니다.

    func configureBubbleImageView(_ imageView: UIImageView) {
        imageView.do {
            $0.image = .imgBubble.withRenderingMode(.alwaysOriginal)
            $0.contentMode = .scaleAspectFill
            $0.layer.shadowRadius = 10
-           $0.layer.shadowOpacity = 1
+           $0.layer.shadowOpacity = 0.13
            $0.layer.shadowOffset = .zero
-           $0.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.13).cgColor
+           $0.layer.shadowColor = UIColor.black.cgColor
+           // 성능 최적화를 위한 shadowPath 설정 고려
+           // $0.layer.shadowPath = UIBezierPath(rect: $0.bounds).cgPath
        }
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef492e and 1d1e416.

📒 Files selected for processing (28)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Enum/HomeSectionType.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/DefaultSectionLayout.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/HomeSectionLayoutProvider.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/Supplementary/HomeSupplementaryItemLayout.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/VoteSectionLayout.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HeaderFooterDisplayable.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HomeSectionLayoutProviding.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionDisplayable.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionLayoutProviding.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Model/NearbyRanking.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Model/RecommendedRestaurant.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Model/Restaurant.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/Model/Vote.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/HiddenGemCellViewModel.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/NearbyRankingCellViewModel.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/PersonalRecommendationCellViewModel.swift (1 hunks)
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/VoteCellViewModel.swift (1 hunks)
✅ Files skipped from review due to trivial changes (13)
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HeaderFooterDisplayable.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionLayoutProviding.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Enum/HomeSectionType.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/VoteSectionLayout.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/DefaultSectionLayout.swift
  • MatzipBook/MatzipBook/Presentation/Home/Model/Restaurant.swift
  • MatzipBook/MatzipBook/Presentation/Home/Model/Vote.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/Supplementary/HomeSupplementaryItemLayout.swift
  • MatzipBook/MatzipBook/Presentation/Home/Model/NearbyRanking.swift
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/PersonalRecommendationCellViewModel.swift
  • MatzipBook/MatzipBook/Presentation/Home/ViewModel/HiddenGemCellViewModel.swift
  • MatzipBook/MatzipBook/Presentation/Home/Model/RecommendedRestaurant.swift
  • MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/HomeSectionLayoutProvider.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift
  • MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift
🧰 Additional context used
🧬 Code Graph Analysis (6)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (3)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (4)
  • numberOfItems (18-20)
  • cellForItem (22-38)
  • header (40-45)
  • footer (47-52)
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift (1)
MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (1)
  • setupStyles (98-104)
MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (1)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift (1)
  • makeSections (12-22)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (3)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (3)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (4)
  • numberOfItems (18-20)
  • cellForItem (22-38)
  • header (40-45)
  • footer (47-52)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (3)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (4)
  • numberOfItems (26-28)
  • cellForItem (30-48)
  • header (50-65)
  • footer (67-82)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (4)
  • numberOfItems (18-20)
  • cellForItem (22-38)
  • header (40-45)
  • footer (47-52)
🔇 Additional comments (8)
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HomeSectionLayoutProviding.swift (1)

10-12: 깔끔한 프로토콜 설계입니다.

홈 섹션별 레이아웃 구성을 위한 명확한 추상화가 잘 구현되어 있습니다. 메소드 시그니처가 직관적이고 단일 책임 원칙을 잘 따르고 있습니다.

MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift (1)

15-21: 섹션 초기화 로직이 잘 구현되었습니다.

HomeSectionFactory를 통한 섹션 생성과 클로저에서의 weak self 캡처가 올바르게 구현되어 있습니다. 메모리 누수를 방지하는 좋은 실천입니다.

MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionDisplayable.swift (1)

10-27: 우수한 프로토콜 설계입니다.

섹션별 표시 로직을 깔끔하게 추상화한 잘 설계된 프로토콜입니다. UICollectionView의 데이터 소스 패턴을 따르면서도 섹션별로 책임을 분리할 수 있게 하여 관심사의 분리가 잘 이루어져 있습니다. 메소드 시그니처도 명확하고 직관적입니다.

MatzipBook/MatzipBook/Presentation/Home/ViewModel/NearbyRankingCellViewModel.swift (1)

23-30: 거리 포맷팅 로직의 단위 가정을 명확히 해주세요.

formattedDistance 프로퍼티에서 nearbyRanking.distance가 킬로미터 단위라고 가정하고 있습니다. 이 가정이 올바른지 확인하고, 가능하다면 NearbyRanking 모델이나 이 프로퍼티에 단위에 대한 문서화를 추가해주세요.

var formattedDistance: String {
    // nearbyRanking.distance는 킬로미터 단위로 가정
    if nearbyRanking.distance < 1.0 {
        let meters: Int = Int(nearbyRanking.distance * 1000)
        return "\(meters)m"
    } else {
        return String(format: "%.1fkm", nearbyRanking.distance)
    }
}
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift (1)

10-83: 섹션 컨트롤러 구현이 잘 구조화되어 있습니다.

SectionDisplayable과 HeaderFooterDisplayable 프로토콜을 올바르게 구현했고, 일관된 패턴을 따라 코드가 잘 구성되어 있습니다. 다른 섹션 컨트롤러들과의 일관성도 유지되고 있습니다.

MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift (1)

10-53: 투표 섹션에 적합한 깔끔한 구현입니다.

헤더/푸터가 필요없는 투표 섹션의 특성에 맞게 적절히 구현되었습니다. SectionDisplayable 프로토콜을 올바르게 구현하고 일관된 패턴을 따르고 있습니다.

MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift (1)

10-83: 일관성 있고 잘 구조화된 섹션 컨트롤러 구현입니다.

모든 섹션 컨트롤러가 일관된 패턴을 따라 구현되어 있어 코드 가독성과 유지보수성이 좋습니다. SectionDisplayable과 HeaderFooterDisplayable 프로토콜을 적절히 활용한 모듈화된 설계가 인상적입니다.

MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift (1)

14-28: 프로퍼티 선언이 깔끔하고 적절합니다!

lazy var를 사용한 collectionView 초기화와 클로저 내부에서 [weak self] 사용이 적절합니다. 메모리 누수를 방지하면서도 필요한 시점에 레이아웃을 설정할 수 있도록 잘 구현되었습니다.

Comment on lines +18 to +20
override func setupStyles() {
contentView.backgroundColor = .systemBlue
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

플레이스홀더 스타일링을 실제 구현으로 교체해 주세요.

현재 systemBlue 배경색은 플레이스홀더로 보입니다. 실제 디자인에 맞는 스타일링으로 교체가 필요합니다.

🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift
at lines 18 to 20, the backgroundColor is set to .systemBlue as a placeholder.
Replace this placeholder color with the actual design-specified background color
to match the intended UI style.


// MARK: - Bindings

func configure(with viewModel: PersonalRecommendationCellViewModel) {}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

configure 메소드 구현이 누락되었습니다.

configure(with viewModel: PersonalRecommendationCellViewModel) 메소드가 비어있어 뷰모델의 데이터가 UI에 반영되지 않습니다. 이는 기능상 중요한 문제입니다.

🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift
at line 14, the configure(with viewModel: PersonalRecommendationCellViewModel)
method is empty, so the view model data is not applied to the UI. Implement this
method to update the cell's UI elements using the properties from the provided
viewModel to properly reflect the data.

Comment on lines +23 to +29
func didTapSearchButton() {
print("DEBUG: Search button tapped")
}

func didTapSeeAllButton(for section: HomeSectionType) {
print("DEBUG: See all button tapped for section >>> \(section)")
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

print문을 적절한 로깅 시스템으로 교체해 주세요.

현재 디버그 print문이 사용되고 있는데, 이는 프로덕션 환경에서 성능 문제를 일으킬 수 있습니다. 적절한 로깅 시스템이나 디버그 모드에서만 동작하는 로거 사용을 고려해보세요.

func didTapSearchButton() {
-    print("DEBUG: Search button tapped")
+    Logger.debug("Search button tapped")
}

func didTapSeeAllButton(for section: HomeSectionType) {
-    print("DEBUG: See all button tapped for section >>> \(section)")
+    Logger.debug("See all button tapped for section: \(section)")
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func didTapSearchButton() {
print("DEBUG: Search button tapped")
}
func didTapSeeAllButton(for section: HomeSectionType) {
print("DEBUG: See all button tapped for section >>> \(section)")
}
func didTapSearchButton() {
Logger.debug("Search button tapped")
}
func didTapSeeAllButton(for section: HomeSectionType) {
Logger.debug("See all button tapped for section: \(section)")
}
🤖 Prompt for AI Agents
In MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift around
lines 23 to 29, replace the debug print statements with a proper logging system
that supports different log levels and can be disabled or filtered in
production. Implement or use an existing logger that only outputs these messages
in debug mode to avoid performance issues in production.

Comment on lines +36 to +72
private static func makeNearbyRankingSection(
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [NearbyRanking] = []

return NearbyRankingSectionController(
headerTitle: "주변 맛집 랭킹",
footerTitle: "주변 맛집 전체보기",
items: items
)
}

private static func makePersonalRecommendationSection(
userName: String,
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [RecommendedRestaurant] = []

return RecommendationSectionController(
headerTitle: "\(userName)님 취향 추천",
footerTitle: "취향 추천 전체보기",
items: items
)
}

private static func makeHiddenGemSection(
generation: String,
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [RecommendedRestaurant] = []

return HiddenGemSectionController(
headerTitle: "\(generation)학번이 추천하는 숨은 찐 맛집",
footerTitle: "찐 맛집 전체보기",
items: items
)
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

onSeeAll 클로저가 사용되지 않습니다

각 섹션 생성 메서드에서 onSeeAll 클로저를 받고 있지만, 실제로 SectionController 생성자에 전달되지 않고 있습니다. 이로 인해 "전체보기" 버튼 액션이 제대로 동작하지 않을 수 있습니다.


🏁 Script executed:

#!/bin/bash
# Description: SectionController 클래스들의 생성자를 확인하여 onSeeAll 파라미터 지원 여부 검토

# NearbyRankingSectionController의 생성자 확인
ast-grep --pattern 'class NearbyRankingSectionController {
  $$$
  init($$$) {
    $$$
  }
  $$$
}'

# RecommendationSectionController의 생성자 확인
ast-grep --pattern 'class RecommendationSectionController {
  $$$
  init($$$) {
    $$$
  }
  $$$
}'

# HiddenGemSectionController의 생성자 확인
ast-grep --pattern 'class HiddenGemSectionController {
  $$$
  init($$$) {
    $$$
  }
  $$$
}'

Length of output: 302


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 NearbyRankingSectionController 정의 검색"
rg "class NearbyRankingSectionController" -n

echo "🔍 RecommendationSectionController 정의 검색"
rg "class RecommendationSectionController" -n

echo "🔍 HiddenGemSectionController 정의 검색"
rg "class HiddenGemSectionController" -n

echo "🔍 SectionController 파일들의 init 시그니처 검색"
rg "init" -n -g "*SectionController.swift"

Length of output: 1566


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 NearbyRankingSectionController init 시그니처"
rg "init" -n -A2 -B2 -g "*NearbyRankingSectionController.swift"

echo "🔍 RecommendationSectionController init 시그니처"
rg "init" -n -A2 -B2 -g "*RecommendationSectionController.swift"

echo "🔍 HiddenGemSectionController init 시그니처"
rg "init" -n -A2 -B2 -g "*HiddenGemSectionController.swift"

Length of output: 2367


onSeeAll 클로저가 전달되지 않아 “전체보기” 버튼이 동작하지 않습니다

HomeSectionFactory.swift (36–72줄)의 각 섹션 생성 메서드가 onSeeAll 클로저를 인자로 받지만, 실제로 SectionController 생성자에 넘기지 않아 버튼 액션이 연결되지 않습니다.

수정 방향:

  • makeNearbyRankingSection, makePersonalRecommendationSection, makeHiddenGemSection 메서드에서 onSeeAll 클로저를 각 컨트롤러의 init에 전달하도록 변경
  • 필요 시 NearbyRankingSectionController, RecommendationSectionController, HiddenGemSectionController init 시그니처에
    onSeeAll: @escaping (HomeSectionType) -> Void 파라미터를 추가하고, footer 버튼 액션에 연결 구현

예시 diff:

- return NearbyRankingSectionController(
-     headerTitle: "주변 맛집 랭킹",
-     footerTitle: "주변 맛집 전체보기",
-     items: items
- )
+ return NearbyRankingSectionController(
+     headerTitle: "주변 맛집 랭킹",
+     footerTitle: "주변 맛집 전체보기",
+     items: items,
+     onSeeAll: onSeeAll
+ )
  • 동일한 방식으로 다른 두 섹션에도 onSeeAll 전달 추가 필요
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static func makeNearbyRankingSection(
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [NearbyRanking] = []
return NearbyRankingSectionController(
headerTitle: "주변 맛집 랭킹",
footerTitle: "주변 맛집 전체보기",
items: items
)
}
private static func makePersonalRecommendationSection(
userName: String,
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [RecommendedRestaurant] = []
return RecommendationSectionController(
headerTitle: "\(userName)님 취향 추천",
footerTitle: "취향 추천 전체보기",
items: items
)
}
private static func makeHiddenGemSection(
generation: String,
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [RecommendedRestaurant] = []
return HiddenGemSectionController(
headerTitle: "\(generation)학번이 추천하는 숨은 찐 맛집",
footerTitle: "찐 맛집 전체보기",
items: items
)
}
private static func makeNearbyRankingSection(
_ onSeeAll: @escaping (HomeSectionType) -> Void
) -> SectionDisplayable {
let items: [NearbyRanking] = []
return NearbyRankingSectionController(
headerTitle: "주변 맛집 랭킹",
footerTitle: "주변 맛집 전체보기",
items: items,
onSeeAll: onSeeAll
)
}
🤖 Prompt for AI Agents
In
MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift
between lines 36 and 72, the onSeeAll closure parameter is accepted by the
section factory methods but not passed to the SectionController initializers,
causing the "See All" button to not function. To fix this, update the
makeNearbyRankingSection, makePersonalRecommendationSection, and
makeHiddenGemSection methods to pass the onSeeAll closure to their respective
SectionController initializers. Additionally, modify the initializers of
NearbyRankingSectionController, RecommendationSectionController, and
HiddenGemSectionController to accept an onSeeAll parameter of type @escaping
(HomeSectionType) -> Void and connect this closure to the footer button action
to enable the button functionality.

Copy link
Member

@alstjr7437 alstjr7437 left a comment

Choose a reason for hiding this comment

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

아직 아무것도 안하고 거의 리뷰만 달고 있는 저가 뭔가 정말 죄송하네요...

매번 작업은 안해놓고 리뷰만 달아서 정말 죄송합니다,, ㅠㅠ

저가 할거리만 늘리는 것 같네요 ㅠ,, 이번에 컨벤션이 갖춰지면 열심히 로그인, 온보딩 화면 작업할게요

Comment on lines +17 to +21
private let leftNameImageView: UIImageView = UIImageView()
private let leftNameLabel: UILabel = UILabel()

private let rightNameImageView: UIImageView = UIImageView()
private let rightNameLabel: UILabel = UILabel()
Copy link
Member

Choose a reason for hiding this comment

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

조금 사소한 부분이지만 해당 부분들을 leftItem과 rightItem으로 ImageView, Label, Button을 컴포넌트로 묶을 수도 있겠네요!

return nil
}

header.configure(title: headerTitle)
Copy link
Member

Choose a reason for hiding this comment

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

header부분의 title이 nil일때 보여주는 텍스트는 필요 없을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 구조상 NearbyRankingSection, PersonalRecommendationSection, HiddenGemSection은 항상 헤더 타이틀이 존재하고 VoteSection처럼 헤더, 푸터가 필요 없는 섹션만 nil을 전달하고 있습니다! 그래서 headerTitle이 nil인 경우는 의도적으로 헤더가 필요 없는 섹션이기 때문에 별도의 기본값은 적용하지 않아도 괜찮다고 판단했습니다. 이후에 headerTitle이 optional이면서도 placeholder를 보여줘야 하는 상황이 생기면 그때 반영해도 충분할 것 같습니다!

Comment on lines +42 to +44
let viewModel: NearbyRankingCellViewModel = NearbyRankingCellViewModel(
nearbyRanking: cellData
)
Copy link
Member

Choose a reason for hiding this comment

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

아래 VC도 같은 부분인데 cellForItem을 할때마다 ViewModel을 init하는게 맞나요??
따로 인스턴스를 가지고 있지 않고 만드는 부분이 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋은 질문 감사합니다!

현재 구조에서는 cellForItem에서 매번 NearbyRankingCellViewModel을 생성하고 있는데 뷰모델이 단순히 데이터 변환만 담당하고 있어서 상태를 보존하거나 복잡한 로직을 처리하지 않기 때문에 매번 생성해도 성능상 큰 문제가 없다고 판단했습니다! 이후에 Diffable DataSource를 도입해소 섹션과 아이템 단위의 상태를 더 효율적으로 관리할 예정이라 뷰모델을 따로 캐싱하거나 배열로 관리하는 방식으로 개선할 여지도 고려하고 있습니다. 즉, 현재 구조는 간단성을 우선한 설계이고 데이터 업데이트나 셀 상태 관리가 복잡해질 때 Diffable과 함께 뷰모델의 재사용을 적용할 계획입니다.

Comment on lines +10 to +22
enum HomeSectionType: Int {
case vote, nearbyRanking, personalRecommendation, hiddenGem
}

extension HomeSectionType {

func sectionLayout() -> NSCollectionLayoutSection {
switch self {
case .vote: return HomeLayout.voteSectionLayout
default: return HomeLayout.defaultSectionLayout
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Home Section을 enum 나눠서 가지고 있으니 어떠한 섹션이 있는지 보기도 좋고 기능적으로 확장성도 좋네요!

Comment on lines +15 to +23
final class HomeSectionLayoutProvider: HomeSectionLayoutProviding {

func layout(for section: HomeSectionType) -> NSCollectionLayoutSection {
switch section {
case .vote: return HomeLayout.voteSectionLayout
default: return HomeLayout.defaultSectionLayout
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

섹션 팩토리랑 layout을 나눈 이유는
View Layout잡는 코드와 내용이 들어가는 코드를 분할하기 위해서 일까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

섹션에 채워질 데이터 구성 책임과 그 데이터를 어떻게 레이아웃으로 표현할지 책임을 분리하여 관심사를 구분했습니다!

struct Vote {
let leftRestaurant: Restaurant
let rightRestaurant: Restaurant
let remainingTime: Int
Copy link
Member

Choose a reason for hiding this comment

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

이부분은 따로 Date로 가지지 않고 Int로 가지고 있는걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아직 API 명세가 완성되지 않아서 우선은 UI에서 바로 표시할 수 있도록 임의로 Int 타입으로 남은 시간을 설정해두었습니다! 이후에 API 명세가 확정되면 서버에서 전달되는 데이터 형식에 맞춰 Date 혹은 다른 적절한 타입으로 수정할 계획입니다:)


final class HomeViewModel {

var universityName: String = "부경대학교"
Copy link
Member

Choose a reason for hiding this comment

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

이친구는 따로 private(set)으로 안한 이유가 있을까요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이후에 대학을 변경할 수 있도록 만들 계획이기 때문입니다!

Comment on lines +11 to +23

private let nearbyRanking: NearbyRanking

init(nearbyRanking: NearbyRanking) {
self.nearbyRanking = nearbyRanking
}

var ranking: Int { nearbyRanking.ranking }
var restaurantName: String { nearbyRanking.name }
var restaurantImage: UIImage? { nearbyRanking.image }
var rating: Double { nearbyRanking.rating }
var starCount: Int { nearbyRanking.starCount }
var formattedDistance: String {
Copy link
Member

Choose a reason for hiding this comment

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

어짜피 let이라면
var 로 아래 인스턴스를 뽑아내지 않고 아래와 같이 안하신 이유가 있을까요?!

Suggested change
private let nearbyRanking: NearbyRanking
init(nearbyRanking: NearbyRanking) {
self.nearbyRanking = nearbyRanking
}
var ranking: Int { nearbyRanking.ranking }
var restaurantName: String { nearbyRanking.name }
var restaurantImage: UIImage? { nearbyRanking.image }
var rating: Double { nearbyRanking.rating }
var starCount: Int { nearbyRanking.starCount }
var formattedDistance: String {
let nearbyRanking: NearbyRanking
init(nearbyRanking: NearbyRanking) {
self.nearbyRanking = nearbyRanking
}
var formattedDistance: String {

Comment on lines +11 to +23

private let personalRecommendation: RecommendedRestaurant

init(personalRecommendation: RecommendedRestaurant) {
self.personalRecommendation = personalRecommendation
}

var restaurantImage: UIImage? { personalRecommendation.image }
var restaurantRating: Double { personalRecommendation.rating }
var restaurantName: String { personalRecommendation.name }
var restaurantDescription: String { personalRecommendation.description }
var foodCategory: String { personalRecommendation.foodCategory }
var themeCategory: String { personalRecommendation.themeCategory }
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분도 위와 같이 궁금해요!

Comment on lines +18 to +26
var leftRestaurantName: String { vote.leftRestaurant.name }
var leftRestaurantImage: UIImage? {
vote.leftRestaurant.thumbnail ?? .imgDummy0
}
var rightRestaurantName: String { vote.rightRestaurant.name }
var rightRestaurantImage: UIImage? {
vote.rightRestaurant.thumbnail ?? .imgDummy1
}
var remainingTime: Int { vote.remainingTime }
Copy link
Member

Choose a reason for hiding this comment

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

이 부분도!🙇

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재는 지적해주신 뷰모델이 모델의 데이터를 감싼 형태로 설계되어 있습니다.
향후 Cell의 표현 방식(예: 거리 단위 포맷, 별점 표시, 카테고리 텍스트 포맷 등)이 바뀔 때 뷰모델에서 쉽게 가공할 수 있도록 하고 UI 레이어에서의 변경에 따라 모델을 직접 건드리지 않도록 명확히 역할을 나누기 위해 뷰모델을 분리했습니다!

말씀주신대로 단순히 let 프로퍼티를 직접 넘겨도 되지만 표현 책임을 뷰모델에 위임해두면 추후 UI 변경이 발생해도 모델에는 영향이 없어서 유지보수가 더 편할 것으로 판단했습니다.

나중에 뷰모델이 너무 얇으면 제거하고 바로 모델을 전달하는 방식으로도 변경 가능하니 의견 주시면 조율해 보겠습니다!

@rafa-e1 rafa-e1 merged commit f046e4d into develop Jul 1, 2025
2 checks passed
@rafa-e1 rafa-e1 deleted the feat/#16 branch July 1, 2025 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎸 Chore 코드 기능과 무관한 프로젝트 설정, 도구 추가, 유지보수 등 환경 개선 작업 ✨ Feature 기능 구현 🛠️ Refactor Refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

홈 투표 UI 및 내비게이션 바 구현

2 participants