Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-195] TCalendar, TraineeHome 화면 작성 #45

Merged
merged 16 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<RightView: View>: View {

@Binding private var currentPage: Date
private var formatter: (Date) -> String
private var rightView: (() -> RightView)?

public init(
currentPage: Binding<Date>,
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
}
}
}
Loading