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/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/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift new file mode 100644 index 00000000..4bb84927 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarCell.swift @@ -0,0 +1,183 @@ +// +// TCalendarCell.swift +// DesignSystem +// +// Created by 박민서 on 2/1/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import FSCalendar + +/// TCalendar에 사용되는 Cell 입니다 +final class TCalendarCell: FSCalendarCell { + // MARK: Properties + static let identifier: String = "TCalendarCell" + static let cellSize: CGSize = CGSize(width: 51, height: 54) + + /// 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 + + // 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) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + 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 + + 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: 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, + isCellSelected: Bool, + eventCount: Int = 0, + isWeekMode: Bool = false + ) { + self.customDate = date + self.isCellSelected = isCellSelected + self.eventCount = eventCount + self.isWeekMode = isWeekMode + + // 현재 날짜 및 선택 상태를 반영, Style 설정 + if isCellSelected { + 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() + } +} + +extension TCalendarCell { + 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/TCalendarHeader.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift new file mode 100644 index 00000000..d3887687 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarHeader.swift @@ -0,0 +1,63 @@ +// +// 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 + private var rightView: (() -> RightView)? + + 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(spacing: 0) { + 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) + }) + } + .frame(maxWidth: .infinity) + .overlay(alignment: .trailing) { + rightView?() + } + .padding(.vertical, 8) + .padding(.horizontal, 20) + } + + 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/Components/Calendar/TCalendarRepresentable.swift b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift new file mode 100644 index 00000000..9d9beddc --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarRepresentable.swift @@ -0,0 +1,146 @@ +// +// 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 + /// 캘린더 높이 + @Binding var calendarHeight: CGFloat + /// 주간/월간 표시 여부 + private var isWeekMode: Bool + /// 캘린더 표시 이벤트 딕셔너리 + private var events: [Date: Int] + + 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 + } + + 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.weekdayHeight = 18 + calendar.rowHeight = TCalendarCell.cellSize.height + 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 + } + + DispatchQueue.main.async { + uiView.bounds.size.height = self.calendarHeight + uiView.frame.size.height = self.calendarHeight + } + + 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 + } + } + + // 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 { + + 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 new file mode 100644 index 00000000..c41c71e8 --- /dev/null +++ b/TnT/Projects/DesignSystem/Sources/Components/Calendar/TCalendarView.swift @@ -0,0 +1,74 @@ +// +// TCalendarView.swift +// DesignSystem +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +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 + /// 캘린더 표시 이벤트 딕셔너리 + private var events: [Date: Int] + + public init( + selectedDate: Binding, + currentPage: Binding, + forceUpdate: UUID = UUID(), + events: [Date: Int], + isWeekMode: Bool = false + ) { + self._selectedDate = selectedDate + self._currentPage = currentPage + self.events = events + self.forceUpdate = forceUpdate + self.isWeekMode = isWeekMode + } + + public var body: some View { + 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() + } +} diff --git a/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift b/TnT/Projects/DesignSystem/Sources/Components/Card/TRecordCard.swift index c6e2c7b5..597f7fc9 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 @@ -71,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) @@ -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() } } } 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..6df3d13c --- /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.neutral100) + .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() + .aspectRatio(contentMode: .fill) + .frame(width: 24, height: 24) + .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) + } + } + } +} 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/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift b/TnT/Projects/DesignSystem/Sources/DesignSystem/Image+DesignSystem.swift index 2ac9953e..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 @@ -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/Entity/RecordListItemEntity.swift b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift new file mode 100644 index 00000000..e3a76870 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/RecordListItemEntity.swift @@ -0,0 +1,41 @@ +// +// RecordListItemEntity.swift +// Domain +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 트레이니 기록 목록 아이템 모델 +public struct RecordListItemEntity: Equatable { + /// 기록 id + public let id: Int + /// 기록 타입 + public let type: RecordType + /// 기록 시간 + public let date: Date + /// 기록 제목 + public let title: String + /// 피드백 여부 + public let hasFeedBack: Bool + /// 기록 이미지 URL + 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 + self.hasFeedBack = hasFeedBack + self.imageUrl = imageUrl + } +} 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 new file mode 100644 index 00000000..d9d93d76 --- /dev/null +++ b/TnT/Projects/Domain/Sources/Entity/WorkoutListItemEntity.swift @@ -0,0 +1,45 @@ +// +// WorkoutListItemEntity.swift +// Domain +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import Foundation + +/// 트레이니 PT 운동 목록 아이템 모델 +public struct WorkoutListItemEntity: Equatable { + /// 수업 Id + public let id: Int + /// 현재 수업 차수 + 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( + id: Int, + currentCount: Int, + startDate: Date, + endDate: Date, + trainerProfileImageUrl: String?, + trainerName: String, + hasRecord: Bool + ) { + self.id = id + self.currentCount = currentCount + self.startDate = startDate + self.endDate = endDate + self.trainerProfileImageUrl = trainerProfileImageUrl + self.trainerName = trainerName + self.hasRecord = hasRecord + } +} diff --git a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift index e94013f2..1d32e4dd 100644 --- a/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift +++ b/TnT/Projects/Domain/Sources/Policy/TDateFormat.swift @@ -14,6 +14,8 @@ 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" @@ -22,6 +24,10 @@ public enum TDateFormat: String { case yyyyMMddDot = "yyyy.MM.dd" /// ""M월 d일" case M월_d일 = "M월 d일" + /// "01월 10일 화요일" + case MM월_dd일_EEEE = "MM월 dd일 EEEE" /// "EE" case EE = "EE" + /// "오후 17:00" (시간 포맷) + case a_HHmm = "a HH:mm" } 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 + } + } + } +} 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..9ebbf554 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeHomeView.swift @@ -0,0 +1,186 @@ +// +// TraineeHomeView.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +import Domain +import DesignSystem + +/// 트레이니의 메인 홈 뷰입니다 +@ViewAction(for: TraineeHomeFeature.self) +public struct TraineeHomeView: View { + + @Bindable public var store: StoreOf + + 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: { + send(.tapAddRecordButton) + }, 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: $store.view_isBottomSheetPresented) { + TraineeRecordStartView(itemContents: [ + ("🏋🏻‍♀️", "개인 운동", { send(.tapAddWorkoutRecordButton) }), + ("🥗", "식단", { send(.tapAddMealRecordButton) }) + ]) + .autoSizingBottomSheet() + } + } + + // MARK: - Sections + @ViewBuilder + private func CalendarSection() -> some View { + VStack(spacing: 16) { + TCalendarHeader( + currentPage: $store.view_currentPage, + formatter: { TDateFormatUtility.formatter(for: .yyyy년_MM월).string(from: $0) }, + rightView: { + Button(action: { + send(.tapAlarmPageButton) + }, label: { + Image(.icnAlarm) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + }) + } + ) + + // Calendar + 금일 수업 카드 + VStack(spacing: 12) { + TCalendarView( + selectedDate: $store.selectedDate, + currentPage: $store.view_currentPage, + events: store.events, + isWeekMode: true + ) + .padding(.horizontal, 20) + + if let sessionInfo = store.sessionInfo { + TWorkoutCard( + chipUIInfo: RecordType.session(count: sessionInfo.currentCount).chipInfo, + timeText: store.view_sessionCardTimeString, + title: "\(sessionInfo.trainerName) 트레이너", + imgURL: .init(string: sessionInfo.trainerProfileImageUrl ?? ""), + hasRecord: sessionInfo.hasRecord, + footerTapAction: { + send(.tapShowSessionRecordButton(id: sessionInfo.id)) + } + ) + .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) { + HStack { + Text(store.view_recordTitleString) + .typographyStyle(.heading3, with: .neutral800) + .padding(20) + Spacer() + } + + VStack(spacing: 12) { + 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() + } + } +} + +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) + } + } + } +} 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..63de2ab6 --- /dev/null +++ b/TnT/Projects/Presentation/Sources/Home/Trainee/TraineeRecordStartView.swift @@ -0,0 +1,69 @@ +// +// TraineeRecordStartVeiw.swift +// Presentation +// +// Created by 박민서 on 2/2/25. +// Copyright © 2025 yapp25thTeamTnT. All rights reserved. +// + +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) + }) + } + } +} 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) + } } 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 ]