diff --git a/MatzipBook/MatzipBook.xcodeproj/project.pbxproj b/MatzipBook/MatzipBook.xcodeproj/project.pbxproj
index ff264cd..d47e15a 100644
--- a/MatzipBook/MatzipBook.xcodeproj/project.pbxproj
+++ b/MatzipBook/MatzipBook.xcodeproj/project.pbxproj
@@ -122,7 +122,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
- LastUpgradeCheck = 1620;
+ LastUpgradeCheck = 1640;
TargetAttributes = {
05E5F5B62D956A6A00F0CB97 = {
CreatedOnToolsVersion = 16.2;
diff --git a/MatzipBook/MatzipBook/Core/Common/Extension/ReusableIdentifier+.swift b/MatzipBook/MatzipBook/Core/Common/Extension/ReusableIdentifier+.swift
new file mode 100644
index 0000000..0881af0
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/Common/Extension/ReusableIdentifier+.swift
@@ -0,0 +1,19 @@
+//
+// ReusableIdentifier+.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/16/25.
+//
+
+import UIKit
+
+protocol ReusableIdentifier: AnyObject {}
+
+extension ReusableIdentifier where Self: UIView {
+
+ static var identifier: String {
+ return String(describing: self)
+ }
+}
+
+extension UICollectionReusableView: ReusableIdentifier {}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift
index 614967c..5ee0576 100644
--- a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipIcon.swift
@@ -16,4 +16,6 @@ enum MatzipIcon: String {
case icLocationUnselected = "ic_location_unselected"
case icPersonSelected = "ic_person_selected"
case icPersonUnselected = "ic_person_unselected"
+ case icSearch = "ic_search"
+ case icClock = "ic_clock"
}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipImage.swift b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipImage.swift
new file mode 100644
index 0000000..1d1a3e4
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipImage.swift
@@ -0,0 +1,14 @@
+//
+// MatzipImage.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/9/25.
+//
+
+import Foundation
+
+enum MatzipImage: String {
+ case imgLogo = "img_logo"
+ case imgBubble = "img_bubble"
+ case imgVS = "img_vs"
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipText.swift b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipText.swift
new file mode 100644
index 0000000..80412c2
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/MatzipText.swift
@@ -0,0 +1,15 @@
+//
+// MatzipText.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import Foundation
+
+enum MatzipText {
+ enum Vote {
+ static let title: String = "당신의 선택은?"
+ static let subtitle: String = "오늘 당신의 맛집을 투표해주세요!"
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift
index 39d9842..9af5728 100644
--- a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIColor+.swift
@@ -27,4 +27,10 @@ extension UIColor {
static let mainBackgroundColor: UIColor = UIColor(hex: "FAFAFA")
static let shadowColor: UIColor = UIColor(hex: "000000", alpha: 0.13)
+ static let sub1: UIColor = UIColor(hex: "787878")
+ static let sub2: UIColor = UIColor(hex: "A8A8A8")
+ static let separatorColor: UIColor = UIColor(hex: "D9D9D9")
+ static let mainLight2: UIColor = UIColor(hex: "FFB273")
+ static let boxColor: UIColor = UIColor(hex: "F0F0F0")
+ static let mainOrange: UIColor = UIColor(hex: "FF8400")
}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift
new file mode 100644
index 0000000..8825cf5
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Foundation/UIFont+.swift
@@ -0,0 +1,20 @@
+//
+// UIFont+.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+enum FontName: String {
+ case bold = "Pretendard-Bold"
+ case regular = "Pretendard-Regular"
+}
+
+extension UIFont {
+
+ static func applyFont(_ name: FontName, ofSize size: CGFloat) -> UIFont {
+ return UIFont(name: name.rawValue, size: size) ?? .systemFont(ofSize: size)
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/Contents.json
new file mode 100644
index 0000000..7ce4622
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "ic_clock.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/ic_clock.svg b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/ic_clock.svg
new file mode 100644
index 0000000..52bd9b0
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_clock.imageset/ic_clock.svg
@@ -0,0 +1,4 @@
+
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json
new file mode 100644
index 0000000..a2deffb
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "ic_search.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/ic_search.svg b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/ic_search.svg
new file mode 100644
index 0000000..f757166
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Icons/ic_search.imageset/ic_search.svg
@@ -0,0 +1,4 @@
+
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/Contents.json
new file mode 100644
index 0000000..f4803ea
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "img_bubble.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/img_bubble.svg b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/img_bubble.svg
new file mode 100644
index 0000000..a937f5a
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_bubble.imageset/img_bubble.svg
@@ -0,0 +1,17 @@
+
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/Contents.json
new file mode 100644
index 0000000..f8c9616
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "img_dummy0.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "img_dummy0@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "img_dummy0@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0.png
new file mode 100644
index 0000000..9febed3
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@2x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@2x.png
new file mode 100644
index 0000000..2363610
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@2x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@3x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@3x.png
new file mode 100644
index 0000000..7b61402
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy0.imageset/img_dummy0@3x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json
new file mode 100644
index 0000000..06923d1
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "img_dummy1.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "img_dummy1@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "img_dummy1@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1.png
new file mode 100644
index 0000000..540c037
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@2x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@2x.png
new file mode 100644
index 0000000..6de7381
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@2x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@3x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@3x.png
new file mode 100644
index 0000000..cb2a3f2
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_dummy1.imageset/img_dummy1@3x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/Contents.json
new file mode 100644
index 0000000..e82579b
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "img_logo.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "img_logo@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "img_logo@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo.png
new file mode 100644
index 0000000..b00a0a8
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@2x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@2x.png
new file mode 100644
index 0000000..924fb72
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@2x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@3x.png b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@3x.png
new file mode 100644
index 0000000..c389c93
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_logo.imageset/img_logo@3x.png differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/Contents.json b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/Contents.json
new file mode 100644
index 0000000..f6498c7
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "img_vs.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/img_vs.svg b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/img_vs.svg
new file mode 100644
index 0000000..a6f386d
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Assets.xcassets/Images/img_vs.imageset/img_vs.svg
@@ -0,0 +1,19 @@
+
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Bold.otf b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Bold.otf
new file mode 100644
index 0000000..8e5e30a
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Bold.otf differ
diff --git a/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Regular.otf b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Regular.otf
new file mode 100644
index 0000000..08bf4cf
Binary files /dev/null and b/MatzipBook/MatzipBook/Core/DesignSystem/Resources/Pretendard-Regular.otf differ
diff --git a/MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift b/MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift
new file mode 100644
index 0000000..0261e9d
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/UI/Extension/UIStackView+.swift
@@ -0,0 +1,23 @@
+//
+// UIStackView+.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/16/25.
+//
+
+import UIKit
+
+extension UIStackView {
+
+ func configureStackView(
+ axis: NSLayoutConstraint.Axis = .horizontal,
+ alignment: UIStackView.Alignment = .fill,
+ distribution: UIStackView.Distribution = .fillEqually,
+ spacing: CGFloat = 0
+ ) {
+ self.axis = axis
+ self.alignment = alignment
+ self.distribution = distribution
+ self.spacing = spacing
+ }
+}
diff --git a/MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift b/MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift
new file mode 100644
index 0000000..30f42d8
--- /dev/null
+++ b/MatzipBook/MatzipBook/Core/UI/Extension/UIView+.swift
@@ -0,0 +1,15 @@
+//
+// UIView+.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/10/25.
+//
+
+import UIKit
+
+extension UIView {
+
+ func addSubviews(_ views: UIView...) {
+ views.forEach { addSubview($0) }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Info.plist b/MatzipBook/MatzipBook/Info.plist
index 0eb786d..84be701 100644
--- a/MatzipBook/MatzipBook/Info.plist
+++ b/MatzipBook/MatzipBook/Info.plist
@@ -2,6 +2,11 @@
+ UIAppFonts
+
+ Pretendard-Bold.otf
+ Pretendard-Regular.otf
+
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
diff --git a/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift b/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift
new file mode 100644
index 0000000..b2b6907
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionReusableView.swift
@@ -0,0 +1,68 @@
+//
+// BaseCollectionReusableView.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+import SnapKit
+import Then
+
+class BaseCollectionReusableView: UICollectionReusableView {
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ setupStyles()
+ setupLayouts()
+ setupConstraints()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ /// 뷰의 시각적 속성(스타일)을 설정합니다.
+ ///
+ /// 이 메서드는 `backgroundColor`, `font`, `textColor`, `cornerRadius` 등
+ /// 레이아웃이나 계층 구조에 영향을 주지 않는 **시각적인 속성만을 설정**하는 데 사용됩니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// titleLabel.textColor = .labelPrimary
+ /// titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
+ /// layer.cornerRadius = 12
+ /// ```
+ func setupStyles() {
+ backgroundColor = .mainBackgroundColor
+ }
+
+ /// 뷰의 계층 구조를 설정합니다.
+ ///
+ /// 이 메서드는 서브뷰를 상위 뷰에 추가하는 작업을 담당하며,
+ /// `addSubview`, `addArrangedSubview` 등을 통해 **뷰의 구조를 구성**합니다.
+ /// 일반적으로 제약 조건 설정 전에 호출됩니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// addSubview(titleLabel)
+ /// stackView.addArrangedSubview(subtitleLabel)
+ /// ```
+ func setupLayouts() {}
+
+ /// 오토레이아웃 제약 조건을 설정합니다.
+ ///
+ /// 이 메서드는 뷰 간의 위치, 크기, 정렬 관계 등의 **제약 조건을 정의**합니다.
+ /// `setupLayouts()` 이후 호출되어야 하며, 레이아웃의 정확한 동작을 위해 필수입니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// titleLabel.snp.makeConstraints {
+ /// $0.top.equalToSuperview().inset(16)
+ /// $0.leading.trailing.equalToSuperview().inset(20)
+ /// }
+ /// ```
+ func setupConstraints() {}
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift b/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift
new file mode 100644
index 0000000..eee2e7b
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Common/Base/BaseCollectionViewCell.swift
@@ -0,0 +1,68 @@
+//
+// BaseCollectionViewCell.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+import SnapKit
+import Then
+
+class BaseCollectionViewCell: UICollectionViewCell {
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ setupStyles()
+ setupLayouts()
+ setupConstraints()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ /// 뷰의 시각적 속성(스타일)을 설정합니다.
+ ///
+ /// 이 메서드는 `backgroundColor`, `font`, `textColor`, `cornerRadius` 등
+ /// 레이아웃이나 계층 구조에 영향을 주지 않는 **시각적인 속성만을 설정**하는 데 사용됩니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// titleLabel.textColor = .labelPrimary
+ /// titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
+ /// layer.cornerRadius = 12
+ /// ```
+ func setupStyles() {
+ contentView.backgroundColor = .mainBackgroundColor
+ }
+
+ /// 뷰의 계층 구조를 설정합니다.
+ ///
+ /// 이 메서드는 서브뷰를 상위 뷰에 추가하는 작업을 담당하며,
+ /// `addSubview`, `addArrangedSubview` 등을 통해 **뷰의 구조를 구성**합니다.
+ /// 일반적으로 제약 조건 설정 전에 호출됩니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// contentView.addSubview(titleLabel)
+ /// stackView.addArrangedSubview(subtitleLabel)
+ /// ```
+ func setupLayouts() {}
+
+ /// 오토레이아웃 제약 조건을 설정합니다.
+ ///
+ /// 이 메서드는 뷰 간의 위치, 크기, 정렬 관계 등의 **제약 조건을 정의**합니다.
+ /// `setupLayouts()` 이후 호출되어야 하며, 레이아웃의 정확한 동작을 위해 필수입니다.
+ ///
+ /// 예:
+ /// ```swift
+ /// titleLabel.snp.makeConstraints {
+ /// $0.top.equalToSuperview().inset(16)
+ /// $0.leading.trailing.equalToSuperview().inset(20)
+ /// }
+ /// ```
+ func setupConstraints() {}
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift
new file mode 100644
index 0000000..6b15529
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/HiddenGemCollectionViewCell.swift
@@ -0,0 +1,21 @@
+//
+// HiddenGemCollectionViewCell.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/16/25.
+//
+
+import UIKit
+
+final class HiddenGemCollectionViewCell: BaseCollectionViewCell {
+
+ // MARK: - Bindings
+
+ func configure(with viewModel: HiddenGemCellViewModel) {}
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ contentView.backgroundColor = .systemIndigo
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift
new file mode 100644
index 0000000..cf2fb5e
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/NearbyRankingCollectionViewCell.swift
@@ -0,0 +1,21 @@
+//
+// NearbyRankingCollectionViewCell.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+final class NearbyRankingCollectionViewCell: BaseCollectionViewCell {
+
+ // MARK: - Bindings
+
+ func configure(with viewModel: NearbyRankingCellViewModel) {}
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ contentView.backgroundColor = .systemGreen
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift
new file mode 100644
index 0000000..2e287bb
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/PersonalRecommendationCollectionViewCell.swift
@@ -0,0 +1,21 @@
+//
+// PersonalRecommendationCollectionViewCell.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/16/25.
+//
+
+import UIKit
+
+final class PersonalRecommendationCollectionViewCell: BaseCollectionViewCell {
+
+ // MARK: - Bindings
+
+ func configure(with viewModel: PersonalRecommendationCellViewModel) {}
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ contentView.backgroundColor = .systemBlue
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift
new file mode 100644
index 0000000..b279fae
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Cell/VoteCollectionViewCell.swift
@@ -0,0 +1,224 @@
+//
+// VoteCollectionViewCell.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+final class VoteCollectionViewCell: BaseCollectionViewCell {
+
+ // MARK: - Properties
+
+ private let titleLabel: UILabel = UILabel()
+ private let subtitleLabel: UILabel = UILabel()
+
+ private let leftNameImageView: UIImageView = UIImageView()
+ private let leftNameLabel: UILabel = UILabel()
+
+ private let rightNameImageView: UIImageView = UIImageView()
+ private let rightNameLabel: UILabel = UILabel()
+
+ private let leftButton: UIButton = UIButton(type: .system)
+ private let vsImageView: UIImageView = UIImageView()
+ private let rightButton: UIButton = UIButton(type: .system)
+
+ private let timeImageView: UIImageView = UIImageView()
+ private let timeLabel: UILabel = UILabel()
+ private let timeStackView: UIStackView = UIStackView()
+
+ private let bubbleStackView: UIStackView = UIStackView()
+ private let voteButtonStackView: UIStackView = UIStackView()
+
+ // MARK: - Bindings
+
+ func configure(with viewModel: VoteCellViewModel) {
+ leftNameLabel.text = viewModel.leftRestaurantName
+ configureVoteButton(leftButton, image: viewModel.leftRestaurantImage)
+
+ rightNameLabel.text = viewModel.rightRestaurantName
+ configureVoteButton(rightButton, image: viewModel.rightRestaurantImage)
+
+ timeLabel.text = "\(viewModel.remainingTime)시간 남음"
+ }
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ contentView.do {
+ $0.backgroundColor = .boxColor
+ $0.clipsToBounds = true
+ $0.layer.cornerRadius = 20
+ }
+
+ configureLabel(
+ titleLabel,
+ text: MatzipText.Vote.title,
+ textColor: .mainOrange,
+ fontName: .bold,
+ ofSize: 28
+ )
+
+ configureLabel(
+ subtitleLabel,
+ text: MatzipText.Vote.subtitle,
+ textColor: .sub1,
+ fontName: .regular,
+ ofSize: 14
+ )
+
+ configureBubbleImageView(leftNameImageView)
+ configureBubbleImageView(rightNameImageView)
+
+ configureLabel(
+ leftNameLabel,
+ fontName: .bold,
+ ofSize: 14,
+ lines: 2
+ )
+
+ configureLabel(
+ rightNameLabel,
+ fontName: .bold,
+ ofSize: 14,
+ lines: 2
+ )
+
+ [bubbleStackView, voteButtonStackView].forEach { $0.configureStackView() }
+ configureDefaultImageView(vsImageView, image: .imgVs)
+ configureDefaultImageView(timeImageView, image: .icClock)
+
+ configureLabel(
+ timeLabel,
+ textColor: .sub2,
+ fontName: .bold,
+ ofSize: 12
+ )
+
+ timeStackView.configureStackView(distribution: .fill, spacing: 4)
+ }
+
+ override func setupLayouts() {
+ [timeImageView, timeLabel].forEach {
+ timeStackView.addArrangedSubview($0)
+ }
+
+ [leftNameImageView, rightNameImageView].forEach {
+ bubbleStackView.addArrangedSubview($0)
+ }
+
+ [leftButton, rightButton].forEach {
+ voteButtonStackView.addArrangedSubview($0)
+ }
+
+ contentView.addSubviews(
+ titleLabel,
+ subtitleLabel,
+ bubbleStackView,
+ voteButtonStackView,
+ vsImageView,
+ timeStackView
+ )
+
+ leftNameImageView.addSubview(leftNameLabel)
+ rightNameImageView.addSubview(rightNameLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.top.equalToSuperview().offset(22)
+ $0.horizontalEdges.equalToSuperview().inset(16)
+ }
+
+ subtitleLabel.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.top.equalTo(titleLabel.snp.bottom).offset(4)
+ $0.horizontalEdges.equalToSuperview().inset(16)
+ }
+
+ leftNameLabel.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.centerY.equalTo(leftNameImageView).offset(-5)
+ $0.horizontalEdges.equalToSuperview().inset(30)
+ }
+
+ rightNameLabel.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.centerY.equalTo(rightNameImageView).offset(-5)
+ $0.horizontalEdges.equalToSuperview().inset(30)
+ }
+
+ bubbleStackView.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.top.equalTo(subtitleLabel.snp.bottom).offset(25)
+ $0.horizontalEdges.equalToSuperview().inset(16)
+ $0.height.lessThanOrEqualTo(50)
+ }
+
+ voteButtonStackView.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.top.equalTo(bubbleStackView.snp.bottom)
+ }
+
+ vsImageView.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.centerY.equalTo(voteButtonStackView)
+ $0.size.equalTo(100)
+ }
+
+ timeStackView.snp.makeConstraints {
+ $0.top.equalTo(voteButtonStackView.snp.bottom).offset(10)
+ $0.trailing.equalToSuperview().inset(25)
+ $0.bottom.equalToSuperview().inset(18)
+ }
+ }
+}
+
+// MARK: - UI Helpers
+
+private extension VoteCollectionViewCell {
+
+ func configureLabel(
+ _ label: UILabel,
+ text: String? = nil,
+ textColor: UIColor = .black,
+ alignment: NSTextAlignment = .center,
+ fontName: FontName,
+ ofSize: CGFloat,
+ lines: Int = 1
+ ) {
+ label.text = text
+ label.textColor = textColor
+ label.textAlignment = alignment
+ label.font = .applyFont(fontName, ofSize: ofSize)
+ label.numberOfLines = lines
+ }
+
+ func configureDefaultImageView(_ imageView: UIImageView, image: UIImage) {
+ imageView.image = image
+ imageView.contentMode = .scaleAspectFit
+ }
+
+ func configureBubbleImageView(_ imageView: UIImageView) {
+ imageView.do {
+ $0.image = .imgBubble.withRenderingMode(.alwaysOriginal)
+ $0.contentMode = .scaleAspectFill
+ $0.layer.shadowRadius = 10
+ $0.layer.shadowOpacity = 1
+ $0.layer.shadowOffset = .zero
+ $0.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.13).cgColor
+ }
+ }
+
+ func configureVoteButton(_ button: UIButton, image: UIImage?) {
+ button.do {
+ $0.backgroundColor = .clear
+ $0.clipsToBounds = true
+ $0.layer.cornerRadius = 20
+ $0.contentMode = .scaleAspectFit
+ $0.setImage(image?.withRenderingMode(.alwaysOriginal), for: .normal)
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift
new file mode 100644
index 0000000..7b15352
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Footer/HomeSectionFooterView.swift
@@ -0,0 +1,90 @@
+//
+// HomeSectionFooterView.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+final class HomeSectionFooterView: BaseCollectionReusableView {
+
+ // MARK: - Properties
+
+ private let seeAllButton: UIButton = UIButton(type: .system)
+
+ 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?()
+ }
+
+ // MARK: - Helpers
+
+ func configure(
+ title: String?,
+ onTap: (() -> Void)? = nil
+ ) {
+ seeAllButton.setAttributedTitle(
+ NSAttributedString(
+ string: title ?? "전체보기",
+ attributes: [
+ .foregroundColor: UIColor.mainLight2,
+ .font: UIFont.applyFont(.bold, ofSize: 12)
+ ]
+ ),
+ for: .normal
+ )
+
+ self.onTap = onTap
+ }
+
+ private func setAddTargets() {
+ seeAllButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
+ }
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ var config: UIButton.Configuration = UIButton.Configuration.plain()
+ config.baseForegroundColor = .mainLight2
+ config.image = UIImage(systemName: "chevron.right")?.withConfiguration(
+ UIImage.SymbolConfiguration(pointSize: 10, weight: .bold)
+ )
+ config.imagePadding = 4
+ config.imagePlacement = .trailing
+ config.cornerStyle = .capsule
+
+ seeAllButton.configuration = config
+ seeAllButton.clipsToBounds = true
+ seeAllButton.layer.cornerRadius = 12
+ seeAllButton.layer.borderColor = UIColor.mainLight2.cgColor
+ seeAllButton.layer.borderWidth = 2
+ seeAllButton.tintColor = .mainLight2
+ }
+
+ override func setupLayouts() {
+ addSubview(seeAllButton)
+ }
+
+ override func setupConstraints() {
+ seeAllButton.snp.makeConstraints {
+ $0.centerY.horizontalEdges.equalToSuperview()
+ $0.height.equalTo(50)
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift
new file mode 100644
index 0000000..db15ae7
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/Header/HomeSectionHeaderView.swift
@@ -0,0 +1,41 @@
+//
+// HomeSectionHeaderView.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import UIKit
+
+final class HomeSectionHeaderView: BaseCollectionReusableView {
+
+ // MARK: - Properties
+
+ private let titleLabel: UILabel = UILabel()
+
+ // MARK: - Helpers
+
+ func configure(title: String?) {
+ titleLabel.text = title
+ }
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ titleLabel.do {
+ $0.textAlignment = .left
+ $0.font = .applyFont(.bold, ofSize: 20)
+ }
+ }
+
+ override func setupLayouts() {
+ addSubview(titleLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints {
+ $0.top.equalToSuperview().offset(40)
+ $0.horizontalEdges.bottom.equalToSuperview()
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift b/MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift
new file mode 100644
index 0000000..ade2461
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Component/HomeNavigationBarView.swift
@@ -0,0 +1,98 @@
+//
+// HomeNavigationBarView.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/9/25.
+//
+
+import UIKit
+
+import SnapKit
+import Then
+
+final class HomeNavigationBarView: UIView {
+
+ // MARK: - Properties
+
+ private let logoImageView: UIImageView = UIImageView()
+ private let universityNameLabel: UILabel = UILabel()
+ private let leftStackView: UIStackView = UIStackView()
+ private let searchButton: UIButton = UIButton()
+ private let separatorView: UIView = UIView()
+
+ // MARK: - Initializer
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ setupStyles()
+ setupLayouts()
+ setupConstraints()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Helpers
+
+ func setTitle(_ universityName: String?) {
+ universityNameLabel.text = universityName
+ }
+
+ func setRightBarButtonAction(_ target: Any?, action: Selector) {
+ searchButton.addTarget(target, action: action, for: .touchUpInside)
+ }
+
+ // MARK: - Setup View
+
+ 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
+ }
+
+ private func setupLayouts() {
+ [logoImageView, universityNameLabel].forEach {
+ leftStackView.addArrangedSubview($0)
+ }
+
+ addSubviews(leftStackView, searchButton, separatorView)
+ }
+
+ private func setupConstraints() {
+ leftStackView.snp.makeConstraints {
+ $0.centerY.equalToSuperview()
+ $0.leading.equalToSuperview().inset(16)
+ }
+
+ searchButton.snp.makeConstraints {
+ $0.centerY.equalToSuperview()
+ $0.trailing.equalToSuperview().inset(16)
+ $0.size.equalTo(40)
+ }
+
+ separatorView.snp.makeConstraints {
+ $0.top.equalTo(searchButton.snp.bottom).offset(8)
+ $0.horizontalEdges.bottom.equalToSuperview()
+ $0.height.equalTo(1)
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift
new file mode 100644
index 0000000..8162a63
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/HiddenGemSectionController.swift
@@ -0,0 +1,83 @@
+//
+// HiddenGemSectionController.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import UIKit
+
+final class HiddenGemSectionController: SectionDisplayable, HeaderFooterDisplayable {
+
+ let headerTitle: String?
+ let footerTitle: String?
+ private let items: [RecommendedRestaurant]
+
+ init(
+ headerTitle: String?,
+ footerTitle: String,
+ items: [RecommendedRestaurant]
+ ) {
+ self.headerTitle = headerTitle
+ self.footerTitle = footerTitle
+ self.items = items
+ }
+
+ func numberOfItems() -> Int {
+ return items.count
+ }
+
+ func cellForItem(
+ at indexPath: IndexPath,
+ in collectionView: UICollectionView
+ ) -> UICollectionViewCell {
+ guard let cell = collectionView.dequeueReusableCell(
+ withReuseIdentifier: HiddenGemCollectionViewCell.identifier,
+ for: indexPath
+ ) as? HiddenGemCollectionViewCell else {
+ return UICollectionViewCell()
+ }
+
+ let cellData: RecommendedRestaurant = items[indexPath.item]
+ let viewModel: HiddenGemCellViewModel = HiddenGemCellViewModel(
+ hiddenGem: cellData
+ )
+ cell.configure(with: viewModel)
+
+ return cell
+ }
+
+ func header(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let header = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionHeader,
+ withReuseIdentifier: HomeSectionHeaderView.identifier,
+ for: indexPath
+ ) as? HomeSectionHeaderView else {
+ return nil
+ }
+
+ header.configure(title: headerTitle)
+
+ return header
+ }
+
+ func footer(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let footer = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionFooter,
+ withReuseIdentifier: HomeSectionFooterView.identifier,
+ for: indexPath
+ ) as? HomeSectionFooterView else {
+ return nil
+ }
+
+ footer.configure(title: footerTitle)
+
+ return footer
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift
new file mode 100644
index 0000000..b2f251c
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/NearbyRankingSectionController.swift
@@ -0,0 +1,83 @@
+//
+// NearbyRankingSectionController.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import UIKit
+
+final class NearbyRankingSectionController: SectionDisplayable, HeaderFooterDisplayable {
+
+ let headerTitle: String?
+ let footerTitle: String?
+ private let items: [NearbyRanking]
+
+ init(
+ headerTitle: String?,
+ footerTitle: String,
+ items: [NearbyRanking]
+ ) {
+ self.headerTitle = headerTitle
+ self.footerTitle = footerTitle
+ self.items = items
+ }
+
+ func numberOfItems() -> Int {
+ return items.count
+ }
+
+ func cellForItem(
+ at indexPath: IndexPath,
+ in collectionView: UICollectionView
+ ) -> UICollectionViewCell {
+ guard let cell = collectionView.dequeueReusableCell(
+ withReuseIdentifier: NearbyRankingCollectionViewCell.identifier,
+ for: indexPath
+ ) as? NearbyRankingCollectionViewCell else {
+ return UICollectionViewCell()
+ }
+
+ let cellData: NearbyRanking = items[indexPath.item]
+ let viewModel: NearbyRankingCellViewModel = NearbyRankingCellViewModel(
+ nearbyRanking: cellData
+ )
+ cell.configure(with: viewModel)
+
+ return cell
+ }
+
+ func header(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let header = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionHeader,
+ withReuseIdentifier: HomeSectionHeaderView.identifier,
+ for: indexPath
+ ) as? HomeSectionHeaderView else {
+ return nil
+ }
+
+ header.configure(title: headerTitle)
+
+ return header
+ }
+
+ func footer(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let footer = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionFooter,
+ withReuseIdentifier: HomeSectionFooterView.identifier,
+ for: indexPath
+ ) as? HomeSectionFooterView else {
+ return nil
+ }
+
+ footer.configure(title: footerTitle)
+
+ return footer
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift
new file mode 100644
index 0000000..0569c63
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/RecommendationSectionController.swift
@@ -0,0 +1,83 @@
+//
+// RecommendationSectionController.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import UIKit
+
+final class RecommendationSectionController: SectionDisplayable, HeaderFooterDisplayable {
+
+ let headerTitle: String?
+ let footerTitle: String?
+ private let items: [RecommendedRestaurant]
+
+ init(
+ headerTitle: String?,
+ footerTitle: String,
+ items: [RecommendedRestaurant]
+ ) {
+ self.headerTitle = headerTitle
+ self.footerTitle = footerTitle
+ self.items = items
+ }
+
+ func numberOfItems() -> Int {
+ return items.count
+ }
+
+ func cellForItem(
+ at indexPath: IndexPath,
+ in collectionView: UICollectionView
+ ) -> UICollectionViewCell {
+ guard let cell = collectionView.dequeueReusableCell(
+ withReuseIdentifier: PersonalRecommendationCollectionViewCell.identifier,
+ for: indexPath
+ ) as? PersonalRecommendationCollectionViewCell else {
+ return UICollectionViewCell()
+ }
+
+ let cellData: RecommendedRestaurant = items[indexPath.item]
+ let viewModel: PersonalRecommendationCellViewModel = PersonalRecommendationCellViewModel(
+ personalRecommendation: cellData
+ )
+ cell.configure(with: viewModel)
+
+ return cell
+ }
+
+ func header(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let header = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionHeader,
+ withReuseIdentifier: HomeSectionHeaderView.identifier,
+ for: indexPath
+ ) as? HomeSectionHeaderView else {
+ return nil
+ }
+
+ header.configure(title: headerTitle)
+
+ return header
+ }
+
+ func footer(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ guard let footer = collectionView.dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionFooter,
+ withReuseIdentifier: HomeSectionFooterView.identifier,
+ for: indexPath
+ ) as? HomeSectionFooterView else {
+ return nil
+ }
+
+ footer.configure(title: footerTitle)
+
+ return footer
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift
new file mode 100644
index 0000000..6e2e6e3
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Controller/VoteSectionController.swift
@@ -0,0 +1,53 @@
+//
+// VoteSectionController.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+final class VoteSectionController: SectionDisplayable {
+
+ private let items: [Vote]
+
+ init(items: [Vote]) {
+ self.items = items
+ }
+
+ func numberOfItems() -> Int {
+ return items.count
+ }
+
+ func cellForItem(
+ at indexPath: IndexPath,
+ in collectionView: UICollectionView
+ ) -> UICollectionViewCell {
+ guard let cell = collectionView.dequeueReusableCell(
+ withReuseIdentifier: VoteCollectionViewCell.identifier,
+ for: indexPath
+ ) as? VoteCollectionViewCell else {
+ return UICollectionViewCell()
+ }
+
+ let cellData: Vote = items[indexPath.item]
+ let viewModel: VoteCellViewModel = VoteCellViewModel(vote: cellData)
+ cell.configure(with: viewModel)
+
+ return cell
+ }
+
+ func header(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ return nil
+ }
+
+ func footer(
+ in collectionView: UICollectionView,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView? {
+ return nil
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Enum/HomeSectionType.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Enum/HomeSectionType.swift
new file mode 100644
index 0000000..7e436a9
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Enum/HomeSectionType.swift
@@ -0,0 +1,22 @@
+//
+// HomeSectionType.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import UIKit
+
+enum HomeSectionType: Int {
+ case vote, nearbyRanking, personalRecommendation, hiddenGem
+}
+
+extension HomeSectionType {
+
+ func sectionLayout() -> NSCollectionLayoutSection {
+ switch self {
+ case .vote: return HomeLayout.voteSectionLayout
+ default: return HomeLayout.defaultSectionLayout
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift
new file mode 100644
index 0000000..2e47728
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Factory/HomeSectionFactory.swift
@@ -0,0 +1,73 @@
+//
+// HomeSectionFactory.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import UIKit
+
+struct HomeSectionFactory {
+
+ static func makeSections(
+ for universityName: String,
+ onSeeAll: @escaping (HomeSectionType) -> Void
+ ) -> [SectionDisplayable] {
+ return [
+ makeVoteSection(),
+ makeNearbyRankingSection(onSeeAll),
+ makePersonalRecommendationSection(userName: "범수", onSeeAll),
+ makeHiddenGemSection(generation: "19", onSeeAll)
+ ]
+ }
+
+ private static func makeVoteSection() -> SectionDisplayable {
+ let items: [Vote] = [
+ Vote(
+ leftRestaurant: Restaurant(name: "그집짬뽕", thumbnail: .imgDummy0),
+ rightRestaurant: Restaurant(name: "오일리", thumbnail: .imgDummy1),
+ remainingTime: 16
+ )
+ ]
+
+ return VoteSectionController(items: items)
+ }
+
+ 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
+ )
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/DefaultSectionLayout.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/DefaultSectionLayout.swift
new file mode 100644
index 0000000..63f040c
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/DefaultSectionLayout.swift
@@ -0,0 +1,38 @@
+//
+// DefaultSectionLayout.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+final class DefaultSectionLayout: SectionLayoutProviding {
+
+ func layout() -> NSCollectionLayoutSection {
+ let itemSize: NSCollectionLayoutSize = NSCollectionLayoutSize(
+ widthDimension: .fractionalWidth(1.0),
+ heightDimension: .fractionalWidth(1.0)
+ )
+ let item: NSCollectionLayoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
+
+ let group: NSCollectionLayoutGroup = NSCollectionLayoutGroup.horizontal(
+ layoutSize: itemSize,
+ subitems: [item]
+ )
+
+ let section: NSCollectionLayoutSection = NSCollectionLayoutSection(group: group)
+ section.contentInsets = NSDirectionalEdgeInsets(
+ top: 10,
+ leading: 20,
+ bottom: 20,
+ trailing: 20
+ )
+ section.boundarySupplementaryItems = [
+ HomeHeaderLayout.create(),
+ HomeFooterLayout.create()
+ ]
+
+ return section
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/HomeSectionLayoutProvider.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/HomeSectionLayoutProvider.swift
new file mode 100644
index 0000000..c8a361b
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/HomeSectionLayoutProvider.swift
@@ -0,0 +1,23 @@
+//
+// HomeSectionLayoutProvider.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+enum HomeLayout {
+ static let voteSectionLayout: NSCollectionLayoutSection = VoteSectionLayout().layout()
+ static let defaultSectionLayout: NSCollectionLayoutSection = DefaultSectionLayout().layout()
+}
+
+final class HomeSectionLayoutProvider: HomeSectionLayoutProviding {
+
+ func layout(for section: HomeSectionType) -> NSCollectionLayoutSection {
+ switch section {
+ case .vote: return HomeLayout.voteSectionLayout
+ default: return HomeLayout.defaultSectionLayout
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/Supplementary/HomeSupplementaryItemLayout.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/Supplementary/HomeSupplementaryItemLayout.swift
new file mode 100644
index 0000000..958074f
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/Supplementary/HomeSupplementaryItemLayout.swift
@@ -0,0 +1,34 @@
+//
+// HomeSupplementaryItemLayout.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+enum HomeHeaderLayout {
+ static func create() -> NSCollectionLayoutBoundarySupplementaryItem {
+ NSCollectionLayoutBoundarySupplementaryItem(
+ layoutSize: NSCollectionLayoutSize(
+ widthDimension: .fractionalWidth(1.0),
+ heightDimension: .estimated(50)
+ ),
+ elementKind: UICollectionView.elementKindSectionHeader,
+ alignment: .top
+ )
+ }
+}
+
+enum HomeFooterLayout {
+ static func create() -> NSCollectionLayoutBoundarySupplementaryItem {
+ NSCollectionLayoutBoundarySupplementaryItem(
+ layoutSize: NSCollectionLayoutSize(
+ widthDimension: .fractionalWidth(1.0),
+ heightDimension: .absolute(50)
+ ),
+ elementKind: UICollectionView.elementKindSectionFooter,
+ alignment: .bottom
+ )
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/VoteSectionLayout.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/VoteSectionLayout.swift
new file mode 100644
index 0000000..1d831c9
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Layout/VoteSectionLayout.swift
@@ -0,0 +1,34 @@
+//
+// VoteSectionLayout.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+final class VoteSectionLayout: SectionLayoutProviding {
+
+ func layout() -> NSCollectionLayoutSection {
+ let itemSize: NSCollectionLayoutSize = NSCollectionLayoutSize(
+ widthDimension: .fractionalWidth(1.0),
+ heightDimension: .estimated(200)
+ )
+ let item: NSCollectionLayoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
+
+ let group: NSCollectionLayoutGroup = NSCollectionLayoutGroup.horizontal(
+ layoutSize: itemSize,
+ subitems: [item]
+ )
+
+ let section: NSCollectionLayoutSection = NSCollectionLayoutSection(group: group)
+ section.contentInsets = NSDirectionalEdgeInsets(
+ top: 25,
+ leading: 20,
+ bottom: 0,
+ trailing: 20
+ )
+
+ return section
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HeaderFooterDisplayable.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HeaderFooterDisplayable.swift
new file mode 100644
index 0000000..817d545
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HeaderFooterDisplayable.swift
@@ -0,0 +1,13 @@
+//
+// HeaderFooterDisplayable.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/18/25.
+//
+
+import Foundation
+
+protocol HeaderFooterDisplayable {
+ var headerTitle: String? { get }
+ var footerTitle: String? { get }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HomeSectionLayoutProviding.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HomeSectionLayoutProviding.swift
new file mode 100644
index 0000000..939b6a0
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/HomeSectionLayoutProviding.swift
@@ -0,0 +1,12 @@
+//
+// HomeSectionLayoutProviding.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+protocol HomeSectionLayoutProviding {
+ func layout(for section: HomeSectionType) -> NSCollectionLayoutSection
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionDisplayable.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionDisplayable.swift
new file mode 100644
index 0000000..d6a6548
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionDisplayable.swift
@@ -0,0 +1,27 @@
+//
+// SectionDisplayable.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+protocol SectionDisplayable {
+ 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?
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionLayoutProviding.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionLayoutProviding.swift
new file mode 100644
index 0000000..bfc3ba1
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/HomeSection/Protocol/SectionLayoutProviding.swift
@@ -0,0 +1,12 @@
+//
+// SectionLayoutProviding.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/17/25.
+//
+
+import UIKit
+
+protocol SectionLayoutProviding {
+ func layout() -> NSCollectionLayoutSection
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/HomeViewController.swift b/MatzipBook/MatzipBook/Presentation/Home/HomeViewController.swift
deleted file mode 100644
index 2c128b8..0000000
--- a/MatzipBook/MatzipBook/Presentation/Home/HomeViewController.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-//
-// HomeViewController.swift
-// MatzipBook
-//
-// Created by RAFA on 5/30/25.
-//
-
-import UIKit
-
-final class HomeViewController: BaseViewController {}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Model/NearbyRanking.swift b/MatzipBook/MatzipBook/Presentation/Home/Model/NearbyRanking.swift
new file mode 100644
index 0000000..3688e13
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Model/NearbyRanking.swift
@@ -0,0 +1,22 @@
+//
+// NearbyRanking.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+struct NearbyRanking {
+ var ranking: Int
+ let name: String
+ let image: UIImage?
+ var rating: Double
+ var distance: Double
+ let foodCategory: String
+ let themeCategory: String
+
+ var starCount: Int {
+ return Int(round(rating))
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Model/RecommendedRestaurant.swift b/MatzipBook/MatzipBook/Presentation/Home/Model/RecommendedRestaurant.swift
new file mode 100644
index 0000000..691fbf6
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Model/RecommendedRestaurant.swift
@@ -0,0 +1,22 @@
+//
+// Recommendation.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+enum RecommendationSectionType {
+ case personalRecommendation, hiddenGem
+}
+
+struct RecommendedRestaurant {
+ let sectionType: RecommendationSectionType
+ let image: UIImage?
+ var rating: Double
+ let name: String
+ var description: String
+ let foodCategory: String
+ let themeCategory: String
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Model/Restaurant.swift b/MatzipBook/MatzipBook/Presentation/Home/Model/Restaurant.swift
new file mode 100644
index 0000000..76c3842
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Model/Restaurant.swift
@@ -0,0 +1,13 @@
+//
+// Restaurant.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+struct Restaurant {
+ let name: String
+ let thumbnail: UIImage?
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/Model/Vote.swift b/MatzipBook/MatzipBook/Presentation/Home/Model/Vote.swift
new file mode 100644
index 0000000..30b5fb0
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/Model/Vote.swift
@@ -0,0 +1,14 @@
+//
+// Vote.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+struct Vote {
+ let leftRestaurant: Restaurant
+ let rightRestaurant: Restaurant
+ let remainingTime: Int
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift b/MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift
new file mode 100644
index 0000000..d757404
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/View/HomeViewController.swift
@@ -0,0 +1,180 @@
+//
+// HomeViewController.swift
+// MatzipBook
+//
+// Created by RAFA on 5/30/25.
+//
+
+import UIKit
+
+final class HomeViewController: BaseViewController {
+
+ // MARK: - Properties
+
+ private let navigationBarView: HomeNavigationBarView = HomeNavigationBarView()
+
+ private lazy var collectionView: UICollectionView = {
+ let layout: UICollectionViewCompositionalLayout =
+ UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
+ guard let sectionType = HomeSectionType(rawValue: sectionIndex) else {
+ return nil
+ }
+ return sectionType.sectionLayout()
+ }
+
+ return UICollectionView(frame: .zero, collectionViewLayout: layout)
+ }()
+
+ private let viewModel: HomeViewModel
+
+ // MARK: - Initializer
+
+ init(viewModel: HomeViewModel) {
+ self.viewModel = viewModel
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Lifecycle
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setDelegates()
+ registerCells()
+ bindViewModel()
+ }
+
+ // MARK: - Actions
+
+ @objc private func didTapSearch() {
+ viewModel.didTapSearchButton()
+ }
+
+ // MARK: - Bindings
+
+ private func bindViewModel() {
+ navigationBarView.setTitle(viewModel.universityName)
+ navigationBarView.setRightBarButtonAction(self, action: #selector(didTapSearch))
+ }
+
+ // MARK: - Helpers
+
+ private func setDelegates() {
+ collectionView.dataSource = self
+ }
+
+ private func registerCells() {
+ let cellTypes: [UICollectionViewCell.Type] = [
+ VoteCollectionViewCell.self,
+ NearbyRankingCollectionViewCell.self,
+ PersonalRecommendationCollectionViewCell.self,
+ HiddenGemCollectionViewCell.self
+ ]
+
+ cellTypes.forEach {
+ collectionView.register($0, forCellWithReuseIdentifier: $0.identifier)
+ }
+
+ let headerFooterTypes: [(viewType: UICollectionReusableView.Type, kind: String)] = [
+ (HomeSectionHeaderView.self, UICollectionView.elementKindSectionHeader),
+ (HomeSectionFooterView.self, UICollectionView.elementKindSectionFooter)
+ ]
+
+ headerFooterTypes.forEach {
+ collectionView.register(
+ $0.viewType,
+ forSupplementaryViewOfKind: $0.kind,
+ withReuseIdentifier: $0.viewType.identifier
+ )
+ }
+ }
+
+ // MARK: - Setup View
+
+ override func setupStyles() {
+ super.setupStyles()
+
+ navigationController?.setNavigationBarHidden(true, animated: false)
+ collectionView.backgroundColor = .clear
+ collectionView.contentInset.bottom = 60
+ }
+
+ override func setupLayouts() {
+ view.addSubviews(navigationBarView, collectionView)
+ }
+
+ override func setupConstraints() {
+ navigationBarView.snp.makeConstraints {
+ $0.top.equalTo(view.safeAreaLayoutGuide).offset(8)
+ $0.horizontalEdges.equalToSuperview()
+ }
+
+ collectionView.snp.makeConstraints {
+ $0.top.equalTo(navigationBarView.snp.bottom)
+ $0.horizontalEdges.bottom.equalToSuperview()
+ }
+ }
+}
+
+// MARK: - UICollectionViewDataSource
+
+extension HomeViewController: UICollectionViewDataSource {
+
+ func numberOfSections(in collectionView: UICollectionView) -> Int {
+ return viewModel.sections.count
+ }
+
+ func collectionView(
+ _ collectionView: UICollectionView,
+ numberOfItemsInSection section: Int
+ ) -> Int {
+ return viewModel.sections[section].numberOfItems()
+ }
+
+ func collectionView(
+ _ collectionView: UICollectionView,
+ cellForItemAt indexPath: IndexPath
+ ) -> UICollectionViewCell {
+ return viewModel.sections[indexPath.section]
+ .cellForItem(at: indexPath, in: collectionView)
+ }
+
+ func collectionView(
+ _ collectionView: UICollectionView,
+ viewForSupplementaryElementOfKind kind: String,
+ at indexPath: IndexPath
+ ) -> UICollectionReusableView {
+ let controller: SectionDisplayable = viewModel.sections[indexPath.section]
+
+ switch kind {
+ case UICollectionView.elementKindSectionHeader:
+ return controller.header(
+ in: collectionView,
+ at: indexPath
+ ) ?? UICollectionReusableView()
+
+ case UICollectionView.elementKindSectionFooter:
+ let footerView: UICollectionReusableView? = controller.footer(
+ in: collectionView,
+ at: indexPath
+ )
+
+ if let footerView = footerView as? HomeSectionFooterView,
+ let displayable = controller as? HeaderFooterDisplayable {
+ footerView.configure(title: displayable.footerTitle) { [weak self] in
+ if let type = HomeSectionType(rawValue: indexPath.section) {
+ self?.viewModel.didTapSeeAllButton(for: type)
+ }
+ }
+ }
+ return footerView ?? UICollectionReusableView()
+
+ default:
+ return UICollectionReusableView()
+ }
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HiddenGemCellViewModel.swift b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HiddenGemCellViewModel.swift
new file mode 100644
index 0000000..f20d98e
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HiddenGemCellViewModel.swift
@@ -0,0 +1,24 @@
+//
+// HiddenGemCellViewModel.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+final class HiddenGemCellViewModel {
+
+ private let hiddenGem: RecommendedRestaurant
+
+ init(hiddenGem: RecommendedRestaurant) {
+ self.hiddenGem = hiddenGem
+ }
+
+ var restaurantImage: UIImage? { hiddenGem.image }
+ var restaurantRating: Double { hiddenGem.rating }
+ var restaurantName: String { hiddenGem.name }
+ var restaurantDescription: String { hiddenGem.description }
+ var foodCategory: String { hiddenGem.foodCategory }
+ var themeCategory: String { hiddenGem.themeCategory }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift
new file mode 100644
index 0000000..80ef9c9
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/HomeViewModel.swift
@@ -0,0 +1,30 @@
+//
+// HomeViewModel.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/15/25.
+//
+
+import Foundation
+
+final class HomeViewModel {
+
+ var universityName: String = "부경대학교"
+ private(set) var sections: [SectionDisplayable] = []
+
+ init() {
+ self.sections = HomeSectionFactory.makeSections(
+ for: universityName
+ ) { [weak self] section in
+ self?.didTapSeeAllButton(for: section)
+ }
+ }
+
+ func didTapSearchButton() {
+ print("DEBUG: Search button tapped")
+ }
+
+ func didTapSeeAllButton(for section: HomeSectionType) {
+ print("DEBUG: See all button tapped for section >>> \(section)")
+ }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/ViewModel/NearbyRankingCellViewModel.swift b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/NearbyRankingCellViewModel.swift
new file mode 100644
index 0000000..bd522cb
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/NearbyRankingCellViewModel.swift
@@ -0,0 +1,34 @@
+//
+// NearbyRankingCellViewModel.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+final class NearbyRankingCellViewModel {
+
+ 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 {
+ if nearbyRanking.distance < 1.0 {
+ let meters: Int = Int(nearbyRanking.distance * 1000)
+ return "\(meters)m"
+ } else {
+ return String(format: "%.1fkm", nearbyRanking.distance)
+ }
+ }
+
+ var foodCategory: String { nearbyRanking.foodCategory }
+ var themeCategory: String { nearbyRanking.themeCategory }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/ViewModel/PersonalRecommendationCellViewModel.swift b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/PersonalRecommendationCellViewModel.swift
new file mode 100644
index 0000000..c37a05c
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/PersonalRecommendationCellViewModel.swift
@@ -0,0 +1,24 @@
+//
+// PersonalRecommendationCellViewModel.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+final class PersonalRecommendationCellViewModel {
+
+ 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 }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Home/ViewModel/VoteCellViewModel.swift b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/VoteCellViewModel.swift
new file mode 100644
index 0000000..c6ee144
--- /dev/null
+++ b/MatzipBook/MatzipBook/Presentation/Home/ViewModel/VoteCellViewModel.swift
@@ -0,0 +1,27 @@
+//
+// VoteCellViewModel.swift
+// MatzipBook
+//
+// Created by 심범수 on 6/29/25.
+//
+
+import UIKit
+
+final class VoteCellViewModel {
+
+ private let vote: Vote
+
+ init(vote: Vote) {
+ self.vote = vote
+ }
+
+ 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 }
+}
diff --git a/MatzipBook/MatzipBook/Presentation/Main/MainTab.swift b/MatzipBook/MatzipBook/Presentation/Main/MainTab.swift
index 7ce6e6a..9060a5c 100644
--- a/MatzipBook/MatzipBook/Presentation/Main/MainTab.swift
+++ b/MatzipBook/MatzipBook/Presentation/Main/MainTab.swift
@@ -30,10 +30,16 @@ enum MainTab: Int, CaseIterable {
var viewController: UIViewController {
switch self {
- case .home: return UINavigationController(rootViewController: HomeViewController())
- case .location: return UINavigationController(rootViewController: MapViewController())
- case .bookmark: return UINavigationController(rootViewController: BookmarkViewController())
- case .profile: return UINavigationController(rootViewController: ProfileViewController())
+ case .home:
+ let viewModel: HomeViewModel = HomeViewModel()
+ let homeVC: HomeViewController = HomeViewController(viewModel: viewModel)
+ return UINavigationController(rootViewController: homeVC)
+ case .location:
+ return UINavigationController(rootViewController: MapViewController())
+ case .bookmark:
+ return UINavigationController(rootViewController: BookmarkViewController())
+ case .profile:
+ return UINavigationController(rootViewController: ProfileViewController())
}
}
}