Conversation
- Common/Extension -> UI/Extension
- 주변 맛집 랭킹 셀 - 맞춤형 추천 셀 - 숨은 찐 맛집 셀
Walkthrough이번 변경사항은 홈 화면의 투표 UI와 커스텀 내비게이션 바를 포함한 홈 화면 전체 구조를 구현하고, 관련된 데이터 모델, 뷰모델, 셀, 섹션 컨트롤러, 레이아웃, 디자인 시스템(폰트, 컬러, 이미지 등)과 확장 유틸리티, 베이스 클래스 등을 추가하는 대규모 초기 개발입니다. 기존 HomeViewController 파일은 제거되고, 새로운 구조로 대체됩니다. Changes
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: 액션 위임
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes(해당 변경사항에서는 모든 코드 변경이 #16 이슈의 "홈 투표 UI 및 내비게이션 바 구현" 목적에 부합하므로, 범위를 벗어난 변경사항이 없습니다.) Poem
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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: 재사용 식별자 네이밍 일관성 및 범위 확장 제안
- UIKit 기본 명칭(
reuseIdentifier)과 달리identifier로 정의되어 혼동을 줄 수 있습니다. 동일 단어 사용이 가독성을 높입니다.- 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:
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) + } }(이 경우
UIKitimport 필요)
선택 사항이니 팀 코딩 컨벤션에 맞춰 판단해 주세요.MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift (1)
10-14:super.setupStyles()호출 여부 확인
BaseCollectionViewCell의setupStyles()가 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.homeVCMatzipBook/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를 사용한 뒤 바로 다른 설정을 체이닝하고 싶을 때 반환값이 없어서 제약이 생깁니다.
@discardableResult와Self를 반환하도록 수정하면 다음과 같이 유연하게 사용할 수 있습니다.-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 = trueMatzipBook/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: 새 섹션 추가 시 누락 위험 – 컴파일 타임 안전성 강화 제안
cellIdentifierswitch 구문은 케이스 추가 시 누락되어도 컴파일 오류가 발생하지 않습니다.@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
⛔ Files ignored due to path filters (15)
MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/ic_clock.svgis excluded by!**/*.svgMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/ic_search.svgis excluded by!**/*.svgMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/img_bubble.svgis excluded by!**/*.svgMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@2x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@3x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@2x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@3x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@2x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@3x.pngis excluded by!**/*.pngMatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/img_vs.svgis excluded by!**/*.svgMatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Bold.otfis excluded by!**/*.otfMatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Regular.otfis 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: universal와preserves-vector-representation: true가 올바르게 설정되어 있어 다양한 해상도에서 선명도를 유지합니다.MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json (1)
1-15: 검색 아이콘 자산 설정이 적절합니다.
SVG 파일이universalidiom으로 등록되었고 벡터 표현이 유지되어 확장성에 문제가 없습니다.MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json (1)
1-23: 더미 이미지 스케일 구성 확인 완료.
1x, 2x, 3x 스케일이 모두universalidiom으로 올바르게 선언되어 있어 다양한 기기에서 정상 표시됩니다.MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift (1)
19-20: 새 아이콘 enum 추가가 적절합니다.
icSearch와icClock케이스가 올바른 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.
| func layout(for section: HomeSection) -> NSCollectionLayoutSection { | ||
| switch section { | ||
| case .vote: VoteSectionLayout().layout() | ||
| default: DefaultSectionLayout().layout() | ||
| } |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
switch 문에서 return 키워드를 작성하지 않아 Void를 반환하게 된 점은 놓친 부분이 맞습니다. 리뷰 주신 대로 return 구문을 작성하여 컴파일 오류를 해결하겠습니다.
현재는 섹션의 수가 적고 레이아웃의 복잡도가 높지 않아 초기 단계에서는 가독성과 구현 단순성을 우선시했습니다. 이후 섹션 수가 많아지고 레이아웃 생성 비용이 높아질 경우를 대비해서 static 프로퍼티를 활용하여 리팩터리;ㅇ하겠습니다.
There was a problem hiding this comment.
@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!
| 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?() | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
There was a problem hiding this comment.
감사합니다. 제안해주신 대로 prepareForReuse()를 구현하여 onTap 클로저를 해제하고 버튼 상태를 초기화하도록 수정하겠습니다.
MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift
Show resolved
Hide resolved
| 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 | ||
| } |
There was a problem hiding this comment.
오토레이아웃 제약 불안정 – 높이 결정 문제
searchButton을 centerY에 맞추고, separatorView.top을 searchButton.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.
| let cell: UICollectionViewCell = collectionView.dequeueReusableCell( | ||
| withReuseIdentifier: section.cellIdentifier, | ||
| for: indexPath | ||
| ) | ||
|
|
||
| return cell | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
...ipBook/MatzipBook/Presentation/Home/Layout/Section/Controller/DefaultSectionController.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
코드 줄을 보니 정말 많더라구요!!
그래도 어느정도 다 읽어보면서 리뷰 남겼습니다!!
따로 궁금한점 질문이나 사용했던걸 구경했어요 사실 ㅋㅋ...
너무 많이 들어오다보니 아직 자세하게 이해는 못했지만 사용해보면서 같이 맞춰나가요!!
혹시 다음 PR부터는 만약
홈 투표 UI 및 내비를 만든다고 하면 중간에 작업이 뻗어 나가는걸 PR로 나눠주실 수 있을까요!
- 홈 투표 UI 및 내비게이션 바 구현
- UI 구현에 필요한 Foundation 구현
- BaseView 등 기본적인 필요한 class 구현
- Footer View 구현
- Header View 구현
- ...
이런식으로 뻗어나가는게 좋을 것 같은데 어떻게 생각하시나요!?
위와 같이 하면 각 부분에 맞는 코드들만 볼 수 있어서 리뷰하기 좋고 기능 분리도 좋을 것 같습니다!
| enum MatzipImage: String { | ||
| case imgLogo = "img_logo" | ||
| case imgBubble = "img_bubble" | ||
| case imgVS = "img_vs" | ||
| } |
There was a problem hiding this comment.
사용되는 image, Text 들을 enum으로 관리해서 좋네요!!
혹시 따로 궁금한점인데 img_logo나 ic_clock등 snake case를 쓰신 이유가 궁금합니다!
There was a problem hiding this comment.
Asset Catalog에서는 이름 구분할 때 대소문자를 구분하지 않아서 이미지 등록할 때 스네이크 케이스로 등록하는 것이 충돌 가능성을 방지하는걸로 알고 있습니다!
| extension UIFont { | ||
|
|
||
| static func applyFont(_ name: FontName, ofSize size: CGFloat) -> UIFont { | ||
| return UIFont(name: name.rawValue, size: size) ?? .systemFont(ofSize: size) | ||
| } | ||
| } |
There was a problem hiding this comment.
오 폰트를 이렇게 쓰는 방법도 있군요
저는 폰트를 쓸때 SwiftUI긴 하지만
static var pretendardBold14: Font { .pretend(type: .bold, size: 14) }
static var pretendardBold16: Font { .pretend(type: .bold, size: 16) }이런식으로 썻는데 범수님 같이 쓰는게 더 확장성이 좋아보이네요!!
There was a problem hiding this comment.
말씀해주신 방식도 충분히 좋은 접근이고 상황에 따라 더 적합할 수도 있다고 생각합니다!
결국 코드에는 정답은 없고 프로젝트의 성격과 팀의 방향성에 따라 유연하게 선택하는게 가장 중요하다고 생각해요
| extension UIView { | ||
|
|
||
| func addSubviews(_ views: UIView...) { | ||
| views.forEach { addSubview($0) } | ||
| } | ||
| } |
There was a problem hiding this comment.
이부분은 addSubview를 여러번 말고 한번에 다 추가하는 방식이네요!! 배워갑니다!! 🙏
...으로 list가 아닌 방식으로 하는 방식도 좋네요!
There was a problem hiding this comment.
감사합니다! 🙏
개인적으로 ... 을 사용하면 줄바꿈했을 때 더 깔끔한 것 같더라고요:)
| configureLabel( | ||
| leftNameLabel, | ||
| text: "그집짬뽕0927", | ||
| fontName: .bold, |
There was a problem hiding this comment.
이 Text 같은 경우는 매개변수로 바꾸실 예정인거죠?
There was a problem hiding this comment.
현재는 UI 구성 상태만 확인하기 위해 임시로 텍스트를 넣어둔 상태이고 실제 가게명은 API 응답 데이터를 통해 받아와 뷰모델에서 바인딩할 예정입니다! 그래서 이 부분은 이후 네트워크 연결 및 뷰모델 설계 시 동적으로 처리될 계획입니다
| seeAllButton.snp.makeConstraints { | ||
| $0.centerY.horizontalEdges.equalToSuperview() | ||
| $0.height.equalTo(50) | ||
| } |
There was a problem hiding this comment.
아직 snapkit에 익숙하지 않아서 열심히 배우겠습니다!
| logoImageView.do { | ||
| $0.image = .imgLogo | ||
| $0.contentMode = .scaleAspectFit | ||
| } |
|
|
||
| enum HomeSection: Int, CaseIterable { | ||
| case vote, nearbyRanking, recommendation, hiddenGem | ||
|
|
||
| var headerTitle: String? { | ||
| switch self { | ||
| case .vote: return nil | ||
| case .nearbyRanking: return "주변 맛집 랭킹" |
There was a problem hiding this comment.
상수값 들도 관리하는 것 같은데 HomeSection이라는 변수명으로 지으신 이유가 궁금해요!
There was a problem hiding this comment.
혹시 질문 주신 부분이 headerTitle이나 footerButtonTitle처럼 HomeSection 내부에 정의된 텍스트 관련 프로퍼티를 말씀하신 걸까요?
만약 그 부분을 말씀하신 거라면 HomeSection은 단순히 텍스트 상수를 모아놓은 enum이라기보다는, 홈 화면 내 각 섹션의 성격과 역할을 표현하고 관련된 뷰 구성 요소들(헤더, 푸터, 셀 ID, supplementary 여부 등)을 응집시킨 모델 타입으로 구성했습니다
혹시 제가 질문 의도를 잘못 이해했다면 다시 한번 말씀 부탁드립니다!
- 지금 다시 코드를 보니 MVVM 패턴의 확장성 측면에서는 구조적으로 제한적일 수 있다는 것을 깨달았네요. 현재는 UI 구성 중심으로 빠르게 작업하면서 뷰모델 분리까지는 고려하지 못했는데 실제 데이터를 바인딩하거나 뷰 로직이 복잡해지는 시점에는 각 섹션을 뷰모델 단위로 분리하고 역할을 재정의하는 방향으로 구조를 개선해야겠어요,
There was a problem hiding this comment.
아하 각 세션의 성격을 표현하는 부분이였군요
저가 물었던 부분은 이 부분이 상수값들을 모아둔 enum이랑 분리되는게 좋지 않을까 했는데
섹션의 성격이 저장된 부분이면 지금처럼 상수값이 들어있어도 괜찮을 것 같아요
| 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? | ||
| } |
There was a problem hiding this comment.
이부분이 그럼 나중에 다른 화면도 섹션을 사용하면 이렇게 사용하면 될까요?
protocol 명칭으로는 다른 섹션 부분에서도 protocol을 채택하면 될까 궁금해요!
만약 섹션을 쓰는 다른곳에서도 하게 된다면 파일 위치를 이동해도 좋을 것 같습니다!!
There was a problem hiding this comment.
좋은 질문 및 의견 감사합니다!
SectionController는 UICollectionView 기반의 섹션 UI를 분리하고 모듈화하기 위한 프로토콜로 설계된 구조이기 때문에 다른 화면에서도 재사용이 가능합니다!
말씀주신 것처럼 이 프로토콜이 특정 화면에 종속되지 않고 공통 역할을 수행한다면 Presentation/Common/Section 또는 Common/Protocol과 같은 경로로 파일 위치를 옮기는데 더 좋은 방향이네요! 반영하도록 하겠습니다:)
|
|
||
| final class VoteSectionController: SectionController { | ||
|
|
||
| func numberOfItems() -> Int { 1 } |
There was a problem hiding this comment.
func numberOfItems() -> Int {
section.numberOfItems
}이부분은 이렇게 안하신 이유가 궁금합니다!
There was a problem hiding this comment.
앗 이 부분은 제가 놓친 부분이네요. 꼼꼼하게 봐주셔서 감사합니다!
좋은 제안 감사합니다! 다음 PR부터는 기능 단위로 더 명확하게 나눠서 작업을 진행하겠습니다:) |
- HiddenGemCellViewModel, NearbyRankingCellViewModel, RecommendationCellViewModel, VoteCellViewModel 추가 - 도메인 모델을 뷰 표현 데이터로 변환하도록 설계 - 향후 API 연동 시 확장 가능하도록 초기 구조 마련
- HomeViewModelProtocol이 불필요한 추상화로 판단되어 제거 - ViewModel을 교체하거나 다형성을 활용할 요구사항이 없고 구체 타입으로 충분히 역할을 수행 가능하므로 단순화 - 코드 가독성과 유지보수성을 위해 인터페이스 중복을 제거
There was a problem hiding this comment.
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 = universityNameMatzipBook/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 반환 로직 개선 필요현재
header나footer가 nil일 경우 빈UICollectionReusableView()를 반환하고 있습니다. 이는 예상치 못한 빈 공간을 만들 수 있습니다.더 나은 접근 방법:
- 섹션 레이아웃에서 해당 supplementary view의 크기를 0으로 설정
- 또는 섹션별로 header/footer 필요 여부를 명시적으로 관리
103-103: 매직 넘버를 상수로 분리하세요
contentInset.bottom = 60의 60이 어떤 의미인지 명확하지 않습니다. 탭바 높이나 다른 UI 요소를 고려한 값이라면 명확한 상수로 분리하는 것이 좋습니다.- collectionView.contentInset.bottom = 60 + collectionView.contentInset.bottom = Constants.collectionViewBottomInsetConstants 구조체에 다음과 같이 정의:
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
📒 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]사용이 적절합니다. 메모리 누수를 방지하면서도 필요한 시점에 레이아웃을 설정할 수 있도록 잘 구현되었습니다.
| override func setupStyles() { | ||
| contentView.backgroundColor = .systemBlue | ||
| } |
There was a problem hiding this comment.
🛠️ 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) {} |
There was a problem hiding this comment.
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.
| func didTapSearchButton() { | ||
| print("DEBUG: Search button tapped") | ||
| } | ||
|
|
||
| func didTapSeeAllButton(for section: HomeSectionType) { | ||
| print("DEBUG: See all button tapped for section >>> \(section)") | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
💡 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,HiddenGemSectionControllerinit 시그니처에
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.
| 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.
alstjr7437
left a comment
There was a problem hiding this comment.
아직 아무것도 안하고 거의 리뷰만 달고 있는 저가 뭔가 정말 죄송하네요...
매번 작업은 안해놓고 리뷰만 달아서 정말 죄송합니다,, ㅠㅠ
저가 할거리만 늘리는 것 같네요 ㅠ,, 이번에 컨벤션이 갖춰지면 열심히 로그인, 온보딩 화면 작업할게요
| private let leftNameImageView: UIImageView = UIImageView() | ||
| private let leftNameLabel: UILabel = UILabel() | ||
|
|
||
| private let rightNameImageView: UIImageView = UIImageView() | ||
| private let rightNameLabel: UILabel = UILabel() |
There was a problem hiding this comment.
조금 사소한 부분이지만 해당 부분들을 leftItem과 rightItem으로 ImageView, Label, Button을 컴포넌트로 묶을 수도 있겠네요!
| return nil | ||
| } | ||
|
|
||
| header.configure(title: headerTitle) |
There was a problem hiding this comment.
header부분의 title이 nil일때 보여주는 텍스트는 필요 없을까요?
There was a problem hiding this comment.
현재 구조상 NearbyRankingSection, PersonalRecommendationSection, HiddenGemSection은 항상 헤더 타이틀이 존재하고 VoteSection처럼 헤더, 푸터가 필요 없는 섹션만 nil을 전달하고 있습니다! 그래서 headerTitle이 nil인 경우는 의도적으로 헤더가 필요 없는 섹션이기 때문에 별도의 기본값은 적용하지 않아도 괜찮다고 판단했습니다. 이후에 headerTitle이 optional이면서도 placeholder를 보여줘야 하는 상황이 생기면 그때 반영해도 충분할 것 같습니다!
| let viewModel: NearbyRankingCellViewModel = NearbyRankingCellViewModel( | ||
| nearbyRanking: cellData | ||
| ) |
There was a problem hiding this comment.
아래 VC도 같은 부분인데 cellForItem을 할때마다 ViewModel을 init하는게 맞나요??
따로 인스턴스를 가지고 있지 않고 만드는 부분이 궁금합니다!
There was a problem hiding this comment.
좋은 질문 감사합니다!
현재 구조에서는 cellForItem에서 매번 NearbyRankingCellViewModel을 생성하고 있는데 뷰모델이 단순히 데이터 변환만 담당하고 있어서 상태를 보존하거나 복잡한 로직을 처리하지 않기 때문에 매번 생성해도 성능상 큰 문제가 없다고 판단했습니다! 이후에 Diffable DataSource를 도입해소 섹션과 아이템 단위의 상태를 더 효율적으로 관리할 예정이라 뷰모델을 따로 캐싱하거나 배열로 관리하는 방식으로 개선할 여지도 고려하고 있습니다. 즉, 현재 구조는 간단성을 우선한 설계이고 데이터 업데이트나 셀 상태 관리가 복잡해질 때 Diffable과 함께 뷰모델의 재사용을 적용할 계획입니다.
| enum HomeSectionType: Int { | ||
| case vote, nearbyRanking, personalRecommendation, hiddenGem | ||
| } | ||
|
|
||
| extension HomeSectionType { | ||
|
|
||
| func sectionLayout() -> NSCollectionLayoutSection { | ||
| switch self { | ||
| case .vote: return HomeLayout.voteSectionLayout | ||
| default: return HomeLayout.defaultSectionLayout | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Home Section을 enum 나눠서 가지고 있으니 어떠한 섹션이 있는지 보기도 좋고 기능적으로 확장성도 좋네요!
| final class HomeSectionLayoutProvider: HomeSectionLayoutProviding { | ||
|
|
||
| func layout(for section: HomeSectionType) -> NSCollectionLayoutSection { | ||
| switch section { | ||
| case .vote: return HomeLayout.voteSectionLayout | ||
| default: return HomeLayout.defaultSectionLayout | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
섹션 팩토리랑 layout을 나눈 이유는
View Layout잡는 코드와 내용이 들어가는 코드를 분할하기 위해서 일까요?
There was a problem hiding this comment.
섹션에 채워질 데이터 구성 책임과 그 데이터를 어떻게 레이아웃으로 표현할지 책임을 분리하여 관심사를 구분했습니다!
| struct Vote { | ||
| let leftRestaurant: Restaurant | ||
| let rightRestaurant: Restaurant | ||
| let remainingTime: Int |
There was a problem hiding this comment.
이부분은 따로 Date로 가지지 않고 Int로 가지고 있는걸까요?
There was a problem hiding this comment.
아직 API 명세가 완성되지 않아서 우선은 UI에서 바로 표시할 수 있도록 임의로 Int 타입으로 남은 시간을 설정해두었습니다! 이후에 API 명세가 확정되면 서버에서 전달되는 데이터 형식에 맞춰 Date 혹은 다른 적절한 타입으로 수정할 계획입니다:)
|
|
||
| final class HomeViewModel { | ||
|
|
||
| var universityName: String = "부경대학교" |
There was a problem hiding this comment.
이친구는 따로 private(set)으로 안한 이유가 있을까요?!
There was a problem hiding this comment.
이후에 대학을 변경할 수 있도록 만들 계획이기 때문입니다!
|
|
||
| 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 { |
There was a problem hiding this comment.
어짜피 let이라면
var 로 아래 인스턴스를 뽑아내지 않고 아래와 같이 안하신 이유가 있을까요?!
| 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 { |
|
|
||
| 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 } |
| 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 } |
There was a problem hiding this comment.
현재는 지적해주신 뷰모델이 모델의 데이터를 감싼 형태로 설계되어 있습니다.
향후 Cell의 표현 방식(예: 거리 단위 포맷, 별점 표시, 카테고리 텍스트 포맷 등)이 바뀔 때 뷰모델에서 쉽게 가공할 수 있도록 하고 UI 레이어에서의 변경에 따라 모델을 직접 건드리지 않도록 명확히 역할을 나누기 위해 뷰모델을 분리했습니다!
말씀주신대로 단순히 let 프로퍼티를 직접 넘겨도 되지만 표현 책임을 뷰모델에 위임해두면 추후 UI 변경이 발생해도 모델에는 영향이 없어서 유지보수가 더 편할 것으로 판단했습니다.
나중에 뷰모델이 너무 얇으면 제거하고 바로 모델을 전달하는 방식으로도 변경 가능하니 의견 주시면 조율해 보겠습니다!
🚀 Pull Request
📌 요약
📄 상세 설명
폴더별 설명
Component/UI 컴포넌트 단위로 구성된 View 계층입니다.
Cell/: 각 섹션에 대응되는UICollectionViewCellHeader/,Footer/:UICollectionReusableView형태로 구성되는 Supplementary ViewEnum/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 이벤트 처리 및 화면 상태에 필요한 데이터를 가공설계 전략 요약
주요 코드 설명
HomeSectionLayoutFactoryHomeSection에 해당하는 레이아웃을 생성해주는 팩토리의 인터페이스입니다.SectionLayoutProvidinglayout())을 통해 다양한 레이아웃을 하나의 타입으로 묶기 위해 사용합니다.DefaultSectionLayoutNSCollectionLayoutSection을 반환합니다.VoteSectionLayoutSupplementaryView를 포함하지 않습니다.HomeSupplementaryItemFactorySupplementaryItem레이아웃을 생성합니다.SectionControllerUICollectionViewDataSource구현을 분산시켜ViewController를 단순화하고 섹션별 책임을 분리하기 위해 사용합니다.DefaultSectionControllerHomeSection을 기반으로셀 identifier,타이틀,SupplementaryView여부 등을 참조하여 UI를 구성합니다.VoteSectionControllerSupplementaryView는 사용하지 않습니다.UICollectionViewCell하나만 구성하며 가장 간결한 섹션 컨트롤러의 예시입니다.HomeLayoutFactoryHomeSection에 대해 적절한SectionLayoutProviding구현체를 선택하여NSCollectionLayoutSection을 반환하는 팩토리입니다.UICollectionViewCompositionalLayout의 섹션별 구성을switch문으로 깔끔하게 위임하고 변경에 유연하게 대응할 수 있도록 하기 위함입니다.📸 구현 화면 (GIF / Screenshot)
🙋 리뷰 가이드
🔗 관련 이슈 (Issue)
✅ PR Check List
Summary by CodeRabbit
신규 기능
스타일
기타