Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ final class SchedulePicker: UICollectionView {
}

// MARK: Public Methods
func addSelectedCell(at indexPath: IndexPath) {
guard mode == .editMode,
let cell = cellForItem(at: indexPath) as? SchedulePickerCell
else { return }
cell.isSelectedCell.toggle()
if cell.isSelectedCell {
selectedCells.insert(indexPath)
} else {
selectedCells.remove(indexPath)
}
}

func configureCellBackground(_ cell: SchedulePickerCell, for indexPath: IndexPath, participants: Int) {
guard mode == .readMode else { return }
let count = cellAvailability[indexPath, default: 0]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// AvailabilityCreateReactor.swift
// Noostak_iOS
//
// Created by 오연서 on 2/26/25.
//

import UIKit
import ReactorKit

final class AvailabilityCreateReactor: Reactor {
enum Action {
case selectCell(IndexPath)
case tapConfirmAvailableTimes
}

enum Mutation {
case toggleCell(IndexPath)
case setAvailableTimes([AvailableTime])
}

struct State {
var selectedCells: Set<IndexPath> = []
var availableTimes: [AvailableTime] = []
}

let initialState = State()

func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .selectCell(let indexPath):
return .just(.toggleCell(indexPath))

case .tapConfirmAvailableTimes:
let selectedCells = currentState.selectedCells
let selectedDateTimes = NSTDateUtility.selectedCellsToDateTime(
selectedCells: selectedCells,
dateHeaders: NSTDateUtility.dateList(mockDateList),
timeHeaders: NSTDateUtility.timeList(mockStartTime, mockEndTime),
originalDateList: mockDateList
)
print(selectedDateTimes)

let availableTimes = selectedDateTimes.map { dateTime -> AvailableTime in
return AvailableTime(date: dateTime, startTime: dateTime, endTime: dateTime)
}
return .just(.setAvailableTimes(availableTimes))
}
}

func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .toggleCell(let indexPath):
newState.selectedCells = newState.selectedCells.symmetricDifference([indexPath])
Comment on lines +54 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 테이블에서의 Cell 선택/비선택 로직을 symetricDifference로 깔끔하게 해결하셨네요
너무 좋습니다 👍👍👍


case .setAvailableTimes(let availableTimes):
newState.availableTimes = availableTimes
}
return newState
}
}

struct AvailableTime: Codable {
let date: String
let startTime: String
let endTime: String
}
Comment on lines +64 to +68
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 따로 Entity로 뺴주시면 좋을 것 같아요!

Comment on lines +64 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일 따로 빼도 될 것 같아요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//
// AvailabilityCreateView.swift
// Noostak_iOS
//
// Created by 오연서 on 2/26/25.
//

import UIKit
import Then
import SnapKit
import RxSwift
import RxCocoa

let mockDateList: [String] = [
"2024-09-05T10:00:00",
"2024-09-06T10:00:00",
"2024-09-09T10:00:00",
"2024-09-10T10:00:00",
"2024-09-11T10:00:00",
"2024-09-12T10:00:00"
]
let mockStartTime: String = "2024-09-05T08:00:00"
let mockEndTime: String = "2024-09-05T23:00:00"
let totalRows = timeHeaders.count + 1
let totalColumns = dateHeaders.count + 1
let dateHeaders: [String] = NSTDateUtility.dateList(mockDateList)
let timeHeaders: [String] = NSTDateUtility.timeList(mockStartTime, mockEndTime)

final class AvailabilityCreateView: UIView {
// MARK: Properties
private let disposeBag = DisposeBag()

// MARK: Views
private let scrollView = UIScrollView()
private let contentView = UIView()
private let availbilityLabel = UILabel()
let schedulePickerView = SchedulePicker(timeHeaders: timeHeaders, dateHeaders: dateHeaders, mode: .readMode)
let confirmButton = AppThemeButton(theme: .grayScale, title: "확인")

// MARK: Init
override init(frame: CGRect) {
super.init(frame: frame)
setUpFoundation()
setUpHierarchy()
setUpUI()
setUpLayout()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: setUpHierarchy
private func setUpHierarchy() {
[scrollView, confirmButton].forEach {
self.addSubview($0)
}
scrollView.addSubview(contentView)
[availbilityLabel, schedulePickerView].forEach {
contentView.addSubview($0)
}
}
Comment on lines +55 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 제가 만들어둔 addSubviews(view1, view2, view2) 이렇게 사용하는게 좋을 것 같아요!


private func setUpFoundation() {
self.backgroundColor = .appWhite
}

// MARK: setUpUI
private func setUpUI() {
availbilityLabel.do {
$0.text = "가능한 시간을\n모두 선택해주세요"
$0.numberOfLines = 2
$0.font = .PretendardStyle.h4_b.font
}

schedulePickerView.do {
$0.showsVerticalScrollIndicator = false
}

confirmButton.do {
$0.backgroundColor = .appGray900
}
}

// MARK: setUpLayout
private func setUpLayout() {
scrollView.snp.makeConstraints {
$0.top.bottom.equalTo(self.safeAreaLayoutGuide)
$0.horizontalEdges.equalToSuperview()
}

contentView.snp.makeConstraints {
$0.edges.equalTo(scrollView.contentLayoutGuide)
$0.width.equalToSuperview()
$0.bottom.equalTo(schedulePickerView.snp.bottom)
}

availbilityLabel.snp.makeConstraints {
$0.top.equalToSuperview().offset(12)
$0.leading.equalToSuperview().offset(16)
}

confirmButton.snp.makeConstraints {
$0.horizontalEdges.equalToSuperview().inset(16)
$0.bottom.equalTo(self.safeAreaLayoutGuide)
$0.height.equalTo(54)
}

schedulePickerView.snp.makeConstraints {
$0.top.equalTo(availbilityLabel.snp.bottom).offset(8)
$0.horizontalEdges.equalToSuperview().inset(16)
$0.bottom.equalTo(self.safeAreaLayoutGuide).inset(62)
}
}
}

extension NSTDateUtility {
///타임테이블 뷰 : "요일 월/일"
static func dateList(_ dateStrings: [String]) -> [String] {
Comment on lines +117 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

화면에서의 Date 표시를 위해 View에서 extension 하신 것은 좋은 선택인 것 같아요!
하지만 이렇게 될 경우 해당 View가 포함된 프로젝트에서는 어디에서든지 이 코드를 사용할 수 있게 될 거에요.
extension을 AvailabilityCreateView로 바꿔서 뷰 내부 함수로 처리하는 건 어떨까요?
+ Reactor에서 일정 데이터를 관리하는 로직이 여기에 포함되어있는건 아닌지 더블 체크 부탁드려요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수를 비슷한 화면인 일정조회화면에서도 사용해서.. 고민이네요

let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식
let displayFormatter = NSTDateUtility(format: .EEMMdd) // 출력 형식

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

///타임테이블 뷰 : "00시"
static 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
}

static func selectedCellsToDateTime(selectedCells: Set<IndexPath>, dateHeaders: [String], timeHeaders: [String], originalDateList: [String]) -> [String] {
let formatter = NSTDateUtility(format: .EEMMdd) // dateList 출력형식
let originalFormatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 원본 포맷

return selectedCells.compactMap { indexPath in
let column = indexPath.item % dateHeaders.count
let row = indexPath.item / dateHeaders.count
let selectedDateHeader = dateHeaders[column]
let selectedTimeHeader = timeHeaders[row]

guard let originalDate = originalDateList.first(where: {
switch originalFormatter.date(from: $0) {
case .success(let parsedDate):
return formatter.string(from: parsedDate) == selectedDateHeader
case .failure:
return false
}
}) else { return nil }

return "\(originalDate.prefix(10))T\(selectedTimeHeader):00:00"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// AvailabilityCreateViewController.swift
// Noostak_iOS
//
// Created by 오연서 on 2/26/25.
//

import UIKit
import ReactorKit
import RxSwift
import RxCocoa
import RxDataSources

final class AvailabilityCreateViewController: UIViewController, View {
// MARK: - Properties
var disposeBag = DisposeBag()
private let rootView = AvailabilityCreateView()

// MARK: - Init
init(reactor: AvailabilityCreateReactor) {
super.init(nibName: nil, bundle: nil)
self.reactor = reactor
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func loadView() {
self.view = rootView
}

override func viewDidLoad() {
super.viewDidLoad()
setUpFoundation()
bindCollectionView()
}

private func setUpFoundation() {
self.view.backgroundColor = .white
}

private func bindCollectionView() {
let items = Observable.just([
SectionModel(model: "Section 1", items: Array(repeating: "", count: totalRows * totalColumns))
])

let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>>(
configureCell: { _, collectionView, indexPath, _ in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: SchedulePickerCell.identifier, for: indexPath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchedulePickerCell.swift 파일에 있는 identifier 속성 지우고 SchedulePickerCell.className 사용하시면 됩니다!

) as? SchedulePickerCell else {
return UICollectionViewCell()
}
cell.configureHeader(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders)
cell.configureTableRoundness(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders)
return cell
}
)
items
.bind(to: rootView.schedulePickerView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}

func bind(reactor: AvailabilityCreateReactor) {
rootView.confirmButton.bind(state: Observable.just(.able))

rootView.schedulePickerView.rx.itemSelected
.compactMap { [weak self] indexPath -> AvailabilityCreateReactor.Action? in
guard let self = self else { return nil }
let row = indexPath.item / totalColumns
let column = indexPath.item % totalColumns
// 헤더 행과 열 선택 비활성화
guard row > 0, column > 0 else {
self.rootView.schedulePickerView.deselectItem(at: indexPath, animated: true)
return nil
}
return AvailabilityCreateReactor.Action.selectCell(indexPath)
}
.bind(to: reactor.action)
.disposed(by: disposeBag)

rootView.confirmButton.rx.tap
.map { AvailabilityCreateReactor.Action.tapConfirmAvailableTimes }
.bind(to: reactor.action)
.disposed(by: disposeBag)
Comment on lines +83 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 해당 버튼을 통해 POST API를 호출한다면,
중간에 debounce를 걸어주시면 좋을 것 같습니다!


reactor.state
.map { $0.selectedCells }
.distinctUntilChanged()
.subscribe(onNext: { [weak self] selectedCells in
guard let self = self else { return }
self.rootView.schedulePickerView.visibleCells.forEach { cell in
guard let indexPath = self.rootView.schedulePickerView.indexPath(for: cell) else { return }
let isSelected = selectedCells.contains(indexPath)
cell.backgroundColor = isSelected ? .appBlue400 : .clear
}
})
.disposed(by: disposeBag)
}
}