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()) } } }