-
Notifications
You must be signed in to change notification settings - Fork 0
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
[Feat/NST-51] #25 Schedule picker 컴포넌트 작성 #29
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// | ||
// SchedulePicker.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 오연서 on 1/10/25. | ||
// | ||
|
||
import UIKit | ||
|
||
final class SchedulePicker: UICollectionView { | ||
enum Mode { | ||
case editMode | ||
case readMode | ||
} | ||
|
||
private let layout = SchedulePickerLayout() | ||
private let timeHeaders: [String] | ||
private let dateHeaders: [String] | ||
private let mode: Mode | ||
private var selectedCells: Set<IndexPath> = [] // edit Mode | ||
private var cellAvailability: [IndexPath: Int] = [:] // read Mode | ||
|
||
init(timeHeaders: [String], dateHeaders: [String], mode: Mode) { | ||
self.timeHeaders = timeHeaders | ||
self.dateHeaders = dateHeaders | ||
self.mode = mode | ||
super.init(frame: .zero, collectionViewLayout: layout) | ||
self.layout.configure(totalRows: timeHeaders.count + 1, totalColumns: dateHeaders.count + 1) | ||
self.register(SchedulePickerCell.self, forCellWithReuseIdentifier: SchedulePickerCell.identifier) | ||
self.cellAvailability = calculateCellAvailability(totalRows: timeHeaders.count + 1, | ||
totalColumns: dateHeaders.count + 1, | ||
//Fix: mockMemberStartTime 변경 필요 | ||
startTimes: mockMemberStartTime) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 캬 야무지네요 다만 init 아래의 내용은 팀 코드 컨벤션대로 따로 셋업 함수에서 처리해주세요! |
||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
func addSelectedCell(at indexPath: IndexPath) { | ||
guard mode == .editMode else { return } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예외 처리 뭐죠 너 오연서 아니지 |
||
if let cell = cellForItem(at: indexPath) as? SchedulePickerCell { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위에서 guard문으로 체크하는 김에 같이 작성해서 |
||
cell.isSelectedCell.toggle() | ||
if cell.isSelectedCell { | ||
selectedCells.insert(indexPath) | ||
} else { | ||
selectedCells.remove(indexPath) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 너무 절차형같아요 반응형으로 리팩토링하는 고민을 해보는건 어떨까요? 현재 코드
반응형이라면
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 모르게쒀요... 이부분 말고도 전체적인 흐름을 반응형으로 바꾸기 위해서 selectedCells와 cellAvailability를 behaviorRelay로 변경하는게 나을까요 |
||
} | ||
} | ||
|
||
func configureCellBackground(_ cell: SchedulePickerCell, for indexPath: IndexPath, participants: Int) { | ||
guard mode == .readMode else { return } | ||
let count = cellAvailability[indexPath, default: 0] | ||
let ratio = Float(count) / Float(participants) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 ratio는 컬러 관련 기능명세와 동일한 내용인가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동일합니당 |
||
|
||
switch ratio { | ||
case 0.01...0.2: | ||
cell.backgroundColor = .appBlue50 | ||
case 0.2...0.4: | ||
cell.backgroundColor = .appBlue200 | ||
case 0.4...0.6: | ||
cell.backgroundColor = .appBlue400 | ||
case 0.6...0.8: | ||
cell.backgroundColor = .appBlue700 | ||
case 0.8...1: | ||
cell.backgroundColor = .appBlue800 | ||
default: | ||
cell.backgroundColor = .clear | ||
} | ||
} | ||
} | ||
|
||
/// .ReadMode 색상 반환 로직 | ||
extension SchedulePicker { | ||
///각 시간에 대한 가능 인원 계산 | ||
private func calculateCellAvailability(totalRows: Int, totalColumns: Int, startTimes: [String]) -> [IndexPath: Int] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 접근제어자 진짜 뭐냐 너 오연서 아니지 |
||
var cellAvailability: [IndexPath: Int] = [:] | ||
let dateTimeMapping = createDateTimeMapping(totalRows: totalRows, totalColumns: totalColumns) | ||
for startTime in startTimes { | ||
if let indexPath = dateTimeMapping[startTime] { | ||
cellAvailability[indexPath, default: 0] += 1 | ||
} | ||
} | ||
return cellAvailability | ||
} | ||
|
||
/// 시각 - cell 매핑 | ||
private func createDateTimeMapping(totalRows: Int, totalColumns: Int) -> [String: IndexPath] { | ||
var mapping: [String: IndexPath] = [:] | ||
//FIX: mockDateList 변경 필요 | ||
let dates = mockDateList.map { String($0.prefix(10)) } | ||
|
||
for row in 1..<totalRows { | ||
for column in 1..<totalColumns { | ||
let time = self.timeHeaders[row - 1] // 시간 | ||
guard column - 1 < dates.count else { continue } | ||
let date = dates[column - 1] // 날짜 | ||
let combinedKey = "\(date)T\(time):00:00" | ||
let indexPath = IndexPath(item: (row * totalColumns) + column, section: 0) | ||
mapping[combinedKey] = indexPath | ||
} | ||
} | ||
return mapping | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 캬 머리싸맨 흔적이 보여서 좋네오 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 실버 탈출기 😅😅 |
||
|
||
func reloadCellBackgrounds() { | ||
self.cellAvailability = calculateCellAvailability(totalRows: self.timeHeaders.count + 1, | ||
totalColumns: self.dateHeaders.count + 1, | ||
startTimes: mockMemberStartTime) | ||
self.reloadData() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에 접근제어가 빠진 이유가 있나요?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. editmode <-> readmode 전환되는 로직이 있어서 뺐어용 |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// | ||
// SchedulePickerCell.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 오연서 on 1/10/25. | ||
// | ||
|
||
import UIKit | ||
|
||
final class SchedulePickerCell: UICollectionViewCell { | ||
|
||
static let identifier = "SchedulePickerCell" | ||
|
||
private var textLabel = UILabel() | ||
var isSelectedCell: Bool = false { | ||
didSet { | ||
backgroundColor = isSelectedCell ? .appBlue400 : .appWhite | ||
} | ||
} | ||
Comment on lines
+15
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rx 말고 didSet 사용한 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굳이 rx로 안해도될거같아서 .... didSet을 사용했는데 딱히 이유는 없어요 😅😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋ 똑같습니다 굳이 rx로 안해도 돼서라고 생각해요! |
||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupCell() | ||
setUpHierarchy() | ||
setUpUI() | ||
setUpLayout() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setUpHierarchy() { | ||
self.addSubview(textLabel) | ||
} | ||
|
||
private func setUpUI() { | ||
textLabel.do { | ||
$0.font = .PretendardStyle.c4_r.font | ||
$0.numberOfLines = 0 | ||
$0.textAlignment = .center | ||
$0.textColor = .appGray600 | ||
} | ||
} | ||
|
||
private func setUpLayout() { | ||
textLabel.snp.makeConstraints { | ||
$0.center.equalToSuperview() | ||
} | ||
} | ||
|
||
private func setupCell() { | ||
self.backgroundColor = .appWhite | ||
self.layer.borderWidth = 0.5 | ||
self.layer.borderColor = UIColor.appGray200.cgColor | ||
} | ||
|
||
func configureHeader(for indexPath: IndexPath, dateHeaders: [String], timeHeaders: [String]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드의 역할이 너무 많은것 같아요! 세부적으로 기능을 쪼개면 더 읽기 편하고 명료한 코드가 될 것 같습니다. |
||
let totalRows = timeHeaders.count + 1 | ||
let totalColumns = dateHeaders.count + 1 | ||
let row = indexPath.item / totalColumns | ||
let column = indexPath.item % totalColumns | ||
|
||
let isTopLeft = indexPath.item == 0 | ||
let isTopRight = indexPath.item == totalColumns - 1 | ||
let isBottomLeft = indexPath.item == (totalRows - 1) * totalColumns | ||
let isBottomRight = indexPath.item == totalRows * totalColumns - 1 | ||
|
||
/// dateHeader, timeHeader text binding | ||
if row == 0, column > 0 { | ||
self.textLabel.text = dateHeaders[column - 1] | ||
} else if column == 0, row > 0 { | ||
self.textLabel.text = "\(timeHeaders[row - 1])시" | ||
} else { | ||
self.textLabel.text = "" | ||
} | ||
|
||
/// 테이블 모서리 둥글게 | ||
if isTopLeft || isTopRight || isBottomLeft || isBottomRight { | ||
self.layer.cornerRadius = 10 | ||
self.layer.masksToBounds = true | ||
self.layer.borderColor = UIColor.appGray200.cgColor | ||
if isTopLeft { | ||
self.layer.maskedCorners = [.layerMinXMinYCorner] | ||
} else if isTopRight { | ||
self.layer.maskedCorners = [.layerMaxXMinYCorner] | ||
} else if isBottomLeft { | ||
self.layer.maskedCorners = [.layerMinXMaxYCorner] | ||
} else if isBottomRight { | ||
self.layer.maskedCorners = [.layerMaxXMaxYCorner] | ||
} | ||
} else { | ||
self.layer.cornerRadius = 0 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// | ||
// SchedulePickerLayout.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 오연서 on 1/10/25. | ||
// | ||
|
||
import UIKit | ||
|
||
final class SchedulePickerLayout: UICollectionViewFlowLayout { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 야무지네여 야무져 다만 해당 레이아웃은 SchedulePicker에만 해당되는 레이아웃이니 SchedulePicker extension으로 선언해서 프로퍼티임을 강조하는게 어떨까요? |
||
private let fixedFirstColumnWidth: CGFloat = 42 | ||
private let fixedFirstRowHeight: CGFloat = 36 | ||
|
||
private var totalRows: Int = 0 | ||
private var totalColumns: Int = 0 | ||
private let minimumSpacing: CGFloat = 0 | ||
|
||
func configure(totalRows: Int = 0, totalColumns: Int = 0) { | ||
self.totalRows = totalRows | ||
self.totalColumns = totalColumns | ||
invalidateLayout() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드는 어떤 코드인가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 레이아웃이 말을 안들어서 추가해놨었는데 빼도 될거같음다 |
||
} | ||
|
||
override func prepare() { | ||
super.prepare() | ||
guard let collectionView = collectionView else { return } | ||
let remainingWidth = collectionView.bounds.width - fixedFirstColumnWidth - CGFloat(totalColumns - 1) * minimumSpacing | ||
let dynamicColumnWidth = remainingWidth / CGFloat(totalColumns - 1) | ||
let dynamicRowHeight = 32.0 | ||
|
||
itemSize = CGSize(width: dynamicColumnWidth, height: dynamicRowHeight) | ||
minimumLineSpacing = 0 | ||
minimumInteritemSpacing = 0 | ||
sectionInset = .zero | ||
} | ||
|
||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { | ||
let attributes = super.layoutAttributesForElements(in: rect) | ||
attributes?.forEach { layoutAttribute in | ||
let indexPath = layoutAttribute.indexPath | ||
let column = indexPath.item % totalColumns | ||
let row = indexPath.item / totalColumns | ||
|
||
// 첫 번째 열의 너비 고정, 열 간 간격 조정 | ||
if column == 0 { | ||
layoutAttribute.frame.size.width = fixedFirstColumnWidth | ||
layoutAttribute.frame.origin.x = 0 | ||
} else { // 두 번째 열 이후 | ||
let previousColumnRight = fixedFirstColumnWidth + CGFloat(column - 1) * (itemSize.width + minimumInteritemSpacing) | ||
layoutAttribute.frame.origin.x = previousColumnRight | ||
} | ||
|
||
// 첫 번째 행의 높이 고정, 행 간 간격 조정 | ||
if indexPath.item < totalColumns { | ||
layoutAttribute.frame.size.height = fixedFirstRowHeight | ||
layoutAttribute.frame.origin.y = 0 | ||
} else { // 두 번째 행 이후 | ||
let previousRowBottom = fixedFirstRowHeight + CGFloat(row - 1) * (itemSize.height + minimumLineSpacing) | ||
layoutAttribute.frame.origin.y = previousRowBottom | ||
} | ||
} | ||
return attributes | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,13 +47,18 @@ public final class NSTDateUtility { | |
|
||
public extension NSTDateUtility { | ||
enum NSTDateFormatter { | ||
case yyyyMMddTHHmmss | ||
case yyyyMMddHHmmss | ||
case yyyyMMdd | ||
case yyyyMM | ||
case EE | ||
case HH | ||
case MMddEE | ||
|
||
var format: String { | ||
switch self { | ||
case .yyyyMMddTHHmmss: | ||
return "yyyy-MM-dd'T'HH:mm:ss" | ||
case .yyyyMMddHHmmss: | ||
return "yyyy-MM-dd HH:mm:ss" | ||
case .yyyyMMdd: | ||
|
@@ -62,6 +67,10 @@ public extension NSTDateUtility { | |
return "yyyy-MM" | ||
case .EE: | ||
return "EE" | ||
case .HH: | ||
return "HH" | ||
case .MMddEE: | ||
return "EE\nMM/dd" | ||
} | ||
} | ||
} | ||
|
@@ -77,3 +86,42 @@ public extension NSTDateUtility { | |
} | ||
} | ||
} | ||
|
||
func dateList(_ dateStrings: [String]) -> [String] { | ||
let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 | ||
let displayFormatter = NSTDateUtility(format: .MMddEE) // 출력 형식 | ||
|
||
return dateStrings.compactMap { dateString in | ||
switch formatter.date(from: dateString) { | ||
case .success(let date): | ||
return displayFormatter.string(from: date) | ||
case .failure(let error): | ||
print("Failed to parse date \(dateString): \(error.localizedDescription)") | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
func timeList(_ startTime: String, _ endTime: String) -> [String] { | ||
let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 | ||
var result: [String] = [] | ||
|
||
switch (formatter.date(from: startTime), formatter.date(from: endTime)) { | ||
case (.success(let start), .success(let end)): | ||
let calendar = Calendar.current | ||
var current = start | ||
|
||
while current <= end { | ||
result.append(NSTDateUtility(format: .HH).string(from: current)) // 출력 형식 | ||
if let nextHour = calendar.date(byAdding: .hour, value: 1, to: current) { | ||
current = nextHour | ||
} else { | ||
break | ||
} | ||
} | ||
default: | ||
print("Failed to parse start or end time.") | ||
return [] | ||
} | ||
return result | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 함수들은 NSTDateUtility 내용에 포함되면서 self로도 사용 가능하니 NSTDateUtility extension으로 두는게 어떨까요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NSTDateUtility 파일 내에 extension으로 따로 빼라는 말인거죠? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞습니다! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 TODO or FIX 사항은 해결된건가요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아뇹 ㅋㅋ 이부분은 화면 만들면서 다시 바꿔볼 예정입니다 ..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아뇨 지금 해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ㅋ