-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/NST-15] 일정생성뷰 작성 #49
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
base: main
Are you sure you want to change the base?
Changes from all 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,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]) | ||
|
|
||
| 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
Collaborator
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. 요건 따로 Entity로 뺴주시면 좋을 것 같아요!
Comment on lines
+64
to
+68
Contributor
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. 파일 따로 빼도 될 것 같아요! |
||
| 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
Contributor
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. 요기 제가 만들어둔 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
Collaborator
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. 화면에서의 Date 표시를 위해 View에서 extension 하신 것은 좋은 선택인 것 같아요!
Collaborator
Author
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 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 | ||
|
Contributor
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. SchedulePickerCell.swift 파일에 있는 identifier 속성 지우고 |
||
| ) 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
Collaborator
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. 혹시 해당 버튼을 통해 POST API를 호출한다면, |
||
|
|
||
| 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) | ||
| } | ||
| } | ||
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.
와 테이블에서의 Cell 선택/비선택 로직을 symetricDifference로 깔끔하게 해결하셨네요
너무 좋습니다 👍👍👍