From 6a367ac555e6afdecb0f650e2d18f144695a9f37 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 00:11:16 +0900 Subject: [PATCH 01/16] =?UTF-8?q?[Feat]=20FSCalendar=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/TCalendarCell.swift | 174 ++++++++++++++++++ .../Components/Calendar/TCalendarView.swift | 123 +++++++++++++ TnT/Tuist/Package.resolved | 9 + TnT/Tuist/Package.swift | 3 +- .../Configuration+Templates.swift | 4 + .../Dependency/DependencyInformation.swift | 3 +- .../Dependency/ExternalDependency.swift | 3 +- 7 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift new file mode 100644 index 00000000..b5b5996a --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift @@ -0,0 +1,174 @@ +// +// CustumCalendarCell.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/1/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import FSCalendar + +class FSCustomCalendarCell: FSCalendarCell { + + static let identifier: String = "CustomCalendarCell" + static let cellSize: CGSize = CGSize(width: 51, height: 54) + + var customDate: Date? + var isCustomSelected: Bool = false + var style: Style = .default + var eventCount: Int = 0 + var isWeekMode: Bool = false + + private let dayLabel = UILabel() + private let eventStackView = UIStackView() + private let eventIcon = UIImageView() + private let eventCountLabel = UILabel() + private let backgroundContainer = UIView() + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + contentView.addSubview(backgroundContainer) + backgroundContainer.layer.cornerRadius = 8 + + // ๐Ÿ“Œ ๋‚ ์งœ ๋ผ๋ฒจ ์„ค์ • + dayLabel.font = Typography.FontStyle.body2Medium.uiFont + dayLabel.textAlignment = .center + + // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์Šคํƒ๋ทฐ ์„ค์ • + eventStackView.axis = .horizontal + eventStackView.spacing = 2 + eventStackView.alignment = .center + + // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์•„์ด์ฝ˜ ์„ค์ • + eventIcon.image = UIImage(resource: .icnStar).withRenderingMode(.alwaysTemplate) + eventIcon.tintColor = UIColor(.red300) + eventIcon.contentMode = .scaleAspectFit + eventIcon.frame = CGRect(x: 0, y: 0, width: 12, height: 12) + + // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์นด์šดํŠธ ๋ผ๋ฒจ ์„ค์ • + eventCountLabel.font = Typography.FontStyle.label2Medium.uiFont + eventCountLabel.textColor = UIColor(.neutral400) + + eventStackView.addArrangedSubview(eventIcon) + eventStackView.addArrangedSubview(eventCountLabel) + + contentView.addSubview(dayLabel) + contentView.addSubview(eventStackView) + + // ๐Ÿ“Œ ์˜คํ† ๋ ˆ์ด์•„์›ƒ ์„ค์ • + dayLabel.translatesAutoresizingMaskIntoConstraints = false + eventStackView.translatesAutoresizingMaskIntoConstraints = false + backgroundContainer.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + dayLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + dayLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + dayLabel.widthAnchor.constraint(equalToConstant: 32), + dayLabel.heightAnchor.constraint(equalToConstant: 32), + + backgroundContainer.centerXAnchor.constraint(equalTo: dayLabel.centerXAnchor), + backgroundContainer.centerYAnchor.constraint(equalTo: dayLabel.centerYAnchor), + backgroundContainer.widthAnchor.constraint(equalTo: dayLabel.widthAnchor), + backgroundContainer.heightAnchor.constraint(equalTo: dayLabel.heightAnchor), + + eventStackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + eventStackView.topAnchor.constraint(equalTo: dayLabel.bottomAnchor, constant: 4) + ]) + } + + /// ๐Ÿ“Œ ์Šคํƒ€์ผ ๋ฐ ์ด๋ฒคํŠธ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ + private func updateAppearance() { + dayLabel.textColor = style.textColor + backgroundContainer.backgroundColor = style.backgroundColor + } + + private func updateEventDisplay() { + eventCountLabel.text = "\(eventCount)" + let eventExists: Bool = eventCount > 0 + eventStackView.isHidden = !eventExists + let presentCount = !isWeekMode && eventExists + eventCountLabel.isHidden = !presentCount + } + + /// ๐Ÿ“Œ ๋‚ ์งœ ์„ค์ • + func configure( + with date: Date, + isSelected: Bool, + eventCount: Int = 0, + isWeekMode: Bool = false + ) { + self.customDate = date + self.isCustomSelected = isSelected + self.eventCount = eventCount + self.isWeekMode = isWeekMode + + // ๐Ÿ”น ํ˜„์žฌ ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ๋™์ ์œผ๋กœ Style ์„ค์ • + if isSelected { + self.style = .selected + } else if Calendar.current.isDateInToday(date) { + self.style = .today + } else { + self.style = .default + } + + dayLabel.text = "\(Calendar.current.component(.day, from: date))" + self.updateAppearance() + self.updateEventDisplay() + } + + override func prepareForReuse() { + super.prepareForReuse() + // โœ… ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ ์ดˆ๊ธฐํ™” + customDate = nil + isCustomSelected = false + isWeekMode = false + + // โœ… ์ด๋ฒคํŠธ ๊ด€๋ จ ์ดˆ๊ธฐํ™” + eventCount = 0 + eventStackView.isHidden = true + eventCountLabel.text = nil + + // โœ… ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” + style = .default + updateAppearance() + updateEventDisplay() + } + +} + +// MARK: - ์Šคํƒ€์ผ ์„ค์ • +extension FSCustomCalendarCell { + enum Style { + case `default` + case today + case selected + + var textColor: UIColor { + switch self { + case .default, .today: + return UIColor(.neutral600) + case .selected: + return UIColor(.common0) + } + } + + var backgroundColor: UIColor { + switch self { + case .default: + return .clear + case .today: + return UIColor(.neutral200) + case .selected: + return UIColor(.neutral900) + } + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift new file mode 100644 index 00000000..39c60f29 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift @@ -0,0 +1,123 @@ +// +// ScrollCalendarView.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 1/31/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import FSCalendar + +public struct FSCalendarView: UIViewRepresentable { + @Binding var selectedDate: Date + @Binding var currentPage: Date + var isWeekMode: Bool + var events: [Date: Int] + + public init( + selectedDate: Binding, + currentPage: Binding, + isWeekMode: Bool = false, + events: [Date: Int] = [:] + ) { + self._selectedDate = selectedDate + self._currentPage = currentPage + self.isWeekMode = isWeekMode + self.events = events + } + + public class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { + var parent: FSCalendarView + + init(_ parent: FSCalendarView) { + self.parent = parent + } + + // ๋‚ ์งœ ์„ ํƒ ์ด๋ฒคํŠธ + public func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { + DispatchQueue.main.async { + self.parent.selectedDate = date + calendar.reloadData() + } + } + + public func calendarCurrentPageDidChange(_ calendar: FSCalendar) { + DispatchQueue.main.async { + self.parent.currentPage = calendar.currentPage + } + } + + public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { + + guard let cell = calendar.dequeueReusableCell(withIdentifier: FSCustomCalendarCell.identifier, for: date, at: position) as? FSCustomCalendarCell else { + return FSCalendarCell() + } + + let isSelected = Calendar.current.isDate(parent.selectedDate, inSameDayAs: date) + let eventCount = parent.events[date] ?? 0 + cell.configure( + with: date, + isSelected: isSelected, + eventCount: eventCount, + isWeekMode: parent.isWeekMode + ) + + return cell + } + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeUIView(context: Context) -> FSCalendar { + let calendar = FSCalendar() + calendar.register(FSCustomCalendarCell.self, forCellReuseIdentifier: FSCustomCalendarCell.identifier) + + // ๊ธฐ๋ณธ ์„ค์ • + calendar.delegate = context.coordinator + calendar.dataSource = context.coordinator + + calendar.locale = Locale(identifier: "ko_KR") + calendar.placeholderType = .none + + // ๐Ÿ“Œ FSCalendar ๊ธฐ๋ณธ ์„ค์ • + calendar.headerHeight = 0 + // Weekday + calendar.appearance.weekdayTextColor = UIColor(.neutral400) + calendar.appearance.weekdayFont = Typography.FontStyle.label2Medium.uiFont + // Today + + // Selected + + calendar.collectionView.contentSize = FSCustomCalendarCell.cellSize + + // Additional + calendar.appearance.selectionColor = .clear + calendar.appearance.todayColor = .clear + calendar.appearance.titleSelectionColor = .clear + calendar.appearance.titleDefaultColor = .clear + calendar.calendarWeekdayView.weekdayLabels[0].textColor = UIColor(.red500) + + print("๋„ˆ๋Š” ๋˜๋‹ˆ(\(events)") + return calendar + } + + public func updateUIView(_ uiView: FSCalendar, context: Context) { + // ์„ ํƒ๋œ ๋‚ ์งœ ๋ฐ˜์˜ + uiView.select(selectedDate) + + // โœ… SwiftUI์—์„œ `currentMonth`๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ `currentPage` ์—…๋ฐ์ดํŠธ + if uiView.currentPage != currentPage { + uiView.setCurrentPage(currentPage, animated: true) + } + + // โœ… ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ชจ๋“œ ๋ณ€๊ฒฝ + let targetScope: FSCalendarScope = isWeekMode ? .week : .month + if uiView.scope != targetScope { + uiView.scope = targetScope + uiView.reloadData() + } + } +} diff --git a/TnT/Tuist/Package.resolved b/TnT/Tuist/Package.resolved index 14c7aa10..4e5346f2 100644 --- a/TnT/Tuist/Package.resolved +++ b/TnT/Tuist/Package.resolved @@ -18,6 +18,15 @@ "version" : "1.0.3" } }, + { + "identity" : "fscalendar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WenchaoD/FSCalendar.git", + "state" : { + "revision" : "0fbdec5172fccb90f707472eeaea4ffe095278f6", + "version" : "2.8.4" + } + }, { "identity" : "kakao-ios-sdk", "kind" : "remoteSourceControl", diff --git a/TnT/Tuist/Package.swift b/TnT/Tuist/Package.swift index 492314c3..9ab78b00 100644 --- a/TnT/Tuist/Package.swift +++ b/TnT/Tuist/Package.swift @@ -17,6 +17,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.17.0"), .package(url: "https://github.com/airbnb/lottie-ios", from: "4.5.0"), .package(url: "https://github.com/kakao/kakao-ios-sdk.git", from: "2.20.0"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.3") + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.3"), + .package(url: "https://github.com/WenchaoD/FSCalendar.git", from: "2.8.4") ] ) diff --git a/TnT/Tuist/ProjectDescriptionHelpers/Configuration/Configuration+Templates.swift b/TnT/Tuist/ProjectDescriptionHelpers/Configuration/Configuration+Templates.swift index 0fcc5b3d..8cf74630 100644 --- a/TnT/Tuist/ProjectDescriptionHelpers/Configuration/Configuration+Templates.swift +++ b/TnT/Tuist/ProjectDescriptionHelpers/Configuration/Configuration+Templates.swift @@ -10,6 +10,10 @@ public extension Configuration { static func defaultSettings() -> Settings { return Settings.settings( + base: [ + "OTHER_LDFLAGS": "$(inherited) -ObjC", + "CODE_SIGN_STYLE": "Manual" + ], configurations: [ .debug(name: .debug, xcconfig: .relativeToRoot("Tuist/Config/Secrets.xcconfig")), .release(name: .release, xcconfig: .relativeToRoot("Tuist/Config/Secrets.xcconfig")), diff --git a/TnT/Tuist/ProjectDescriptionHelpers/Dependency/DependencyInformation.swift b/TnT/Tuist/ProjectDescriptionHelpers/Dependency/DependencyInformation.swift index 7da5ee1c..6af5c9b2 100644 --- a/TnT/Tuist/ProjectDescriptionHelpers/Dependency/DependencyInformation.swift +++ b/TnT/Tuist/ProjectDescriptionHelpers/Dependency/DependencyInformation.swift @@ -12,7 +12,7 @@ let dependencyInfo: [DependencyInformation: [DependencyInformation]] = [ .Presentation: [.DIContainer, .DesignSystem, .Domain, .ComposableArchitecture], .Domain: [.SwiftDepedencies], .Data: [.Domain, .KakaoSDKUser, .SwiftDepedencies], - .DesignSystem: [.Lottie], + .DesignSystem: [.Lottie, .FSCalendar], .DIContainer: [.Domain, .Data] ] @@ -27,6 +27,7 @@ public enum DependencyInformation: String, CaseIterable, Sendable { case ComposableArchitecture = "ComposableArchitecture" case KakaoSDKUser = "KakaoSDKUser" case SwiftDepedencies = "Dependencies" + case FSCalendar = "FSCalendar" } public extension DependencyInformation { diff --git a/TnT/Tuist/ProjectDescriptionHelpers/Dependency/ExternalDependency.swift b/TnT/Tuist/ProjectDescriptionHelpers/Dependency/ExternalDependency.swift index 32da25b8..3ee6f572 100644 --- a/TnT/Tuist/ProjectDescriptionHelpers/Dependency/ExternalDependency.swift +++ b/TnT/Tuist/ProjectDescriptionHelpers/Dependency/ExternalDependency.swift @@ -11,5 +11,6 @@ let externalDependency: [DependencyInformation] = [ .KakaoSDKUser, .Lottie, .ComposableArchitecture, - .SwiftDepedencies + .SwiftDepedencies, + .FSCalendar ] From f11183f21e03e0daa85e6e377e22d31999267f48 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 00:11:51 +0900 Subject: [PATCH 02/16] =?UTF-8?q?[Feat]=20TCalendar=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/TCalendarCell.swift | 109 ++++++++------- .../Components/Calendar/TCalendarView.swift | 129 +++++++++--------- 2 files changed, 125 insertions(+), 113 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift index b5b5996a..4bb84927 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift @@ -1,5 +1,5 @@ // -// CustumCalendarCell.swift +// TCalendarCell.swift // DesignSystem // // Created by ๋ฐ•๋ฏผ์„œ on 2/1/25. @@ -8,26 +8,35 @@ import FSCalendar -class FSCustomCalendarCell: FSCalendarCell { - - static let identifier: String = "CustomCalendarCell" +/// TCalendar์— ์‚ฌ์šฉ๋˜๋Š” Cell ์ž…๋‹ˆ๋‹ค +final class TCalendarCell: FSCalendarCell { + // MARK: Properties + static let identifier: String = "TCalendarCell" static let cellSize: CGSize = CGSize(width: 51, height: 54) - var customDate: Date? - var isCustomSelected: Bool = false - var style: Style = .default - var eventCount: Int = 0 - var isWeekMode: Bool = false + /// Cell์— ํ‘œ์‹œ๋˜๋Š” ๋‚ ์งœ + private var customDate: Date? + /// Cell ์ด ์„ ํƒ๋˜์—ˆ๋Š”์ง€ ํ‘œ์‹œ + private var isCellSelected: Bool = false + /// Cell ์Šคํƒ€์ผ + private var style: Style = .default + /// Cell์— ํ‘œ์‹œ๋˜๋Š” ์ผ์ • ์นด์šดํŠธ + private var eventCount: Int = 0 + /// ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ชจ๋“œ์ธ์ง€ ํ‘œ์‹œ + private var isWeekMode: Bool = false - private let dayLabel = UILabel() - private let eventStackView = UIStackView() - private let eventIcon = UIImageView() - private let eventCountLabel = UILabel() - private let backgroundContainer = UIView() + // MARK: UI Elements + private let dayLabel: UILabel = UILabel() + private let eventStackView: UIStackView = UIStackView() + private let eventIcon: UIImageView = UIImageView() + private let eventCountLabel: UILabel = UILabel() + private let backgroundContainer: UIView = UIView() override init(frame: CGRect) { super.init(frame: frame) setupUI() + setUpHierarchy() + setUpConstraint() } required init(coder aDecoder: NSCoder) { @@ -35,35 +44,34 @@ class FSCustomCalendarCell: FSCalendarCell { } private func setupUI() { - contentView.addSubview(backgroundContainer) backgroundContainer.layer.cornerRadius = 8 - // ๐Ÿ“Œ ๋‚ ์งœ ๋ผ๋ฒจ ์„ค์ • dayLabel.font = Typography.FontStyle.body2Medium.uiFont dayLabel.textAlignment = .center - // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์Šคํƒ๋ทฐ ์„ค์ • eventStackView.axis = .horizontal eventStackView.spacing = 2 eventStackView.alignment = .center - // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์•„์ด์ฝ˜ ์„ค์ • eventIcon.image = UIImage(resource: .icnStar).withRenderingMode(.alwaysTemplate) eventIcon.tintColor = UIColor(.red300) eventIcon.contentMode = .scaleAspectFit eventIcon.frame = CGRect(x: 0, y: 0, width: 12, height: 12) - // ๐Ÿ“Œ ์ด๋ฒคํŠธ ์นด์šดํŠธ ๋ผ๋ฒจ ์„ค์ • eventCountLabel.font = Typography.FontStyle.label2Medium.uiFont eventCountLabel.textColor = UIColor(.neutral400) - + } + + private func setUpHierarchy() { eventStackView.addArrangedSubview(eventIcon) eventStackView.addArrangedSubview(eventCountLabel) + contentView.addSubview(backgroundContainer) contentView.addSubview(dayLabel) contentView.addSubview(eventStackView) - - // ๐Ÿ“Œ ์˜คํ† ๋ ˆ์ด์•„์›ƒ ์„ค์ • + } + + private func setUpConstraint() { dayLabel.translatesAutoresizingMaskIntoConstraints = false eventStackView.translatesAutoresizingMaskIntoConstraints = false backgroundContainer.translatesAutoresizingMaskIntoConstraints = false @@ -84,34 +92,55 @@ class FSCustomCalendarCell: FSCalendarCell { ]) } - /// ๐Ÿ“Œ ์Šคํƒ€์ผ ๋ฐ ์ด๋ฒคํŠธ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ + /// ์…€ ์Šคํƒ€์ผ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ private func updateAppearance() { dayLabel.textColor = style.textColor backgroundContainer.backgroundColor = style.backgroundColor } + /// ์ผ์ • ์นด์šดํŠธ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ private func updateEventDisplay() { eventCountLabel.text = "\(eventCount)" let eventExists: Bool = eventCount > 0 eventStackView.isHidden = !eventExists - let presentCount = !isWeekMode && eventExists + let presentCount: Bool = !isWeekMode && eventExists eventCountLabel.isHidden = !presentCount } - /// ๐Ÿ“Œ ๋‚ ์งœ ์„ค์ • + override func prepareForReuse() { + super.prepareForReuse() + // ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ ์ดˆ๊ธฐํ™” + customDate = nil + isCellSelected = false + isWeekMode = false + + // ์ผ์ • ๊ด€๋ จ ์ดˆ๊ธฐํ™” + eventCount = 0 + eventStackView.isHidden = true + eventCountLabel.text = nil + + // ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” + style = .default + updateAppearance() + updateEventDisplay() + } +} + +extension TCalendarCell { + /// ์…€ ์„ค์ • func configure( with date: Date, - isSelected: Bool, + isCellSelected: Bool, eventCount: Int = 0, isWeekMode: Bool = false ) { self.customDate = date - self.isCustomSelected = isSelected + self.isCellSelected = isCellSelected self.eventCount = eventCount self.isWeekMode = isWeekMode - // ๐Ÿ”น ํ˜„์žฌ ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ๋™์ ์œผ๋กœ Style ์„ค์ • - if isSelected { + // ํ˜„์žฌ ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜, Style ์„ค์ • + if isCellSelected { self.style = .selected } else if Calendar.current.isDateInToday(date) { self.style = .today @@ -123,29 +152,9 @@ class FSCustomCalendarCell: FSCalendarCell { self.updateAppearance() self.updateEventDisplay() } - - override func prepareForReuse() { - super.prepareForReuse() - // โœ… ๋‚ ์งœ ๋ฐ ์„ ํƒ ์ƒํƒœ ์ดˆ๊ธฐํ™” - customDate = nil - isCustomSelected = false - isWeekMode = false - - // โœ… ์ด๋ฒคํŠธ ๊ด€๋ จ ์ดˆ๊ธฐํ™” - eventCount = 0 - eventStackView.isHidden = true - eventCountLabel.text = nil - - // โœ… ์Šคํƒ€์ผ ์ดˆ๊ธฐํ™” - style = .default - updateAppearance() - updateEventDisplay() - } - } -// MARK: - ์Šคํƒ€์ผ ์„ค์ • -extension FSCustomCalendarCell { +extension TCalendarCell { enum Style { case `default` case today diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift index 39c60f29..3e35928c 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift @@ -9,11 +9,17 @@ import SwiftUI import FSCalendar -public struct FSCalendarView: UIViewRepresentable { - @Binding var selectedDate: Date - @Binding var currentPage: Date - var isWeekMode: Bool - var events: [Date: Int] +/// ์•ฑ ์ „๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์บ˜๋ฆฐ๋”์ž…๋‹ˆ๋‹ค. +/// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. +public struct TCalendarView: UIViewRepresentable { + /// ์„ ํƒํ•œ ๋‚ ์งœ + @Binding private var selectedDate: Date + /// ํ˜„์žฌ ํŽ˜์ด์ง€ + @Binding private var currentPage: Date + /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ ์—ฌ๋ถ€ + private var isWeekMode: Bool + /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ์ด๋ฒคํŠธ ๋”•์…”๋„ˆ๋ฆฌ + private var events: [Date: Int] public init( selectedDate: Binding, @@ -27,97 +33,94 @@ public struct FSCalendarView: UIViewRepresentable { self.events = events } - public class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { - var parent: FSCalendarView - - init(_ parent: FSCalendarView) { - self.parent = parent - } - - // ๋‚ ์งœ ์„ ํƒ ์ด๋ฒคํŠธ - public func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { - DispatchQueue.main.async { - self.parent.selectedDate = date - calendar.reloadData() - } - } - - public func calendarCurrentPageDidChange(_ calendar: FSCalendar) { - DispatchQueue.main.async { - self.parent.currentPage = calendar.currentPage - } - } - - public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { - - guard let cell = calendar.dequeueReusableCell(withIdentifier: FSCustomCalendarCell.identifier, for: date, at: position) as? FSCustomCalendarCell else { - return FSCalendarCell() - } - - let isSelected = Calendar.current.isDate(parent.selectedDate, inSameDayAs: date) - let eventCount = parent.events[date] ?? 0 - cell.configure( - with: date, - isSelected: isSelected, - eventCount: eventCount, - isWeekMode: parent.isWeekMode - ) - - return cell - } - } - public func makeCoordinator() -> Coordinator { Coordinator(self) } public func makeUIView(context: Context) -> FSCalendar { - let calendar = FSCalendar() - calendar.register(FSCustomCalendarCell.self, forCellReuseIdentifier: FSCustomCalendarCell.identifier) + let calendar: FSCalendar = FSCalendar() + // Cell ์„ค์ • + calendar.register(TCalendarCell.self, forCellReuseIdentifier: TCalendarCell.identifier) + calendar.collectionView.contentSize = TCalendarCell.cellSize // ๊ธฐ๋ณธ ์„ค์ • calendar.delegate = context.coordinator calendar.dataSource = context.coordinator - calendar.locale = Locale(identifier: "ko_KR") - calendar.placeholderType = .none - // ๐Ÿ“Œ FSCalendar ๊ธฐ๋ณธ ์„ค์ • + // UI ์„ค์ • + calendar.placeholderType = .none calendar.headerHeight = 0 - // Weekday calendar.appearance.weekdayTextColor = UIColor(.neutral400) calendar.appearance.weekdayFont = Typography.FontStyle.label2Medium.uiFont - // Today - - // Selected - - calendar.collectionView.contentSize = FSCustomCalendarCell.cellSize - - // Additional calendar.appearance.selectionColor = .clear calendar.appearance.todayColor = .clear calendar.appearance.titleSelectionColor = .clear calendar.appearance.titleDefaultColor = .clear calendar.calendarWeekdayView.weekdayLabels[0].textColor = UIColor(.red500) - print("๋„ˆ๋Š” ๋˜๋‹ˆ(\(events)") return calendar } public func updateUIView(_ uiView: FSCalendar, context: Context) { - // ์„ ํƒ๋œ ๋‚ ์งœ ๋ฐ˜์˜ + // `selectedDate` ๋ฐ˜์˜ uiView.select(selectedDate) - // โœ… SwiftUI์—์„œ `currentMonth`๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ `currentPage` ์—…๋ฐ์ดํŠธ + // `currentPage` ๋ฐ˜์˜ if uiView.currentPage != currentPage { uiView.setCurrentPage(currentPage, animated: true) } - // โœ… ์ฃผ๊ฐ„/์›”๊ฐ„ ๋ชจ๋“œ ๋ณ€๊ฒฝ + // `isWeekMode` ๋ฐ˜์˜ let targetScope: FSCalendarScope = isWeekMode ? .week : .month if uiView.scope != targetScope { uiView.scope = targetScope - uiView.reloadData() + } + + uiView.reloadData() + } +} + +public extension TCalendarView { + final class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { + var parent: TCalendarView + + init(_ parent: TCalendarView) { + self.parent = parent + } + + // ๋‚ ์งœ ์„ ํƒ ์ด๋ฒคํŠธ + public func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { + DispatchQueue.main.async { + self.parent.selectedDate = date + calendar.reloadData() + } + } + + // ํ˜„์žฌ ํŽ˜์ด์ง€ ์ „ํ™˜ ์ด๋ฒคํŠธ + public func calendarCurrentPageDidChange(_ calendar: FSCalendar) { + DispatchQueue.main.async { + self.parent.currentPage = calendar.currentPage + } + } + + // ์บ˜๋ฆฐ๋” ์…€ ์ฃผ์ž… + public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { + + guard let cell = calendar.dequeueReusableCell(withIdentifier: TCalendarCell.identifier, for: date, at: position) as? TCalendarCell else { + return FSCalendarCell() + } + + let isSelected: Bool = Calendar.current.isDate(parent.selectedDate, inSameDayAs: date) + let eventCount: Int = parent.events[date] ?? 0 + cell.configure( + with: date, + isCellSelected: isSelected, + eventCount: eventCount, + isWeekMode: parent.isWeekMode + ) + + return cell } } } From 35deca3fe7450c98b7b1e2f3b67e0b66978a503d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 02:05:53 +0900 Subject: [PATCH 03/16] =?UTF-8?q?[Feat]=20TCalendar=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/TCalendarRepresentable.swift | 125 ++++++++++++++++++ .../Components/Calendar/TCalendarView.swift | 120 ++++------------- 2 files changed, 148 insertions(+), 97 deletions(-) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift new file mode 100644 index 00000000..29f9604c --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift @@ -0,0 +1,125 @@ +// +// TCalendarRepresentable.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 1/31/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import FSCalendar + + +public struct TCalendarRepresentable: UIViewRepresentable { + /// ์„ ํƒํ•œ ๋‚ ์งœ + @Binding private var selectedDate: Date + /// ํ˜„์žฌ ํŽ˜์ด์ง€ + @Binding private var currentPage: Date + /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ ์—ฌ๋ถ€ + private var isWeekMode: Bool + /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ์ด๋ฒคํŠธ ๋”•์…”๋„ˆ๋ฆฌ + private var events: [Date: Int] + + public init( + selectedDate: Binding, + currentPage: Binding, + isWeekMode: Bool = false, + events: [Date: Int] = [:] + ) { + self._selectedDate = selectedDate + self._currentPage = currentPage + self.isWeekMode = isWeekMode + self.events = events + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeUIView(context: Context) -> FSCalendar { + let calendar: FSCalendar = FSCalendar() + // Cell ์„ค์ • + calendar.register(TCalendarCell.self, forCellReuseIdentifier: TCalendarCell.identifier) + calendar.collectionView.contentSize = TCalendarCell.cellSize + + // ๊ธฐ๋ณธ ์„ค์ • + calendar.delegate = context.coordinator + calendar.dataSource = context.coordinator + calendar.locale = Locale(identifier: "ko_KR") + + // UI ์„ค์ • + calendar.placeholderType = .none + calendar.headerHeight = 0 + calendar.appearance.weekdayTextColor = UIColor(.neutral400) + calendar.appearance.weekdayFont = Typography.FontStyle.label2Medium.uiFont + calendar.appearance.selectionColor = .clear + calendar.appearance.todayColor = .clear + calendar.appearance.titleSelectionColor = .clear + calendar.appearance.titleDefaultColor = .clear + calendar.calendarWeekdayView.weekdayLabels[0].textColor = UIColor(.red500) + + return calendar + } + + public func updateUIView(_ uiView: FSCalendar, context: Context) { + // `selectedDate` ๋ฐ˜์˜ + uiView.select(selectedDate) + + // `currentPage` ๋ฐ˜์˜ + if uiView.currentPage != currentPage { + uiView.setCurrentPage(currentPage, animated: true) + } + + // `isWeekMode` ๋ฐ˜์˜ + let targetScope: FSCalendarScope = isWeekMode ? .week : .month + if uiView.scope != targetScope { + uiView.scope = targetScope + } + + uiView.reloadData() + } +} + +public extension TCalendarRepresentable { + final class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { + var parent: TCalendarRepresentable + + init(_ parent: TCalendarRepresentable) { + self.parent = parent + } + + // ๋‚ ์งœ ์„ ํƒ ์ด๋ฒคํŠธ + public func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { + DispatchQueue.main.async { + self.parent.selectedDate = date + calendar.reloadData() + } + } + + // ํ˜„์žฌ ํŽ˜์ด์ง€ ์ „ํ™˜ ์ด๋ฒคํŠธ + public func calendarCurrentPageDidChange(_ calendar: FSCalendar) { + DispatchQueue.main.async { + self.parent.currentPage = calendar.currentPage + } + } + + // ์บ˜๋ฆฐ๋” ์…€ ์ฃผ์ž… + public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { + + guard let cell = calendar.dequeueReusableCell(withIdentifier: TCalendarCell.identifier, for: date, at: position) as? TCalendarCell else { + return FSCalendarCell() + } + + let isSelected: Bool = Calendar.current.isDate(parent.selectedDate, inSameDayAs: date) + let eventCount: Int = parent.events[date] ?? 0 + cell.configure( + with: date, + isCellSelected: isSelected, + eventCount: eventCount, + isWeekMode: parent.isWeekMode + ) + + return cell + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift index 3e35928c..6d8ffd60 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift @@ -1,21 +1,23 @@ // -// ScrollCalendarView.swift +// TCalendarView.swift // DesignSystem // -// Created by ๋ฐ•๋ฏผ์„œ on 1/31/25. +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. // Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. // import SwiftUI -import FSCalendar /// ์•ฑ ์ „๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์บ˜๋ฆฐ๋”์ž…๋‹ˆ๋‹ค. /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. -public struct TCalendarView: UIViewRepresentable { +public struct TCalendarView: View { + /// ์„ ํƒํ•œ ๋‚ ์งœ - @Binding private var selectedDate: Date + @Binding var selectedDate: Date /// ํ˜„์žฌ ํŽ˜์ด์ง€ - @Binding private var currentPage: Date + @Binding var currentPage: Date + /// ์—…๋ฐ์ดํŠธ ํ”Œ๋ž˜๊ทธ + @State private var forceUpdate: UUID = UUID() /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ ์—ฌ๋ถ€ private var isWeekMode: Bool /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ์ด๋ฒคํŠธ ๋”•์…”๋„ˆ๋ฆฌ @@ -24,103 +26,27 @@ public struct TCalendarView: UIViewRepresentable { public init( selectedDate: Binding, currentPage: Binding, - isWeekMode: Bool = false, - events: [Date: Int] = [:] + forceUpdate: UUID = UUID(), + events: [Date: Int], + isWeekMode: Bool = false ) { self._selectedDate = selectedDate self._currentPage = currentPage - self.isWeekMode = isWeekMode self.events = events + self.forceUpdate = forceUpdate + self.isWeekMode = isWeekMode } - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - public func makeUIView(context: Context) -> FSCalendar { - let calendar: FSCalendar = FSCalendar() - // Cell ์„ค์ • - calendar.register(TCalendarCell.self, forCellReuseIdentifier: TCalendarCell.identifier) - calendar.collectionView.contentSize = TCalendarCell.cellSize - - // ๊ธฐ๋ณธ ์„ค์ • - calendar.delegate = context.coordinator - calendar.dataSource = context.coordinator - calendar.locale = Locale(identifier: "ko_KR") - - // UI ์„ค์ • - calendar.placeholderType = .none - calendar.headerHeight = 0 - calendar.appearance.weekdayTextColor = UIColor(.neutral400) - calendar.appearance.weekdayFont = Typography.FontStyle.label2Medium.uiFont - calendar.appearance.selectionColor = .clear - calendar.appearance.todayColor = .clear - calendar.appearance.titleSelectionColor = .clear - calendar.appearance.titleDefaultColor = .clear - calendar.calendarWeekdayView.weekdayLabels[0].textColor = UIColor(.red500) - - return calendar - } - - public func updateUIView(_ uiView: FSCalendar, context: Context) { - // `selectedDate` ๋ฐ˜์˜ - uiView.select(selectedDate) - - // `currentPage` ๋ฐ˜์˜ - if uiView.currentPage != currentPage { - uiView.setCurrentPage(currentPage, animated: true) - } - - // `isWeekMode` ๋ฐ˜์˜ - let targetScope: FSCalendarScope = isWeekMode ? .week : .month - if uiView.scope != targetScope { - uiView.scope = targetScope - } - - uiView.reloadData() - } -} -public extension TCalendarView { - final class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { - var parent: TCalendarView - - init(_ parent: TCalendarView) { - self.parent = parent - } - - // ๋‚ ์งœ ์„ ํƒ ์ด๋ฒคํŠธ - public func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { - DispatchQueue.main.async { - self.parent.selectedDate = date - calendar.reloadData() - } - } - - // ํ˜„์žฌ ํŽ˜์ด์ง€ ์ „ํ™˜ ์ด๋ฒคํŠธ - public func calendarCurrentPageDidChange(_ calendar: FSCalendar) { - DispatchQueue.main.async { - self.parent.currentPage = calendar.currentPage - } - } - - // ์บ˜๋ฆฐ๋” ์…€ ์ฃผ์ž… - public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { - - guard let cell = calendar.dequeueReusableCell(withIdentifier: TCalendarCell.identifier, for: date, at: position) as? TCalendarCell else { - return FSCalendarCell() - } - - let isSelected: Bool = Calendar.current.isDate(parent.selectedDate, inSameDayAs: date) - let eventCount: Int = parent.events[date] ?? 0 - cell.configure( - with: date, - isCellSelected: isSelected, - eventCount: eventCount, - isWeekMode: parent.isWeekMode - ) - - return cell + public var body: some View { + TCalendarRepresentable( + selectedDate: $selectedDate, + currentPage: $currentPage, + isWeekMode: isWeekMode, + events: events + ) + .id(forceUpdate) + .onChange(of: events) { + forceUpdate = UUID() } } } From 02e6d4b30d46bcbf8f16d59ce773767cf2091ba7 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 02:17:46 +0900 Subject: [PATCH 04/16] =?UTF-8?q?[Feat]=20TCalendarHeader=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/TCalendarHeader.swift | 51 +++++++++++++++++++ .../DesignSystem/Image+DesignSystem.swift | 2 +- .../Sources/Utility/TDateFormatUtility.swift | 4 +- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift new file mode 100644 index 00000000..03d0bcda --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift @@ -0,0 +1,51 @@ +// +// TCalendarHeader.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// TCalendarView์˜ ํ—ค๋”์ž…๋‹ˆ๋‹ค +/// ์›” ์ด๋™ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค +public struct TCalendarHeader: View { + + @Binding private var currentPage: Date + private var formatter: (Date) -> String + + public init(currentPage: Binding, formatter: @escaping (Date) -> String) { + self._currentPage = currentPage + self.formatter = formatter + } + + public var body: some View { + HStack { + Button(action: { + movePage(-1) + }, label: { + Image(.icnTriangleLeft32px) + .resizable() + .frame(width: 32, height: 32) + }) + + Text("\(formatter(currentPage))") + .typographyStyle(.heading3, with: .neutral900) + + Button(action: { + movePage(1) + }, label: { + Image(.icnTriangleRight32px) + .resizable() + .frame(width: 32, height: 32) + }) + } + } + + private func movePage(_ direction: Int) { + if let nextPage = Calendar.current.date(byAdding: .month, value: direction, to: currentPage) { + currentPage = nextPage + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift index 2ac9953e..293f3ab6 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift @@ -64,7 +64,7 @@ public extension ImageResource { static let icnArrowRight: ImageResource = DesignSystemAsset.icnArrowRight.imageResource static let icnArrowUp: ImageResource = DesignSystemAsset.icnArrowUp.imageResource static let icnTriangleLeft32px: ImageResource = DesignSystemAsset.icnTriangleLeft32.imageResource - static let icnTrianleRight32px: ImageResource = DesignSystemAsset.icnTriangleRight32.imageResource + static let icnTriangleRight32px: ImageResource = DesignSystemAsset.icnTriangleRight32.imageResource static let icnWarning: ImageResource = DesignSystemAsset.icnWarning.imageResource } diff --git a/TnT/Projects/Domain/Sources/Utility/TDateFormatUtility.swift b/TnT/Projects/Domain/Sources/Utility/TDateFormatUtility.swift index aec01a04..56926f7c 100644 --- a/TnT/Projects/Domain/Sources/Utility/TDateFormatUtility.swift +++ b/TnT/Projects/Domain/Sources/Utility/TDateFormatUtility.swift @@ -8,7 +8,7 @@ import Foundation -enum TDateFormatUtility { +public enum TDateFormatUtility { /// `NSCache`๋ฅผ ํ™œ์šฉํ•œ ํฌ๋งท๋ณ„ `DateFormatter` ์บ์‹ฑ private static let cache: NSCache = { let cache: NSCache = NSCache() @@ -16,7 +16,7 @@ enum TDateFormatUtility { }() /// ํฌ๋งท์— ๋งž๋Š” `DateFormatter` ๋ฐ˜ํ™˜ (์บ์‹ฑ๋œ ์ธ์Šคํ„ด์Šค ์žฌ์‚ฌ์šฉ) - static func formatter(for format: TDateFormat) -> DateFormatter { + public static func formatter(for format: TDateFormat) -> DateFormatter { // ์บ์‹œ๋œ DateFormatter๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ์ƒ์„ฑ ํ›„ ์ €์žฅ if let cachedFormatter = cache.object(forKey: format.rawValue as NSString) { return cachedFormatter From 648522c4e92c113a5c068beda9a03360db0f7e4c Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 02:29:31 +0900 Subject: [PATCH 05/16] =?UTF-8?q?[Feat]=20Header=20rightView=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/TCalendarHeader.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift index 03d0bcda..4436b30a 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift @@ -10,18 +10,24 @@ import SwiftUI /// TCalendarView์˜ ํ—ค๋”์ž…๋‹ˆ๋‹ค /// ์›” ์ด๋™ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค -public struct TCalendarHeader: View { +public struct TCalendarHeader: View { @Binding private var currentPage: Date private var formatter: (Date) -> String + private var rightView: (() -> RightView)? - public init(currentPage: Binding, formatter: @escaping (Date) -> String) { - self._currentPage = currentPage - self.formatter = formatter - } + public init( + currentPage: Binding, + formatter: @escaping (Date) -> String, + rightView: (() -> RightView)? = nil + ) { + self._currentPage = currentPage + self.formatter = formatter + self.rightView = rightView + } public var body: some View { - HStack { + HStack(spacing: 0) { Button(action: { movePage(-1) }, label: { @@ -41,6 +47,11 @@ public struct TCalendarHeader: View { .frame(width: 32, height: 32) }) } + .frame(maxWidth: .infinity) + .overlay(alignment: .trailing) { + rightView?() + .padding(.trailing, 20) + } } private func movePage(_ direction: Int) { From 25c663ce4542e48e4260278533fad155f8e64485 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 03:32:21 +0900 Subject: [PATCH 06/16] =?UTF-8?q?[Feat[=20AutoResizingBottomSheet=20Modifi?= =?UTF-8?q?er=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoSizingBottomSheetModifier.swift | 48 +++++++++++++++++++ .../Home/Trainee/TraineeHomeView.swift | 9 ++++ .../Home/Trainee/TraineeRecordStartView.swift | 9 ++++ 3 files changed, 66 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift create mode 100644 TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift create mode 100644 TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift b/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift new file mode 100644 index 00000000..cedb76e0 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/BottomSheet/AutoSizingBottomSheetModifier.swift @@ -0,0 +1,48 @@ +// +// AutoSizingBottomSheetModifier.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// ๋ฐ”ํ…€์‹œํŠธ์˜ ๋†’์ด๋ฅผ ์ž๋™ ์กฐ์ •ํ•˜๋Š” ViewModifier +/// ๋‚ด๋ถ€ ์ปจํ…์ธ ์˜ ํฌ๊ธฐ๋ฅผ ์ธก์ •ํ•˜์—ฌ ์ ์ ˆํ•œ ๋†’์ด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค +struct AutoSizingBottomSheetModifier: ViewModifier { + /// ๋ฐ”ํ…€์‹œํŠธ ์ƒ๋‹จ์˜ ๊ทธ๋ž˜๋ฒ„ ํ‘œ์‹œ + let presentationDragIndicator: Visibility + /// ์ธก์ •๋œ ์ปจํ…์ธ ์˜ ๋†’์ด (์ดˆ๊ธฐ๊ฐ’ 300) + @State private var contentHeight: CGFloat = 300 + + init(presentationDragIndicator: Visibility = .visible) { + self.presentationDragIndicator = presentationDragIndicator + } + + func body(content: Content) -> some View { + content + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + contentHeight = proxy.size.height + 50 + } + .onChange(of: proxy.size.height) { _, newHeight in + contentHeight = newHeight + 50 + } + } + ) + .presentationDetents([.height(contentHeight)]) + .presentationDragIndicator(presentationDragIndicator) + } +} + +public extension View { + /// ๋ทฐ์— ์ž๋™ ํฌ๊ธฐ ์กฐ์ • ๋ฐ”ํ…€์‹œํŠธ๋ฅผ ์ ์šฉํ•˜๋Š” Modifier + /// - Parameter presentationDragIndicator: ๋ฐ”ํ…€์‹œํŠธ ์ƒ๋‹จ Grabber์˜ ๊ฐ€์‹œ์„ฑ ์„ค์ • (๊ธฐ๋ณธ๊ฐ’: .visible) + /// - Returns: ์ž๋™ ํฌ๊ธฐ ์กฐ์ • ๋ฐ”ํ…€์‹œํŠธ๊ฐ€ ์ ์šฉ๋œ ๋ทฐ + func autoSizingBottomSheet(presentationDragIndicator: Visibility = .visible) -> some View { + self.modifier(AutoSizingBottomSheetModifier(presentationDragIndicator: presentationDragIndicator)) + } +} diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift new file mode 100644 index 00000000..bad6cce0 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift @@ -0,0 +1,9 @@ +// +// TraineeHomeView.swift +// Presentation +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift new file mode 100644 index 00000000..157d4569 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift @@ -0,0 +1,9 @@ +// +// TraineeRecordStartVeiw.swift +// Presentation +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation From 887491253186c855590f79ac16ac33c27a748af3 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 03:32:42 +0900 Subject: [PATCH 07/16] =?UTF-8?q?[Feat]=20TraineeRecordStartView=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Trainee/TraineeRecordStartView.swift | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift index 157d4569..63de2ab6 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift @@ -6,4 +6,64 @@ // Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. // -import Foundation +import SwiftUI + +import DesignSystem + +/// TraineeHomeView์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ก ์„ ํƒ์šฉ ๋ฐ”ํ…€ ์‹œํŠธ ๋ทฐ +struct TraineeRecordStartView: View { + /// ๋ฒ„ํŠผ ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ + var itemContents: [(emoji: String, title: String, action: () -> Void)] + /// ๋ฐ”ํ…€์‹œํŠธ ๋†’์ด + @State private var contentHeight: CGFloat = 300 + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("์–ด๋–ค ๊ธฐ๋ก์„ ํ•˜์‹œ๊ฒ ์–ด์š”?") + .typographyStyle(.heading3, with: .neutral900) + .padding(20) + + VStack(spacing: 12) { + ForEach(itemContents.indices, id: \.self) { index in + let item = itemContents[index] + RecordStartButton(action: item.action, emoji: item.emoji, title: item.title) + } + } + } + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + contentHeight = proxy.size.height + 50 // โœ… ๋‚ด๋ถ€ ๋†’์ด๋ฅผ ์ธก์ •ํ•˜์—ฌ ์ €์žฅ + } + .onChange(of: proxy.size.height) { _, newHeight in + contentHeight = newHeight + 50 // โœ… ๋†’์ด ๋ณ€๊ฒฝ ๊ฐ์ง€ ์‹œ ์—…๋ฐ์ดํŠธ + } + } + ) + .presentationDetents([.height(contentHeight)]) // โœ… ์ธก์ •ํ•œ ๋†’์ด ์ ์šฉ + .presentationDragIndicator(.visible) + } +} + +private extension TraineeRecordStartView { + struct RecordStartButton: View { + let action: () -> Void + let emoji: String + let title: String + + var body: some View { + Button(action: action, label: { + HStack(spacing: 4) { + Text(emoji) + .typographyStyle(.heading3) + Text(title) + .typographyStyle(.body1Semibold, with: .neutral600) + Spacer() + } + .padding(.horizontal, 20) + .padding(.vertical, 4) + }) + } + } +} From c14b526f2483464c3e51741a51f22e38537c076c Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 05:06:57 +0900 Subject: [PATCH 08/16] =?UTF-8?q?[Feat]=20TWorkoutCard=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Card/TWorkoutCard.swift | 141 ++++++++++++++++++ .../Components/Card/TimeIndicator.swift | 32 ++++ 2 files changed, 173 insertions(+) create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift create mode 100644 TnT/Projects/DesignSystem/Sources/Components/Card/TimeIndicator.swift diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift new file mode 100644 index 00000000..fd452527 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift @@ -0,0 +1,141 @@ +// +// TWorkoutCard.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// ํŠธ๋ ˆ์ด๋‹ˆ - PT ์šด๋™ ์นด๋“œ +public struct TWorkoutCard: View { + private let chipUIInfo: TChip.UIInfo + private let timeText: String + private let title: String + private let imgURL: URL? + private let hasRecord: Bool + private let footerTapAction: (() -> Void)? + + public init( + chipUIInfo: TChip.UIInfo, + timeText: String, + title: String, + imgURL: URL?, + hasRecord: Bool, + footerTapAction: (() -> Void)? = nil + ) { + self.chipUIInfo = chipUIInfo + self.timeText = timeText + self.title = title + self.imgURL = imgURL + self.hasRecord = hasRecord + self.footerTapAction = footerTapAction + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + Header() + + MainBody() + + if hasRecord { + Footer() + } + } + .padding(.horizontal, 12) + .padding(.vertical, 16) + .background(Color.common0) + .clipShape(.rect(cornerRadius: 12)) + } + + // MARK: Section + @ViewBuilder + public func Header() -> some View { + HStack { + TChip(uiInfo: chipUIInfo) + Spacer() + TimeIndicator(timeText: timeText) + } + } + + @ViewBuilder + public func MainBody() -> some View { + HStack(spacing: 6) { + if let imgURL { + TrainerImage(imgURL: imgURL) + } else { + Image(.imgDefaultTrainerImage) + .resizable() + .frame(width: 24, height: 24) + .scaledToFill() + .clipShape(Circle()) + } + + Text(title) + .typographyStyle(.body1Bold, with: .neutral800) + + Spacer() + } + } + + @ViewBuilder + public func Footer() -> some View { + HStack { + Button(action: { + footerTapAction?() + }, label: { + HStack(spacing: 2) { + Image(.icnStarSmile) + .resizable() + .frame(width: 24, height: 24) + Text("์ˆ˜์—… ๊ธฐ๋ก ๋ณด๊ธฐ") + .typographyStyle(.label2Medium) + } + }) + Spacer() + } + } +} + +private extension TWorkoutCard { + struct TrainerImage: View { + let imgURL: URL + + init(imgURL: URL) { + self.imgURL = imgURL + } + + var body: some View { + AsyncImage(url: imgURL) { phase in + switch phase { + case .empty: + ProgressView() + .tint(.red500) + .frame(width: 24, height: 24) + + case .success(let image): + image + .resizable() + .frame(width: 24, height: 24) + .scaledToFill() + .clipShape(Circle()) + + case .failure(let error): + Image(.imgDefaultTrainerImage) + .resizable() + .frame(width: 24, height: 24) + .scaledToFill() + .clipShape(Circle()) + + @unknown default: + Image(.imgDefaultTrainerImage) + .resizable() + .frame(width: 24, height: 24) + .scaledToFill() + .clipShape(Circle()) + } + } + } + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TimeIndicator.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TimeIndicator.swift new file mode 100644 index 00000000..2f024caa --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TimeIndicator.swift @@ -0,0 +1,32 @@ +// +// TimeIndicator.swift +// DesignSystem +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +/// Card ์šฐ์ธก ์ƒ๋‹จ - ์ด๋ชจ์ง€ + ์˜คํ›„ 16:00 ~ ์˜คํ›„ 18:00 +struct TimeIndicator: View { + private let timeText: String + + init(timeText: String) { + self.timeText = timeText + } + + var body: some View { + HStack { + Spacer() + HStack(spacing: 4) { + Image(.icnClock) + .resizable() + .scaledToFill() + .frame(width: 16, height: 16) + Text(timeText) + .typographyStyle(.label2Medium, with: .neutral500) + } + } + } +} From 3abc8a40042fd74ff9b0760b1d4f8e62e3703e7b Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 05:07:26 +0900 Subject: [PATCH 09/16] =?UTF-8?q?[Feat]=20TRecordCard=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Card/TRecordCard.swift | 61 +++++++------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift index c6e2c7b5..eccb3fa0 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift @@ -8,25 +8,29 @@ import SwiftUI +/// ํŠธ๋ ˆ์ด๋‹ˆ - ์šด๋™/์‹๋‹จ ์นด๋“œ public struct TRecordCard: View { private let chipUIInfo: TChip.UIInfo private let timeText: String private let title: String private let imgURL: URL? - private let feedbackCount: Int? + private let hasFeedback: Bool + private let footerTapAction: (() -> Void)? public init( chipUIInfo: TChip.UIInfo, timeText: String, title: String, imgURL: URL?, - feedbackCount: Int? + hasFeedback: Bool, + footerTapAction: (() -> Void)? ) { self.chipUIInfo = chipUIInfo self.timeText = timeText self.title = title self.imgURL = imgURL - self.feedbackCount = feedbackCount + self.hasFeedback = hasFeedback + self.footerTapAction = footerTapAction } public var body: some View { @@ -34,7 +38,6 @@ public struct TRecordCard: View { HStack(alignment: .top, spacing: 0) { if let imgURL { ImageSection(imgURL: imgURL) - } VStack(alignment: .leading, spacing: 12) { @@ -48,12 +51,14 @@ public struct TRecordCard: View { .padding(.bottom, imgURL == nil ? 20 : 16) } - if let feedbackCount { - Footer(feedbackCount: feedbackCount) + if hasFeedback { + Footer() .padding(.horizontal, 16) .padding(.bottom, 12) } } + .background(Color.common0) + .clipShape(.rect(cornerRadius: 12)) } // MARK: Section @@ -96,42 +101,20 @@ public struct TRecordCard: View { } @ViewBuilder - public func Footer(feedbackCount: Int) -> some View { + public func Footer() -> some View { HStack { - HStack(spacing: 2) { - Image(.icnStarSmile) - .resizable() - .frame(width: 24, height: 24) - Text("๋ฐ›์€ ํ”ผ๋“œ๋ฐฑ") - .typographyStyle(.label2Medium) - Text("\(feedbackCount)") - .typographyStyle(.label2Bold, with: .neutral500) - } - Spacer() - } - } -} - -private extension TRecordCard { - struct TimeIndicator: View { - private let timeText: String - - init(timeText: String) { - self.timeText = timeText - } - - var body: some View { - HStack { - Spacer() - HStack(spacing: 4) { - Image(.icnClock) + Button(action: { + footerTapAction?() + }, label: { + HStack(spacing: 2) { + Image(.icnStarSmile) .resizable() - .scaledToFill() - .frame(width: 16, height: 16) - Text(timeText) - .typographyStyle(.label2Medium, with: .neutral500) + .frame(width: 24, height: 24) + Text("๋ฐ›์€ ํ”ผ๋“œ๋ฐฑ") + .typographyStyle(.label2Medium) } - } + }) + Spacer() } } } From 845709b1f7f67665043446d1a6d1232c3cc88920 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:37:25 +0900 Subject: [PATCH 10/16] =?UTF-8?q?[Feat]=20Record,Workout=20ListItem=20Enti?= =?UTF-8?q?ty=20=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/RecordListItemEntity.swift | 37 +++++++++++++++++ .../Entity/WorkoutListItemEntity.swift | 41 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift create mode 100644 TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift diff --git a/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift new file mode 100644 index 00000000..2f9f0ddf --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift @@ -0,0 +1,37 @@ +// +// RecordListItemEntity.swift +// Domain +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// ํŠธ๋ ˆ์ด๋‹ˆ ๊ธฐ๋ก ๋ชฉ๋ก ์•„์ดํ…œ ๋ชจ๋ธ +public struct RecordListItemEntity { + /// ๊ธฐ๋ก ํƒ€์ž… + public let type: RecordType + /// ๊ธฐ๋ก ์‹œ๊ฐ„ + public let date: Date + /// ๊ธฐ๋ก ์ œ๋ชฉ + public let title: String + /// ํ”ผ๋“œ๋ฐฑ ์—ฌ๋ถ€ + public let hasFeedBack: Bool + /// ๊ธฐ๋ก ์ด๋ฏธ์ง€ URL + public let imageUrl: String? + + public init( + type: RecordType, + date: Date, + title: String, + hasFeedBack: Bool, + imageUrl: String? + ) { + self.type = type + self.date = date + self.title = title + self.hasFeedBack = hasFeedBack + self.imageUrl = imageUrl + } +} diff --git a/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift new file mode 100644 index 00000000..17172907 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift @@ -0,0 +1,41 @@ +// +// WorkoutListItemEntity.swift +// Domain +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// ํŠธ๋ ˆ์ด๋‹ˆ PT ์šด๋™ ๋ชฉ๋ก ์•„์ดํ…œ ๋ชจ๋ธ +public struct WorkoutListItemEntity { + /// ํ˜„์žฌ ์ˆ˜์—… ์ฐจ์ˆ˜ + public let currentCount: Int + /// ์ˆ˜์—… ์‹œ์ž‘ ์‹œ๊ฐ„ + public let startDate: Date + /// ์ˆ˜์—… ์ข…๋ฃŒ ์‹œ๊ฐ„ + public let endDate: Date + /// ํŠธ๋ ˆ์ด๋„ˆ ํ”„๋กœํ•„ ์‚ฌ์ง„ URL + public let trainerProfileImageUrl: String? + /// ํŠธ๋ ˆ์ด๋„ˆ ์ด๋ฆ„ + public let trainerName: String + /// ๊ธฐ๋ก ์—ฌ๋ถ€ + public let hasRecord: Bool + + public init( + currentCount: Int, + startDate: Date, + endDate: Date, + trainerProfileImageUrl: String?, + trainerName: String, + hasRecord: Bool + ) { + self.currentCount = currentCount + self.startDate = startDate + self.endDate = endDate + self.trainerProfileImageUrl = trainerProfileImageUrl + self.trainerName = trainerName + self.hasRecord = hasRecord + } +} From 25e39ad609430333dd5f69e488ee4dcfef9274a9 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:37:44 +0900 Subject: [PATCH 11/16] =?UTF-8?q?[Feat]=20TDateFormat=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=ED=8F=AC=EB=A7=B7=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TnT/Projects/Domain/Sources/Policy/TDateFormat.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift index c8c1b664..9a9f72b6 100644 --- a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift +++ b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift @@ -14,12 +14,18 @@ public enum TDateFormat: String { case yyyyMMdd = "yyyy-MM-dd" /// "yyyy-MM" case yyyyMM = "yyyy-MM" + /// "yyyy๋…„ MM์›”" + case yyyy๋…„_MM์›” = "yyyy๋…„ MM์›”" /// "MM-dd" case MMdd = "MM-dd" /// "yyyy/MM/dd" case yyyyMMddSlash = "yyyy/MM/dd" /// "yyyy.MM.dd" case yyyyMMddDot = "yyyy.MM.dd" + /// "01์›” 10์ผ ํ™”์š”์ผ" + case MM์›”_dd์ผ_EEEE = "MM์›” dd์ผ EEEE" /// "EE" case EE = "EE" + /// "์˜คํ›„ 17:00" (์‹œ๊ฐ„ ํฌ๋งท) + case a_HHmm = "a HH:mm" } From 9a897ef8276a857026176c293cbd38e893c8887e Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:38:57 +0900 Subject: [PATCH 12/16] =?UTF-8?q?[Chore]=20TWorkoutCard=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20UI=20=EB=82=B4=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Sources/Components/Card/TWorkoutCard.swift | 2 +- .../DesignSystem/Sources/Components/Chip/TChip.swift | 4 ++-- .../Sources/PresentationSupport/RecordType.swift | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift index fd452527..09b08c63 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift @@ -45,7 +45,7 @@ public struct TWorkoutCard: View { } .padding(.horizontal, 12) .padding(.vertical, 16) - .background(Color.common0) + .background(Color.neutral100) .clipShape(.rect(cornerRadius: 12)) } diff --git a/TnT/Projects/DesignSystem/Sources/Components/Chip/TChip.swift b/TnT/Projects/DesignSystem/Sources/Components/Chip/TChip.swift index 703d85c2..3ba8d794 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Chip/TChip.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Chip/TChip.swift @@ -46,9 +46,9 @@ public struct TChip: View { .typographyStyle(.label1Bold) } Text(title) - .typographyStyle(.label1Bold, with: style.textColor) + .typographyStyle(.label2Bold, with: style.textColor) } - .padding(.horizontal, 8) + .padding(.horizontal, 10) .padding(.vertical, 6) .background(style.backgroundColor) .clipShape(.capsule) diff --git a/TnT/Projects/Presentation/Sources/PresentationSupport/RecordType.swift b/TnT/Projects/Presentation/Sources/PresentationSupport/RecordType.swift index 6ed9a3ad..4ba83b59 100644 --- a/TnT/Projects/Presentation/Sources/PresentationSupport/RecordType.swift +++ b/TnT/Projects/Presentation/Sources/PresentationSupport/RecordType.swift @@ -31,4 +31,9 @@ extension RecordType { return type.emoji } } + + /// ์•ฑ์—์„œ ์‚ฌ์šฉ๋˜๋Š” Chip์— ์—ฐ๊ฒฐ๋˜๋Š” ์ „์ฒด UI ์ •๋ณด ์ž…๋‹ˆ๋‹ค + var chipInfo: TChip.UIInfo { + return .init(leadingEmoji: self.emoji, title: self.koreanName, style: self.chipStyle) + } } From 94a4dd173aaeb5c65c375f86bf95ad44b3d44794 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:39:27 +0900 Subject: [PATCH 13/16] =?UTF-8?q?[Fix]=20TCalendarView=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EB=86=92=EC=9D=B4=20=EA=B4=80=EB=A0=A8=20UI=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Calendar/TCalendarHeader.swift | 3 +- .../Calendar/TCalendarRepresentable.swift | 23 ++++++++++- .../Components/Calendar/TCalendarView.swift | 40 ++++++++++++++----- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift index 4436b30a..d3887687 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift @@ -50,8 +50,9 @@ public struct TCalendarHeader: View { .frame(maxWidth: .infinity) .overlay(alignment: .trailing) { rightView?() - .padding(.trailing, 20) } + .padding(.vertical, 8) + .padding(.horizontal, 20) } private func movePage(_ direction: Int) { diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift index 29f9604c..9d9beddc 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift @@ -9,12 +9,13 @@ import SwiftUI import FSCalendar - public struct TCalendarRepresentable: UIViewRepresentable { /// ์„ ํƒํ•œ ๋‚ ์งœ @Binding private var selectedDate: Date /// ํ˜„์žฌ ํŽ˜์ด์ง€ @Binding private var currentPage: Date + /// ์บ˜๋ฆฐ๋” ๋†’์ด + @Binding var calendarHeight: CGFloat /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ ์—ฌ๋ถ€ private var isWeekMode: Bool /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ์ด๋ฒคํŠธ ๋”•์…”๋„ˆ๋ฆฌ @@ -23,11 +24,13 @@ public struct TCalendarRepresentable: UIViewRepresentable { public init( selectedDate: Binding, currentPage: Binding, + calendarHeight: Binding, isWeekMode: Bool = false, events: [Date: Int] = [:] ) { self._selectedDate = selectedDate self._currentPage = currentPage + self._calendarHeight = calendarHeight self.isWeekMode = isWeekMode self.events = events } @@ -38,6 +41,7 @@ public struct TCalendarRepresentable: UIViewRepresentable { public func makeUIView(context: Context) -> FSCalendar { let calendar: FSCalendar = FSCalendar() + // Cell ์„ค์ • calendar.register(TCalendarCell.self, forCellReuseIdentifier: TCalendarCell.identifier) calendar.collectionView.contentSize = TCalendarCell.cellSize @@ -50,6 +54,8 @@ public struct TCalendarRepresentable: UIViewRepresentable { // UI ์„ค์ • calendar.placeholderType = .none calendar.headerHeight = 0 + calendar.weekdayHeight = 18 + calendar.rowHeight = TCalendarCell.cellSize.height calendar.appearance.weekdayTextColor = UIColor(.neutral400) calendar.appearance.weekdayFont = Typography.FontStyle.label2Medium.uiFont calendar.appearance.selectionColor = .clear @@ -76,6 +82,11 @@ public struct TCalendarRepresentable: UIViewRepresentable { uiView.scope = targetScope } + DispatchQueue.main.async { + uiView.bounds.size.height = self.calendarHeight + uiView.frame.size.height = self.calendarHeight + } + uiView.reloadData() } } @@ -103,6 +114,16 @@ public extension TCalendarRepresentable { } } + // Week/Month ๋ชจ๋“œ ์ „ํ™˜ + public func calendar(_ calendar: FSCalendar, boundingRectWillChange bounds: CGRect, animated: Bool) { + DispatchQueue.main.async { + calendar.bounds.size.height = bounds.height + calendar.frame.size.height = bounds.height + calendar.setNeedsLayout() + calendar.layoutIfNeeded() + } + } + // ์บ˜๋ฆฐ๋” ์…€ ์ฃผ์ž… public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { diff --git a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift index 6d8ffd60..c41c71e8 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift @@ -12,12 +12,18 @@ import SwiftUI /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. public struct TCalendarView: View { + /// ์ฃผ๊ฐ„ ์บ˜๋ฆฐ๋” ๋†’์ด + static let weeklyCalendarHeight: CGFloat = 80 + /// ์›”๊ฐ„ ์บ˜๋ฆฐ๋” ๋†’์ด + static let monthlyCalendarHeight: CGFloat = 340 /// ์„ ํƒํ•œ ๋‚ ์งœ @Binding var selectedDate: Date /// ํ˜„์žฌ ํŽ˜์ด์ง€ @Binding var currentPage: Date /// ์—…๋ฐ์ดํŠธ ํ”Œ๋ž˜๊ทธ @State private var forceUpdate: UUID = UUID() + /// ์บ˜๋ฆฐ๋” ๋†’์ด + @State private var calendarHeight: CGFloat = monthlyCalendarHeight /// ์ฃผ๊ฐ„/์›”๊ฐ„ ํ‘œ์‹œ ์—ฌ๋ถ€ private var isWeekMode: Bool /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ์ด๋ฒคํŠธ ๋”•์…”๋„ˆ๋ฆฌ @@ -38,15 +44,31 @@ public struct TCalendarView: View { } public var body: some View { - TCalendarRepresentable( - selectedDate: $selectedDate, - currentPage: $currentPage, - isWeekMode: isWeekMode, - events: events - ) - .id(forceUpdate) - .onChange(of: events) { - forceUpdate = UUID() + GeometryReader { proxy in + TCalendarRepresentable( + selectedDate: $selectedDate, + currentPage: $currentPage, + calendarHeight: $calendarHeight, + isWeekMode: isWeekMode, + events: events + ) + .frame(width: proxy.size.width, height: TCalendarView.monthlyCalendarHeight) + .id(forceUpdate) + .onChange(of: events) { + forceUpdate = UUID() + } + .onAppear { + calendarHeight = isWeekMode + ? TCalendarView.weeklyCalendarHeight + : TCalendarView.monthlyCalendarHeight + } + .onChange(of: isWeekMode) { + calendarHeight = isWeekMode + ? TCalendarView.weeklyCalendarHeight + : TCalendarView.monthlyCalendarHeight + } } + .frame(height: calendarHeight) + .clipped() } } From 538f8a30275d30f99cb3efe501fdab4b1acf1c9b Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:39:45 +0900 Subject: [PATCH 14/16] =?UTF-8?q?[Feat]=20TraineeHomeView=20UI=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Trainee/TraineeHomeView.swift | 181 +++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift index bad6cce0..896d42d3 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift @@ -6,4 +6,183 @@ // Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. // -import Foundation +import SwiftUI +import ComposableArchitecture + +import Domain +import DesignSystem + +/// ํŠธ๋ ˆ์ด๋‹ˆ์˜ ๋ฉ”์ธ ํ™ˆ ๋ทฐ์ž…๋‹ˆ๋‹ค +public struct TraineeHomeView: View { + + // MARK: ์ž„์‹œ State + @State var ispresented: Bool = false + @State var selectedDate: Date = Date() + @State var currentPage: Date = Date() + @State var events: [Date: Int] = [:] + @State var todaysSessionInfo: WorkoutListItemEntity? = .init(currentCount: 8, startDate: .now, endDate: .now, trainerProfileImageUrl: nil, trainerName: "๊น€๋ฏผ์ˆ˜", hasRecord: true) + @State var records: [RecordListItemEntity] = [ + .init(type: .meal(type: .lunch), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: true, imageUrl: nil), + .init(type: .meal(type: .dinner), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: false, imageUrl: "https://images.genius.com/8e0b15e4847f8e59db7dfda22b4db4ec.1000x1000x1.png"), + .init(type: .meal(type: .morning), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: true, imageUrl: nil) + ] + @State var toggleMode: Bool = true + + + public init() {} + + public var body: some View { + ScrollView { + VStack(spacing: 0) { + CalendarSection() + + RecordListSection() + .background(Color.neutral100) + + Spacer() + } + } + .overlay(alignment: .bottomTrailing) { + Button(action: { + // TODO: STORE + ispresented = true + }, label: { + Image(.icnPlus) + .renderingMode(.template) + .resizable() + .tint(Color.common0) + .frame(width: 24, height: 24) + .padding(16) + .background(Color.neutral900) + .clipShape(.rect(cornerRadius: 16)) + }) + .padding(.bottom, 20) + .padding(.trailing, 12) + } + .navigationBarBackButtonHidden() + .sheet(isPresented: $ispresented) { + TraineeRecordStartView(itemContents: [ + ("๐Ÿ‹๐Ÿปโ€โ™€๏ธ", "๊ฐœ์ธ ์šด๋™", { + // TODO: Store ์—ฐ๊ฒฐ + print("pop") + }), + ("๐Ÿฅ—", "์‹๋‹จ", { + // TODO: Store ์—ฐ๊ฒฐ + print("pop") + }) + ]) + .autoSizingBottomSheet() + } + } + + // MARK: - Sections + @ViewBuilder + private func CalendarSection() -> some View { + VStack(spacing: 16) { + TCalendarHeader( + currentPage: $currentPage, + formatter: { TDateFormatUtility.formatter(for: .yyyy๋…„_MM์›”).string(from: $0) }, + rightView: { + Button(action: { + // TODO: Store ์—ฐ๊ฒฐ + print("pop") + toggleMode.toggle() + }, label: { + Image(.icnAlarm) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + }) + } + ) + + // Calendar + ๊ธˆ์ผ ์ˆ˜์—… ์นด๋“œ + VStack(spacing: 12) { + TCalendarView( + selectedDate: $selectedDate, + currentPage: $currentPage, + events: events, + isWeekMode: toggleMode + ) + .padding(.horizontal, 20) + + if let todaysSessionInfo { + TWorkoutCard( + chipUIInfo: RecordType.session(count: todaysSessionInfo.currentCount).chipInfo, + timeText: "\(TDateFormatUtility.formatter(for: .a_HHmm).string(from: todaysSessionInfo.startDate)) ~ \(TDateFormatUtility.formatter(for: .a_HHmm).string(from: todaysSessionInfo.endDate))", + title: "\(todaysSessionInfo.trainerName) ํŠธ๋ ˆ์ด๋„ˆ", + imgURL: .init(string: todaysSessionInfo.trainerProfileImageUrl ?? ""), + hasRecord: todaysSessionInfo.hasRecord, + footerTapAction: { + // TODO: STORe + print("์–ใ…‚์‚") + } + ) + .padding(.horizontal, 20) + .padding(.bottom, 16) + } else { + SessionEmptyView() + .padding(.horizontal, 20) + .padding(.vertical, 16) + } + } + + } + .padding(.vertical, 12) + + } + + @ViewBuilder + private func RecordListSection() -> some View { + VStack(alignment: .leading, spacing: 0) { + Text(TDateFormatUtility.formatter(for: .MM์›”_dd์ผ_EEEE).string(from: selectedDate)) + .typographyStyle(.heading3, with: .neutral800) + .padding(20) + + VStack(spacing: 12) { + ForEach(records.indices, id: \.self) { index in + let item = records[index] + TRecordCard( + chipUIInfo: item.type.chipInfo, + timeText: TDateFormatUtility.formatter(for: .a_HHmm).string(from: item.date), + title: "์ž๊ณ  ์‹ถ์–ด์š” ์ง„์งœ๋กœ ใ„ดใ…‡ใ…ใ„นใ…ใ„ดใ…‡๋ž˜ใ…ฃใ…‘ใ…•ใ…—ใ…ใ…ˆใ„ท;ใ…ใ…“ใ…‘ใ…—ใ…ใ…ˆใ„ท๋ž˜ใ…‘;ใ…—ใ…“ใ…ใ„ทใ„นใ…ˆ;ใ…ใ…—ใ…•ใ…‘ใ„ทใ„นใ…ใ…ˆ", + imgURL: URL(string: item.imageUrl ?? ""), + hasFeedback: item.hasFeedBack, + footerTapAction: { + // TODO: STORE + print("pop\(index)") + } + ) + } + } + .padding(.horizontal, 16) + } + } +} + +private extension TraineeHomeView { + /// ์˜ˆ์ •๋œ ์ˆ˜์—…์ด ์—†์–ด์š” + struct SessionEmptyView: View { + var body: some View { + Text("์˜ˆ์ •๋œ ์ˆ˜์—…์ด ์—†์–ด์š”") + .typographyStyle(.label1Medium, with: .neutral400) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + } + } + + /// ์•„์ง ๊ธฐ๋ก์ด ์—†์–ด์š” + struct RecordEmptyView: View { + var body: some View { + VStack(spacing: 4) { + Text("์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์‹์‚ฌ์™€ ์šด๋™์„ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”") + .typographyStyle(.body2Bold, with: .neutral600) + .frame(maxWidth: .infinity) + + Text("์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์‹์‚ฌ์™€ ์šด๋™์„ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”") + .typographyStyle(.label1Medium, with: .neutral400) + .frame(maxWidth: .infinity) + } + } + } +} From 0a87542638c220805b60997452eefcef081aaa5d Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 3 Feb 2025 01:50:14 +0900 Subject: [PATCH 15/16] =?UTF-8?q?[Feat]=20TraineeHomeFeature=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Entity/RecordListItemEntity.swift | 6 +- .../Domain/Sources/Entity/RecordType.swift | 2 +- .../Entity/WorkoutListItemEntity.swift | 6 +- .../Sources/Extension/CGFloat+.swift | 31 ++++ .../Home/Trainee/TraineeHomeFeature.swift | 132 ++++++++++++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 TnT/Projects/Presentation/Sources/Extension/CGFloat+.swift create mode 100644 TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift diff --git a/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift index 2f9f0ddf..e3a76870 100644 --- a/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift @@ -9,7 +9,9 @@ import Foundation /// ํŠธ๋ ˆ์ด๋‹ˆ ๊ธฐ๋ก ๋ชฉ๋ก ์•„์ดํ…œ ๋ชจ๋ธ -public struct RecordListItemEntity { +public struct RecordListItemEntity: Equatable { + /// ๊ธฐ๋ก id + public let id: Int /// ๊ธฐ๋ก ํƒ€์ž… public let type: RecordType /// ๊ธฐ๋ก ์‹œ๊ฐ„ @@ -22,12 +24,14 @@ public struct RecordListItemEntity { public let imageUrl: String? public init( + id: Int, type: RecordType, date: Date, title: String, hasFeedBack: Bool, imageUrl: String? ) { + self.id = id self.type = type self.date = date self.title = title diff --git a/TnT/Projects/Domain/Sources/Entity/RecordType.swift b/TnT/Projects/Domain/Sources/Entity/RecordType.swift index ed0c1a46..bf052fe3 100644 --- a/TnT/Projects/Domain/Sources/Entity/RecordType.swift +++ b/TnT/Projects/Domain/Sources/Entity/RecordType.swift @@ -7,7 +7,7 @@ // /// ์•ฑ์—์„œ ์กด์žฌํ•˜๋Š” ๊ธฐ๋ก ์œ ํ˜•์„ ์ •์˜ํ•œ ์—ด๊ฑฐํ˜• -public enum RecordType: Sendable { +public enum RecordType: Sendable, Equatable { /// nํšŒ์ฐจ ์ˆ˜์—… case session(count: Int) /// ์šด๋™ diff --git a/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift index 17172907..d9d93d76 100644 --- a/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift +++ b/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift @@ -9,7 +9,9 @@ import Foundation /// ํŠธ๋ ˆ์ด๋‹ˆ PT ์šด๋™ ๋ชฉ๋ก ์•„์ดํ…œ ๋ชจ๋ธ -public struct WorkoutListItemEntity { +public struct WorkoutListItemEntity: Equatable { + /// ์ˆ˜์—… Id + public let id: Int /// ํ˜„์žฌ ์ˆ˜์—… ์ฐจ์ˆ˜ public let currentCount: Int /// ์ˆ˜์—… ์‹œ์ž‘ ์‹œ๊ฐ„ @@ -24,6 +26,7 @@ public struct WorkoutListItemEntity { public let hasRecord: Bool public init( + id: Int, currentCount: Int, startDate: Date, endDate: Date, @@ -31,6 +34,7 @@ public struct WorkoutListItemEntity { trainerName: String, hasRecord: Bool ) { + self.id = id self.currentCount = currentCount self.startDate = startDate self.endDate = endDate diff --git a/TnT/Projects/Presentation/Sources/Extension/CGFloat+.swift b/TnT/Projects/Presentation/Sources/Extension/CGFloat+.swift new file mode 100644 index 00000000..f998ab28 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Extension/CGFloat+.swift @@ -0,0 +1,31 @@ +// +// CGFloat+.swift +// Presentation +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/3/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI + +public extension CGFloat { + /// ์ƒ๋‹จ safeArea + static var safeAreaTop: CGFloat { + guard let window = UIApplication.shared.connectedScenes + .compactMap({ ($0 as? UIWindowScene)?.keyWindow }) + .first else { return 44 } + + let topInset = window.safeAreaInsets.top + return topInset > 0 ? topInset : 0 + } + + /// ํ•˜๋‹จ safeArea + static var safeAreaBottom: CGFloat { + guard let window = UIApplication.shared.connectedScenes + .compactMap({ ($0 as? UIWindowScene)?.keyWindow }) + .first else { return 34 } + + let bottomInset = window.safeAreaInsets.bottom + return bottomInset > 0 ? bottomInset : 34 + } +} diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift new file mode 100644 index 00000000..08e603b7 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeFeature.swift @@ -0,0 +1,132 @@ +// +// TraineeHomeFeature.swift +// Presentation +// +// Created by ๋ฐ•๋ฏผ์„œ on 2/2/25. +// Copyright ยฉ 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation +import ComposableArchitecture + +import Domain + +@Reducer +public struct TraineeHomeFeature { + + @ObservableState + public struct State: Equatable { + // MARK: Data related state + /// ์„ ํƒ๋œ ๋‚ ์งœ + var selectedDate: Date + /// ์บ˜๋ฆฐ๋” ์ด๋ฒคํŠธ + var events: [Date: Int] + /// ์ˆ˜์—… ์ •๋ณด + var sessionInfo: WorkoutListItemEntity? + /// ๊ธฐ๋ก ์ •๋ณด ๋ชฉ๋ก + var records: [RecordListItemEntity] + + // MARK: UI related state + /// ์บ˜๋ฆฐ๋” ํ‘œ์‹œ ํŽ˜์ด์ง€ + var view_currentPage: Date + /// ์ˆ˜์—… ์นด๋“œ ์‹œ๊ฐ„ ํ‘œ์‹œ + var view_sessionCardTimeString: String { + guard let sessionInfo else { return "" } + return "\(TDateFormatUtility.formatter(for: .a_HHmm).string(from: sessionInfo.startDate)) ~ \(TDateFormatUtility.formatter(for: .a_HHmm).string(from: sessionInfo.endDate))" + } + /// ๊ธฐ๋ก ์ œ๋ชฉ ํ‘œ์‹œ + var view_recordTitleString: String { + return TDateFormatUtility.formatter(for: .MM์›”_dd์ผ_EEEE).string(from: selectedDate) + } + /// ์„ ํƒ ๋ฐ”ํ…€ ์‹œํŠธ ํ‘œ์‹œ + var view_isBottomSheetPresented: Bool + + public init( + selectedDate: Date = .now, + events: [Date: Int] = [:], + sessionInfo: WorkoutListItemEntity? = nil, + records: [RecordListItemEntity] = [], + view_currentPage: Date = .now, + view_isBottomSheetPresented: Bool = false + ) { + self.selectedDate = selectedDate + self.events = events + self.sessionInfo = sessionInfo + self.records = records + self.view_currentPage = view_currentPage + self.view_isBottomSheetPresented = view_isBottomSheetPresented + } + } + + @Dependency(\.traineeUseCase) private var traineeUseCase: TraineeUseCase + + public enum Action: Sendable, ViewAction { + /// ๋ทฐ์—์„œ ๋ฐœ์ƒํ•œ ์•ก์…˜์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + case view(View) + /// ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฌ๋ถ€ ์„ค์ • + case setNavigating + + @CasePathable + public enum View: Sendable, BindableAction { + /// ๋ฐ”์ธ๋”ฉํ•  ์•ก์…˜์„ ์ฒ˜๋ฆฌ + case binding(BindingAction) + /// ์šฐ์ธก ์ƒ๋‹จ ์•Œ๋ฆผ ํŽ˜์ด์ง€ ๋ณด๊ธฐ ๋ฒ„ํŠผ ํƒญ + case tapAlarmPageButton + /// ์ƒ๋‹จ ์ˆ˜์—… ๊ธฐ๋ก ๋ณด๊ธฐ ๋ฒ„ํŠผ ํƒญ + case tapShowSessionRecordButton(id: Int) + /// ๊ธฐ๋ก ๋ชฉ๋ก ํ”ผ๋“œ๋ฐฑ ๋ณด๊ธฐ ๋ฒ„ํŠผ ํƒญ + case tapShowRecordFeedbackButton(id: Int) + /// ์šฐ์ธก ํ•˜๋‹จ ๊ธฐ๋ก ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํƒญ + case tapAddRecordButton + /// ๊ฐœ์ธ ์šด๋™ ๊ธฐ๋ก ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํƒญ + case tapAddWorkoutRecordButton + /// ์‹๋‹จ ๊ธฐ๋ก ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํƒญ + case tapAddMealRecordButton + } + } + + public init() {} + + public var body: some ReducerOf { + BindingReducer(action: \.view) + + Reduce { state, action in + switch action { + + case .view(let action): + switch action { + case .binding(\.selectedDate): + print(state.events[state.selectedDate]) + return .none + case .binding: + return .none + case .tapAlarmPageButton: + // TODO: ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ ์‹œ ์ถ”๊ฐ€ + print("tapAlarmPageButton") + return .none + case .tapShowSessionRecordButton(let id): + // TODO: ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ ์‹œ ์ถ”๊ฐ€ + print("tapShowSessionRecordButton \(id)") + return .none + case .tapShowRecordFeedbackButton(let id): + // TODO: ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ ์‹œ ์ถ”๊ฐ€ + print("tapShowRecordFeedbackButton \(id)") + return .none + case .tapAddRecordButton: + state.view_isBottomSheetPresented = true + return .none + case .tapAddWorkoutRecordButton: + // TODO: ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ ์‹œ ์ถ”๊ฐ€ + print("tapAddWorkoutRecordButton") + return .none + case .tapAddMealRecordButton: + // TODO: ๋„ค๋น„๊ฒŒ์ด์…˜ ์—ฐ๊ฒฐ ์‹œ ์ถ”๊ฐ€ + print("tapAddMealRecordButton") + return .none + } + case .setNavigating: + return .none + } + } + } +} From e27737ce9e8df3a7abe7e46ba43d0427c1c29b83 Mon Sep 17 00:00:00 2001 From: Minseo Park <125115284+FpRaArNkK@users.noreply.github.com> Date: Mon, 3 Feb 2025 01:55:00 +0900 Subject: [PATCH 16/16] =?UTF-8?q?[Feat]=20TraineeHomeView=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Size=16, Type=clock.svg | 4 +- .../Contents.json | 0 .../Size=16, Type=star.svg | 2 +- .../icn_star_smile.imageset/Contents.json | 11 +- .../Size=24, Type=smile.svg | 8 +- .../Sources/Components/Card/TRecordCard.swift | 2 +- .../Components/Card/TWorkoutCard.swift | 2 +- .../DesignSystem/Image+DesignSystem.swift | 2 +- .../Home/Trainee/TraineeHomeView.swift | 116 +++++++++--------- 9 files changed, 68 insertions(+), 79 deletions(-) rename TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/{icn_start_empty.imageset => icn_star.imageset}/Contents.json (100%) rename TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/{icn_start_empty.imageset => icn_star.imageset}/Size=16, Type=star.svg (98%) diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_clock.imageset/Size=16, Type=clock.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_clock.imageset/Size=16, Type=clock.svg index 25a289bd..c9d9bce5 100644 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_clock.imageset/Size=16, Type=clock.svg +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_clock.imageset/Size=16, Type=clock.svg @@ -1,7 +1,7 @@ - - + + diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_start_empty.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star.imageset/Contents.json similarity index 100% rename from TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_start_empty.imageset/Contents.json rename to TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star.imageset/Contents.json diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_start_empty.imageset/Size=16, Type=star.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star.imageset/Size=16, Type=star.svg similarity index 98% rename from TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_start_empty.imageset/Size=16, Type=star.svg rename to TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star.imageset/Size=16, Type=star.svg index 6a817248..131d69da 100644 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_start_empty.imageset/Size=16, Type=star.svg +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star.imageset/Size=16, Type=star.svg @@ -1,6 +1,6 @@ - + diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Contents.json b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Contents.json index 9c542589..bc24cf7d 100644 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Contents.json +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "Size=24, Type=smile.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Size=24, Type=smile.svg b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Size=24, Type=smile.svg index c2a8eb55..8e3849de 100644 --- a/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Size=24, Type=smile.svg +++ b/TnT/Projects/DesignSystem/Resources/Assets.xcassets/Icons/icn_star_smile.imageset/Size=24, Type=smile.svg @@ -1,9 +1,9 @@ - - - - + + + + diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift index eccb3fa0..597f7fc9 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift @@ -76,8 +76,8 @@ public struct TRecordCard: View { case .success(let image): image .resizable() + .aspectRatio(contentMode: .fill) .frame(width: 140, height: 140) - .scaledToFill() .clipShape(.rect(cornerRadius: 16)) .padding(.leading, 12) .padding(.vertical, 12) diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift index 09b08c63..6df3d13c 100644 --- a/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift +++ b/TnT/Projects/DesignSystem/Sources/Components/Card/TWorkoutCard.swift @@ -117,8 +117,8 @@ private extension TWorkoutCard { case .success(let image): image .resizable() + .aspectRatio(contentMode: .fill) .frame(width: 24, height: 24) - .scaledToFill() .clipShape(Circle()) case .failure(let error): diff --git a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift index 293f3ab6..97d6c646 100644 --- a/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift +++ b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift @@ -44,7 +44,7 @@ public extension ImageResource { static let icnTriangleDown: ImageResource = DesignSystemAsset.icnTriangleDown.imageResource static let icnTriangleRight: ImageResource = DesignSystemAsset.icnTriangleRight.imageResource static let icnClock: ImageResource = DesignSystemAsset.icnClock.imageResource - static let icnStar: ImageResource = DesignSystemAsset.icnStartEmpty.imageResource + static let icnStar: ImageResource = DesignSystemAsset.icnStar.imageResource static let icnStarSmile: ImageResource = DesignSystemAsset.icnStarSmile.imageResource static let icnWriteBlackFilled: ImageResource = DesignSystemAsset.icnWriteBlackFilled.imageResource static let icnPlus: ImageResource = DesignSystemAsset.icnPlus.imageResource diff --git a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift index 896d42d3..9ebbf554 100644 --- a/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift @@ -13,39 +13,37 @@ import Domain import DesignSystem /// ํŠธ๋ ˆ์ด๋‹ˆ์˜ ๋ฉ”์ธ ํ™ˆ ๋ทฐ์ž…๋‹ˆ๋‹ค +@ViewAction(for: TraineeHomeFeature.self) public struct TraineeHomeView: View { - // MARK: ์ž„์‹œ State - @State var ispresented: Bool = false - @State var selectedDate: Date = Date() - @State var currentPage: Date = Date() - @State var events: [Date: Int] = [:] - @State var todaysSessionInfo: WorkoutListItemEntity? = .init(currentCount: 8, startDate: .now, endDate: .now, trainerProfileImageUrl: nil, trainerName: "๊น€๋ฏผ์ˆ˜", hasRecord: true) - @State var records: [RecordListItemEntity] = [ - .init(type: .meal(type: .lunch), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: true, imageUrl: nil), - .init(type: .meal(type: .dinner), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: false, imageUrl: "https://images.genius.com/8e0b15e4847f8e59db7dfda22b4db4ec.1000x1000x1.png"), - .init(type: .meal(type: .morning), date: .now, title: "์ž๊ณ ์‹ถ๋‹ค", hasFeedBack: true, imageUrl: nil) - ] - @State var toggleMode: Bool = true + @Bindable public var store: StoreOf - - public init() {} + public init(store: StoreOf) { + self.store = store + } public var body: some View { ScrollView { VStack(spacing: 0) { CalendarSection() + .background(Color.common0) RecordListSection() + .frame(maxWidth: .infinity) .background(Color.neutral100) - + Spacer() } } + .background( + VStack { + Color.common0 + Color.neutral100 + } + ) .overlay(alignment: .bottomTrailing) { Button(action: { - // TODO: STORE - ispresented = true + send(.tapAddRecordButton) }, label: { Image(.icnPlus) .renderingMode(.template) @@ -60,16 +58,10 @@ public struct TraineeHomeView: View { .padding(.trailing, 12) } .navigationBarBackButtonHidden() - .sheet(isPresented: $ispresented) { + .sheet(isPresented: $store.view_isBottomSheetPresented) { TraineeRecordStartView(itemContents: [ - ("๐Ÿ‹๐Ÿปโ€โ™€๏ธ", "๊ฐœ์ธ ์šด๋™", { - // TODO: Store ์—ฐ๊ฒฐ - print("pop") - }), - ("๐Ÿฅ—", "์‹๋‹จ", { - // TODO: Store ์—ฐ๊ฒฐ - print("pop") - }) + ("๐Ÿ‹๐Ÿปโ€โ™€๏ธ", "๊ฐœ์ธ ์šด๋™", { send(.tapAddWorkoutRecordButton) }), + ("๐Ÿฅ—", "์‹๋‹จ", { send(.tapAddMealRecordButton) }) ]) .autoSizingBottomSheet() } @@ -80,13 +72,11 @@ public struct TraineeHomeView: View { private func CalendarSection() -> some View { VStack(spacing: 16) { TCalendarHeader( - currentPage: $currentPage, + currentPage: $store.view_currentPage, formatter: { TDateFormatUtility.formatter(for: .yyyy๋…„_MM์›”).string(from: $0) }, rightView: { Button(action: { - // TODO: Store ์—ฐ๊ฒฐ - print("pop") - toggleMode.toggle() + send(.tapAlarmPageButton) }, label: { Image(.icnAlarm) .resizable() @@ -99,23 +89,22 @@ public struct TraineeHomeView: View { // Calendar + ๊ธˆ์ผ ์ˆ˜์—… ์นด๋“œ VStack(spacing: 12) { TCalendarView( - selectedDate: $selectedDate, - currentPage: $currentPage, - events: events, - isWeekMode: toggleMode + selectedDate: $store.selectedDate, + currentPage: $store.view_currentPage, + events: store.events, + isWeekMode: true ) .padding(.horizontal, 20) - if let todaysSessionInfo { + if let sessionInfo = store.sessionInfo { TWorkoutCard( - chipUIInfo: RecordType.session(count: todaysSessionInfo.currentCount).chipInfo, - timeText: "\(TDateFormatUtility.formatter(for: .a_HHmm).string(from: todaysSessionInfo.startDate)) ~ \(TDateFormatUtility.formatter(for: .a_HHmm).string(from: todaysSessionInfo.endDate))", - title: "\(todaysSessionInfo.trainerName) ํŠธ๋ ˆ์ด๋„ˆ", - imgURL: .init(string: todaysSessionInfo.trainerProfileImageUrl ?? ""), - hasRecord: todaysSessionInfo.hasRecord, + chipUIInfo: RecordType.session(count: sessionInfo.currentCount).chipInfo, + timeText: store.view_sessionCardTimeString, + title: "\(sessionInfo.trainerName) ํŠธ๋ ˆ์ด๋„ˆ", + imgURL: .init(string: sessionInfo.trainerProfileImageUrl ?? ""), + hasRecord: sessionInfo.hasRecord, footerTapAction: { - // TODO: STORe - print("์–ใ…‚์‚") + send(.tapShowSessionRecordButton(id: sessionInfo.id)) } ) .padding(.horizontal, 20) @@ -135,27 +124,36 @@ public struct TraineeHomeView: View { @ViewBuilder private func RecordListSection() -> some View { VStack(alignment: .leading, spacing: 0) { - Text(TDateFormatUtility.formatter(for: .MM์›”_dd์ผ_EEEE).string(from: selectedDate)) - .typographyStyle(.heading3, with: .neutral800) - .padding(20) + HStack { + Text(store.view_recordTitleString) + .typographyStyle(.heading3, with: .neutral800) + .padding(20) + Spacer() + } VStack(spacing: 12) { - ForEach(records.indices, id: \.self) { index in - let item = records[index] - TRecordCard( - chipUIInfo: item.type.chipInfo, - timeText: TDateFormatUtility.formatter(for: .a_HHmm).string(from: item.date), - title: "์ž๊ณ  ์‹ถ์–ด์š” ์ง„์งœ๋กœ ใ„ดใ…‡ใ…ใ„นใ…ใ„ดใ…‡๋ž˜ใ…ฃใ…‘ใ…•ใ…—ใ…ใ…ˆใ„ท;ใ…ใ…“ใ…‘ใ…—ใ…ใ…ˆใ„ท๋ž˜ใ…‘;ใ…—ใ…“ใ…ใ„ทใ„นใ…ˆ;ใ…ใ…—ใ…•ใ…‘ใ„ทใ„นใ…ใ…ˆ", - imgURL: URL(string: item.imageUrl ?? ""), - hasFeedback: item.hasFeedBack, - footerTapAction: { - // TODO: STORE - print("pop\(index)") - } - ) + if !store.records.isEmpty { + ForEach(store.records, id: \.id) { item in + TRecordCard( + chipUIInfo: item.type.chipInfo, + timeText: TDateFormatUtility.formatter(for: .a_HHmm).string(from: item.date), + title: item.title, + imgURL: URL(string: item.imageUrl ?? ""), + hasFeedback: item.hasFeedBack, + footerTapAction: { + send(.tapShowRecordFeedbackButton(id: item.id)) + } + ) + } + } else { + RecordEmptyView() + .padding(.top, 80) + .padding(.bottom, 100) } } .padding(.horizontal, 16) + + Spacer() } } } @@ -175,7 +173,7 @@ private extension TraineeHomeView { struct RecordEmptyView: View { var body: some View { VStack(spacing: 4) { - Text("์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์‹์‚ฌ์™€ ์šด๋™์„ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”") + Text("์•„์ง ๊ธฐ๋ก์ด ์—†์–ด์š”") .typographyStyle(.body2Bold, with: .neutral600) .frame(maxWidth: .infinity)