From 5466776a0bcd350541c6ed9de312751392f858db Mon Sep 17 00:00:00 2001 From: luckyyy Date: Tue, 4 Feb 2025 01:22:11 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Feat/#30]=20=EC=95=BD=EC=86=8D=EC=83=81?= =?UTF-8?q?=EC=84=B8=EB=B7=B0.....?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Application/SceneDelegate.swift | 2 +- .../Noostak_iOS/Domain/Entity/Schedule.swift | 28 ++- .../Global/Extension/UIFont+.swift | 20 +- .../ic_group_default.imageset/Contents.json | 12 + .../Supervisor account.svg | 10 + .../ic_share.imageset/Contents.json | 12 + .../ic_share.imageset/icon_share.svg | 5 + .../ic_vector.imageset/Contents.json | 12 + .../ic_vector.imageset/Vector.svg | 3 + .../Global/Utils/NSTDateUtility.swift | 34 ++- .../Presentation/Dummy/ViewController.swift | 160 ++++++++++++- .../GroupDetail/View/Cell/ConfirmedCVC.swift | 98 ++++++++ .../View/Cell/ConfirmedCellReactor.swift | 22 ++ .../GroupDetail/View/Cell/InProgressCVC.swift | 140 +++++++++++ .../View/Cell/InProgressCellReactor.swift | 22 ++ .../View/Cell/ScheduleCellReactor.swift | 22 ++ .../GroupDetail/View/GroupDetailView.swift | 217 ++++++++++++++++++ .../ViewController/GroupDetailReactor.swift | 149 ++++++++++++ .../GroupDetailViewController.swift | 134 +++++++++++ 19 files changed, 1080 insertions(+), 22 deletions(-) create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Contents.json create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Supervisor account.svg create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/Contents.json create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/icon_share.svg create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json create mode 100644 Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift create mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift diff --git a/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift b/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift index e78d7eb..bb91354 100644 --- a/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift +++ b/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift @@ -16,7 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - let vc = ViewController() + let vc = GroupDetailViewController(reactor: GroupDetailReactor()) window.rootViewController = vc self.window = window window.makeKeyAndVisible() diff --git a/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift b/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift index fed3f40..f786419 100644 --- a/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift +++ b/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift @@ -22,19 +22,19 @@ struct Schedule { ///약속 카테고리 let category: ScheduleCategory ///약속 생성기간 - let dates: [Date] + let selectionDates: [Date] ///약속생성 시작일 - var startDate: Date? { - return dates.sorted().first + var selectionStartDate: Date? { + return selectionDates.sorted().first } ///약속생성 종료일 - var endDate: Date? { - return dates.sorted().last + var selectionEndDate: Date? { + return selectionDates.sorted().last } - ///약속 시작시각 - let startTime: Date - ///약속 종료시각 - let endTime: Date + ///약속생성 시작시각 + let selectionStartTime: Date + ///약속생성 종료시각 + let selectionEndTime: Date ///소요시간 var duration: Int? } @@ -42,10 +42,20 @@ struct Schedule { struct ExtendedSchedule { ///스케쥴 let schedule: Schedule + ///약속 날짜(1순위, 확정) + let date: String + ///약속 시작시각(1순위, 확정) + let startTime: String + ///약속 종료시각(1순위, 확정) + let endTime: String ///가능한 친구 let availableMembers: [User] ///불가능한 친구 let unavailableMembers: [User] + ///그룹 총인원 + var groupMemberCount: Int + ///가능한 인원 + var availableMemberCount: Int } extension ScheduleCategory { diff --git a/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift b/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift index bf4488f..82cadf0 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift @@ -46,6 +46,7 @@ extension UIFont { case h1_sb case h2_b case h3_sb + case h3_22_SB case h4_b case h4_sb case h5_b @@ -63,14 +64,15 @@ extension UIFont { case c2_sb case c3_r case c4_r - + var font: UIFont { switch self { - case .h1_b: return UIFont.pretendard(.bold, size: 56) - case .h1_sb: return UIFont.pretendard(.semibold, size: 56) - case .h2_b: return UIFont.pretendard(.bold, size: 27) + case .h1_b: return UIFont.pretendard(.bold, size: 27) + case .h1_sb: return UIFont.pretendard(.semibold, size: 27) + case .h2_b: return UIFont.pretendard(.bold, size: 24) case .h3_sb: return UIFont.pretendard(.semibold, size: 24) - case .h4_b: return UIFont.pretendard(.bold, size: 24) + case .h3_22_SB: return UIFont.pretendard(.semibold, size: 22) + case .h4_b: return UIFont.pretendard(.bold, size: 20) case .h4_sb: return UIFont.pretendard(.semibold, size: 20) case .h5_b: return UIFont.pretendard(.bold, size: 18) case .t1_sb: return UIFont.pretendard(.semibold, size: 18) @@ -89,13 +91,13 @@ extension UIFont { case .c4_r: return UIFont.pretendard(.regular, size: 11) } } - + // Line Height (LHLHUnit) var lineHeightUnit: CGFloat { switch self { - case .h1_b, .h1_sb, .h2_b, .h3_sb, .h4_b, .h4_sb, .h5_b, - .t1_sb, .t2_r, .t3_b, .t4_b, .b1_sb, .b2_r, .b4_sb, - .b4_sb_1percent, .b4_r, .b5_r, .c1_b, .c2_sb, .c3_r, .c4_r: + case .h1_b, .h1_sb, .h2_b, .h3_sb, .h3_22_SB, .h4_b, .h4_sb, .h5_b, + .t1_sb, .t2_r, .t3_b, .t4_b, .b1_sb, .b2_r, .b4_sb, + .b4_sb_1percent, .b4_r, .b5_r, .c1_b, .c2_sb, .c3_r, .c4_r: return 140 } } diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Contents.json new file mode 100644 index 0000000..0c69458 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Supervisor account.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Supervisor account.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Supervisor account.svg new file mode 100644 index 0000000..31acc5c --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_group_default.imageset/Supervisor account.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/Contents.json new file mode 100644 index 0000000..b2d0c9d --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_share.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/icon_share.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/icon_share.svg new file mode 100644 index 0000000..ffeb759 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_share.imageset/icon_share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json new file mode 100644 index 0000000..ba0cba8 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Vector.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg new file mode 100644 index 0000000..9901a0e --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift index e3e029d..f473d86 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift @@ -88,6 +88,7 @@ public extension NSTDateUtility { } extension NSTDateUtility { + ///타임테이블 뷰 : "요일 월/일" static func dateList(_ dateStrings: [String]) -> [String] { let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 let displayFormatter = NSTDateUtility(format: .MMddEE) // 출력 형식 @@ -102,7 +103,8 @@ extension NSTDateUtility { } } } - + + ///타임테이블 뷰 : "00시" static func timeList(_ startTime: String, _ endTime: String) -> [String] { let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 var result: [String] = [] @@ -126,4 +128,34 @@ extension NSTDateUtility { } return result } + + ///약속상세뷰 : "9월 7일 (일) 10:00~12:00" + static func durationList(_ startTime: String, _ endTime: String) -> String { + let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") + + // 날짜 포맷: "9월 7일 (일)" + dateFormatter.dateFormat = "M월 d일 (E)" + + // 시간 포맷: "10:00" + let timeFormatter = DateFormatter() + timeFormatter.locale = Locale(identifier: "ko_KR") + timeFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") + timeFormatter.dateFormat = "HH:mm" + + switch (formatter.date(from: startTime), formatter.date(from: endTime)) { + case (.success(let start), .success(let end)): + let dateString = dateFormatter.string(from: start) // "9월 7일 (일)" + let startTimeString = timeFormatter.string(from: start) // "10:00" + let endTimeString = timeFormatter.string(from: end) // "12:00" + + return "\(dateString) \(startTimeString)~\(endTimeString)" + + default: + print("Failed to parse start or end time.") + return "" + } + } } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift index 8ad6f11..6928d53 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift @@ -6,12 +6,168 @@ // import UIKit +import SnapKit +import RxSwift +import RxCocoa +import RxDataSources -class ViewController: UIViewController { +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 mockDateList2: [String] = [ + "2024-09-05T10:00:00", + "2024-09-06T10:00:00", + "2024-09-09T10:00:00" +] +let mockStartTime: String = "2024-09-05T08:00:00" +let mockEndTime: String = "2024-09-05T10:00:00" +let mockEndTime2: String = "2024-09-05T17:00:00" +let participants: Int = 5 +let mockMemberStartTime: [String] = [ + "2024-09-05T08:00:00", // 1명 + "2024-09-05T09:00:00", + "2024-09-05T10:00:00", + "2024-09-05T11:00:00", + "2024-09-05T11:00:00", + "2024-09-05T12:00:00", + "2024-09-05T12:00:00", + "2024-09-06T11:00:00", + "2024-09-06T11:00:00", + "2024-09-06T11:00:00", + "2024-09-06T11:00:00", + "2024-09-06T12:00:00", // 5명 + "2024-09-06T12:00:00", + "2024-09-06T12:00:00", + "2024-09-06T12:00:00", + "2024-09-06T12:00:00", + "2024-09-06T13:00:00", // 3명 + "2024-09-06T13:00:00", + "2024-09-06T13:00:00", + "2024-09-06T14:00:00", // 2명 + "2024-09-06T14:00:00", + "2024-09-06T14:00:00", + "2024-09-06T15:00:00", // 1명 + "2024-09-06T16:00:00", // 4명 + "2024-09-06T16:00:00", + "2024-09-06T16:00:00", + "2024-09-06T16:00:00", + "2024-09-05T15:00:00", // 1명 + "2024-09-05T16:00:00", // 4명 + "2024-09-05T16:00:00", + "2024-09-05T16:00:00", + "2024-09-05T16:00:00", // 0명 + "2024-09-05T18:00:00", // 3명 + "2024-09-05T18:00:00", + "2024-09-05T18:00:00", + "2024-09-05T19:00:00", // 5명 + "2024-09-05T19:00:00", + "2024-09-05T19:00:00", + "2024-09-05T20:00:00", // 2명 + "2024-09-05T20:00:00", + "2024-09-05T21:00:00" // 1명 +] +let dateHeaders: [String] = NSTDateUtility.dateList(mockDateList) +let timeHeaders: [String] = NSTDateUtility.timeList(mockStartTime, mockEndTime) +let dateHeaders2: [String] = NSTDateUtility.dateList(mockDateList2) +let timeHeaders2: [String] = NSTDateUtility.timeList(mockStartTime, mockEndTime2) +let totalRows = timeHeaders.count + 1 +let totalColumns = dateHeaders.count + 1 +let totalRows2 = timeHeaders2.count + 1 +let totalColumns2 = dateHeaders2.count + 1 + +final class ViewController: UIViewController { + private lazy var schedulePicker1 = SchedulePicker(timeHeaders: timeHeaders, dateHeaders: dateHeaders, mode: .editMode) + private lazy var schedulePicker2 = SchedulePicker(timeHeaders: timeHeaders2, dateHeaders: dateHeaders2, mode: .readMode) + private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + setupView() + bindCollectionView() } + + private func setupView() { + view.backgroundColor = .white + view.addSubview(schedulePicker1) + view.addSubview(schedulePicker2) + + schedulePicker1.translatesAutoresizingMaskIntoConstraints = false + schedulePicker2.translatesAutoresizingMaskIntoConstraints = false + schedulePicker1.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.top.equalToSuperview() + $0.height.equalTo(55 + totalRows * 32) + } + schedulePicker2.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.top.equalTo(schedulePicker1.snp.bottom).offset(10) + $0.height.equalTo(UIScreen.main.bounds.height - 170) + } + } + + private func bindCollectionView() { + let items = Observable.just([ + SectionModel(model: "Section 1", items: Array(repeating: "", count: totalRows * totalColumns)) + ]) + let items2 = Observable.just([ + SectionModel(model: "Section 1", items: Array(repeating: "", count: totalRows2 * totalColumns2)) + ]) + + let dataSource = RxCollectionViewSectionedReloadDataSource>( + configureCell: { [weak self] _, collectionView, indexPath, _ in + guard let self = self, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: SchedulePickerCell.identifier, for: indexPath + ) as? SchedulePickerCell else { + return UICollectionViewCell() + } + cell.configureHeader(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders) + cell.configureTableRoundness(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders) + self.schedulePicker1.configureCellBackground(cell, for: indexPath, participants: participants) + return cell + } + ) + + let dataSource2 = RxCollectionViewSectionedReloadDataSource>( + configureCell: { [weak self] _, collectionView, indexPath, _ in + guard let self = self, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: SchedulePickerCell.identifier, for: indexPath + ) as? SchedulePickerCell else { + return UICollectionViewCell() + } + cell.configureHeader(for: indexPath, dateHeaders: dateHeaders2, timeHeaders: timeHeaders2) + cell.configureTableRoundness(for: indexPath, dateHeaders: dateHeaders2, timeHeaders: timeHeaders2) + self.schedulePicker2.configureCellBackground(cell, for: indexPath, participants: participants) + self.schedulePicker2.updateCellAvailability(with: mockDateList, startTimes: mockMemberStartTime) + return cell + } + ) + items + .bind(to: schedulePicker1.rx.items(dataSource: dataSource)) + .disposed(by: disposeBag) + items2 + .bind(to: schedulePicker2.rx.items(dataSource: dataSource2)) + .disposed(by: disposeBag) + + schedulePicker1.rx.itemSelected + .subscribe(onNext: { [weak self] indexPath in + guard let self = self else { return } + let row = indexPath.item / totalColumns + let column = indexPath.item % totalColumns + + guard row > 0, column > 0 else { + self.schedulePicker1.deselectItem(at: indexPath, animated: true) // 첫 번째 행과 첫 번째 열은 선택 불가 + return + } + self.schedulePicker1.addSelectedCell(at: indexPath) + }) + .disposed(by: disposeBag) + } } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift new file mode 100644 index 0000000..06d9933 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift @@ -0,0 +1,98 @@ +// +// ConfirmedCVC.swift +// Noostak_iOS +// +// Created by 오연서 on 2/1/25. +// + +import UIKit +import SnapKit +import Then +import ReactorKit + +final class ConfirmedCVC: UICollectionViewCell, View { + + // MARK: Properties + static let identifier = "ConfirmedCVC" + var disposeBag = DisposeBag() + + // MARK: Views + private let chip = UIView() + private let scheduleTitleLabel = UILabel() + private let timeLabel = UILabel() + private let divider = UIView() + + // MARK: Init + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpUI() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: setUpHierarchy + private func setUpHierarchy() { + [chip, scheduleTitleLabel, timeLabel, divider].forEach { + self.addSubview($0) + } + } + + // MARK: setUpUI + private func setUpUI() { + chip.do { + $0.layer.cornerRadius = 6.5 + } + + scheduleTitleLabel.do { + $0.font = .PretendardStyle.b4_sb.font + $0.textColor = .appGray900 + } + + timeLabel.do { + $0.font = .PretendardStyle.b5_r.font + $0.textColor = .appGray800 + } + + divider.do { + $0.backgroundColor = .appGray200 + } + } + + // MARK: setUpLayout + private func setUpLayout() { + chip.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.leading.equalToSuperview().offset(6) + $0.size.equalTo(13) + } + + scheduleTitleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.leading.equalTo(chip.snp.trailing).offset(9) + } + + timeLabel.snp.makeConstraints { + $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(1) + $0.leading.equalTo(scheduleTitleLabel) + } + + divider.snp.makeConstraints { + $0.top.equalTo(timeLabel.snp.bottom).offset(15) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(1) + } + } +} + +extension ConfirmedCVC { + func bind(reactor: ConfirmedCellReactor) { + let schedule = reactor.currentState.schedule + chip.backgroundColor = schedule.schedule.category.displayColor + scheduleTitleLabel.text = schedule.schedule.name + timeLabel.text = NSTDateUtility.durationList(schedule.startTime, schedule.endTime) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift new file mode 100644 index 0000000..09a4a95 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift @@ -0,0 +1,22 @@ +// +// ConfirmedCellReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/3/25. +// + +import ReactorKit +import RxSwift + +final class ConfirmedCellReactor: Reactor { + typealias Action = NoAction + struct State { + let schedule: ExtendedSchedule + } + + let initialState: State + + init(schedule: ExtendedSchedule) { + self.initialState = State(schedule: schedule) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift new file mode 100644 index 0000000..11c5972 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift @@ -0,0 +1,140 @@ +// +// InProgressCVC.swift +// Noostak_iOS +// +// Created by 오연서 on 2/1/25. +// + +import UIKit +import SnapKit +import Then +import ReactorKit + +final class InProgressCVC: UICollectionViewCell, View { + + // MARK: Properties + static let identifier = "InProgressCVC" + var disposeBag = DisposeBag() + + // MARK: Views + private let content = UIView() + private let scheduleTitleLabel = UILabel() + private let moveButton = UIButton() + private let timeLabel = UILabel() + private let availabilityLabel = UILabel() + private let backgroundProgressBar = UIView() + private let progressBar = UIView() + + // MARK: Init + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpUI() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: setUpHierarchy + private func setUpHierarchy() { + self.addSubview(content) + [scheduleTitleLabel, scheduleTitleLabel, moveButton, timeLabel, availabilityLabel, backgroundProgressBar, progressBar].forEach { + content.addSubview($0) + } + } + + // MARK: setUpUI + private func setUpUI() { + content.do { + $0.backgroundColor = .appWhite + $0.layer.borderColor = UIColor.appGray200.cgColor + $0.layer.cornerRadius = 15 + $0.layer.borderWidth = 1 + } + + scheduleTitleLabel.do { + $0.font = .PretendardStyle.b4_sb.font + $0.textColor = .appGray900 + } + + moveButton.do { + $0.setImage(.icVector, for: .normal) + } + + timeLabel.do { + $0.font = .PretendardStyle.b5_r.font + $0.textColor = .appGray800 + } + + availabilityLabel.do { + $0.font = .PretendardStyle.b5_r.font + $0.textColor = .appGray700 + } + + backgroundProgressBar.do { + $0.backgroundColor = .appGray200 + $0.layer.cornerRadius = 2 + } + + progressBar.do { + $0.backgroundColor = .appBlue300 + $0.layer.cornerRadius = 2 + } + } + + // MARK: setUpLayout + private func setUpLayout() { + content.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + scheduleTitleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(19) + $0.leading.equalToSuperview().offset(16) + } + + moveButton.snp.makeConstraints { + $0.top.trailing.equalToSuperview().inset(22) + $0.size.equalTo(18) + } + + timeLabel.snp.makeConstraints { + $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(10) + $0.leading.equalTo(scheduleTitleLabel) + } + + availabilityLabel.snp.makeConstraints { + $0.centerY.equalTo(timeLabel) + $0.trailing.equalToSuperview().inset(16) + } + + backgroundProgressBar.snp.makeConstraints { + $0.top.equalTo(timeLabel.snp.bottom).offset(10) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(4) + } + + progressBar.snp.makeConstraints { + $0.top.equalTo(timeLabel.snp.bottom).offset(10) + $0.leading.equalTo(backgroundProgressBar) + $0.height.equalTo(4) + } + } +} + +extension InProgressCVC { + func bind(reactor: InProgressCellReactor) { + let schedule = reactor.currentState.schedule + scheduleTitleLabel.text = schedule.schedule.name + timeLabel.text = NSTDateUtility.durationList(schedule.startTime, schedule.endTime) + availabilityLabel.text = "\(schedule.availableMemberCount) / \(schedule.groupMemberCount)" + progressBar.snp.makeConstraints { + $0.top.equalTo(timeLabel.snp.bottom).offset(10) + $0.leading.equalTo(backgroundProgressBar) + $0.width.equalTo(50) + $0.height.equalTo(4) + } + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift new file mode 100644 index 0000000..434f07e --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift @@ -0,0 +1,22 @@ +// +// InProgressCellReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/3/25. +// + +import ReactorKit +import RxSwift + +final class InProgressCellReactor: Reactor { + typealias Action = NoAction + struct State { + let schedule: ExtendedSchedule + } + + let initialState: State + + init(schedule: ExtendedSchedule) { + self.initialState = State(schedule: schedule) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift new file mode 100644 index 0000000..7bc3cb9 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift @@ -0,0 +1,22 @@ +// +// ScheduleCellReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/3/25. +// + +import ReactorKit +import RxSwift + +final class ScheduleCellReactor: Reactor { + typealias Action = NoAction + struct State { + let schedule: ExtendedSchedule + } + + let initialState: State + + init(schedule: ExtendedSchedule) { + self.initialState = State(schedule: schedule) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift new file mode 100644 index 0000000..35a5170 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift @@ -0,0 +1,217 @@ +// +// GroupDetailView.swift +// Noostak_iOS +// +// Created by 오연서 on 1/31/25. +// + +import UIKit +import Then +import SnapKit +import RxSwift +import RxCocoa + +final class GroupDetailView: UIView { + + // MARK: Properties + private let disposeBag = DisposeBag() + + // MARK: Views + private let profileImageView = UIImageView() + private let groupNameLabel = UILabel() + private lazy var shareButton = UIButton() + private lazy var groupMemberButton = UIButton() + private let scheduleListLabel = UILabel() + let segmentedControl = UISegmentedControl(items: ["진행 중", "확정"]) + private let underlineView = UIView() + private let selectedUnderlineView = UIView() + let defaultLabel = UILabel() + let inProgressCollectionView: UICollectionView + let confirmedCollectionView: UICollectionView + + // MARK: Init + override init(frame: CGRect) { + let layout = UICollectionViewFlowLayout() + self.inProgressCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + self.confirmedCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + super.init(frame: frame) + setUpFoundation() + setUpHierarchy() + setUpUI() + setUpLayout() + bindSegmentControl() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: setUpHierarchy + private func setUpHierarchy() { + [profileImageView, groupNameLabel, shareButton, groupMemberButton, scheduleListLabel, + segmentedControl, underlineView, selectedUnderlineView, + defaultLabel, inProgressCollectionView, confirmedCollectionView].forEach { + self.addSubview($0) + } + } + + private func setUpFoundation() { + self.backgroundColor = .appWhite + } + // MARK: setUpUI + private func setUpUI() { + profileImageView.do { + $0.layer.cornerRadius = 7.14 + $0.image = .icGroupDefault + } + + groupNameLabel.do { + $0.font = .PretendardStyle.h1_b.font + $0.textColor = .appGray900 + $0.text = "누스탁" + } + + shareButton.do { + $0.setImage(.icShare, for: .normal) + } + + groupMemberButton.do { + var config = UIButton.Configuration.filled() + config.baseBackgroundColor = .clear + config.baseForegroundColor = .appGray800 + config.attributedTitle = AttributedString("그룹 멤버 8", + attributes: AttributeContainer([.font: UIFont.PretendardStyle.b2_r.font])) + config.image = .icVector + config.imagePlacement = .trailing + config.imagePadding = 5 + config.contentInsets = .zero + $0.configuration = config + + } + + scheduleListLabel.do { + $0.font = .PretendardStyle.b1_sb.font + $0.textColor = .appGray800 + $0.text = "약속 리스트" + } + + segmentedControl.do { + $0.selectedSegmentIndex = 0 + $0.selectedSegmentTintColor = .clear + $0.setBackgroundImage(UIImage(), for: .normal, barMetrics: .default) + $0.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) + $0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.appGray900, + NSAttributedString.Key.font: UIFont.PretendardStyle.b1_sb.font], for: .selected) + $0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.appGray500, + NSAttributedString.Key.font: UIFont.PretendardStyle.b1_sb.font], for: .normal) + } + + underlineView.do { + $0.backgroundColor = .appGray200 + $0.layer.cornerRadius = 2 + } + + selectedUnderlineView.do { + $0.backgroundColor = .appGray900 + $0.layer.cornerRadius = 2 + } + + defaultLabel.do { + $0.font = .PretendardStyle.b2_r.font + $0.textColor = .appGray900 + $0.isHidden = true + } + + inProgressCollectionView.do { + $0.isHidden = false + } + + confirmedCollectionView.do { + $0.isHidden = true + } + } + + // MARK: setUpLayout + private func setUpLayout() { + profileImageView.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide).offset(21) + $0.leading.equalToSuperview().inset(16) + $0.size.equalTo(40) + } + + groupNameLabel.snp.makeConstraints { + $0.centerY.equalTo(profileImageView) + $0.leading.equalTo(profileImageView.snp.trailing).offset(8) + } + + shareButton.snp.makeConstraints { + $0.top.equalTo(profileImageView) + $0.trailing.equalToSuperview().inset(16) + $0.size.equalTo(24) + } + + groupMemberButton.snp.makeConstraints { + $0.top.equalTo(profileImageView.snp.bottom).offset(9) + $0.leading.equalTo(profileImageView) + } + + scheduleListLabel.snp.makeConstraints { + $0.top.equalTo(groupMemberButton.snp.bottom).offset(24) + $0.leading.equalToSuperview().inset(17) + } + + segmentedControl.snp.makeConstraints { + $0.top.equalTo(scheduleListLabel.snp.bottom).offset(25) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(22) + } + + underlineView.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom).offset(16) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(4) + } + + selectedUnderlineView.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom).offset(16) + $0.leading.equalTo(segmentedControl.snp.leading) + $0.width.equalTo(segmentedControl.snp.width).dividedBy(2) + $0.height.equalTo(4) + } + + defaultLabel.snp.makeConstraints { + $0.top.equalTo(selectedUnderlineView.snp.bottom).offset(142) + $0.centerX.equalToSuperview() + } + + inProgressCollectionView.snp.makeConstraints { + $0.top.equalTo(selectedUnderlineView.snp.bottom).offset(19) + $0.horizontalEdges.bottom.equalToSuperview() + $0.bottom.equalTo(self.safeAreaLayoutGuide) + } + + confirmedCollectionView.snp.makeConstraints { + $0.top.equalTo(selectedUnderlineView.snp.bottom).offset(13) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(self.safeAreaLayoutGuide) + } + } + + // MARK: - Bind Methods + private func bindSegmentControl() { + segmentedControl.rx.selectedSegmentIndex + .subscribe(onNext: { [weak self] index in + guard let self = self else { return } + let segmentWidth = self.segmentedControl.frame.width / 2 + let leadingDistance = CGFloat(index) * segmentWidth + + UIView.animate(withDuration: 0.2) { + self.selectedUnderlineView.snp.updateConstraints { + $0.leading.equalTo(self.segmentedControl.snp.leading).offset(leadingDistance) + } + self.layoutIfNeeded() + } + }) + .disposed(by: disposeBag) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift new file mode 100644 index 0000000..f2b5d79 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift @@ -0,0 +1,149 @@ +// +// GroupDetailReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 1/31/25. +// + +import ReactorKit +import RxSwift +import UIKit + +final class GroupDetailReactor: Reactor { + enum Action { + case selectSegment(Int) // 세그먼트 선택 + case loadInProgressData + case loadConfirmedData + } + enum Mutation { + case setSelectedSegment(Int) + case setInProgressData([InProgressCellReactor]) + case setConfirmedData([ConfirmedCellReactor]) + } + struct State { + var selectedSegmentIndex: Int = 0 + var inProgressCellReactors: [InProgressCellReactor] = [] + var confirmedCellReactors: [ConfirmedCellReactor] = [] + } + + let initialState = State( + selectedSegmentIndex: 0, + inProgressCellReactors: mockInProgressData.map { InProgressCellReactor(schedule: $0) }, + confirmedCellReactors: mockConfirmedData.map { ConfirmedCellReactor(schedule: $0) } + ) + + func mutate(action: Action) -> Observable { + switch action { + case .selectSegment(let index): + return Observable.just(.setSelectedSegment(index)) + + case .loadInProgressData: + let reactors = mockInProgressData.map { InProgressCellReactor(schedule: $0) } + return Observable.just(.setInProgressData(reactors)) + + case .loadConfirmedData: + let reactors = mockConfirmedData.map { ConfirmedCellReactor(schedule: $0) } + return Observable.just(.setConfirmedData(reactors)) + } + } + +// func mutate(action: Action) -> Observable { +// switch action { +// case .selectSegment(let index): +// return Observable.just(.setSelectedSegment(index)) +// +// case .loadInProgressData: +// return Observable.just(.setInProgressData(mockInProgressData.map { InProgressCellReactor(schedule: $0) })) +// .observe(on: MainScheduler.instance) +// +// case .loadConfirmedData: +// return Observable.just(.setConfirmedData(mockConfirmedData.map { ConfirmedCellReactor(schedule: $0) })) +// .observe(on: MainScheduler.instance) +// } +// } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setSelectedSegment(let index): + newState.selectedSegmentIndex = index + + case .setInProgressData(let reactors): + newState.inProgressCellReactors = reactors + + case .setConfirmedData(let reactors): + newState.confirmedCellReactors = reactors + } + return newState + } +} + +let mockInProgressData: [ExtendedSchedule] = [ + ExtendedSchedule(schedule: Schedule(id: 1, + name: "뉴스탹진행중", + category: .hobby, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-05T10:00:00", + startTime: "2024-09-05T10:00:00", + endTime: "2024-09-05T18:00:00", + availableMembers: [], + unavailableMembers: [], + groupMemberCount: 24, + availableMemberCount: 11), + ExtendedSchedule(schedule: Schedule(id: 2, + name: "뉴스탹2", + category: .important, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-05T10:00:00", + startTime: "2024-09-05T10:00:00", + endTime: "2024-09-05T18:00:00", + availableMembers: [], + unavailableMembers: [], + groupMemberCount: 23, + availableMemberCount: 12), + ExtendedSchedule(schedule: Schedule(id: 1, + name: "뉴스탹3", + category: .hobby, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-05T10:00:00", + startTime: "2024-09-05T10:00:00", + endTime: "2024-09-05T18:00:00", + availableMembers: [], + unavailableMembers: [], + groupMemberCount: 24, + availableMemberCount: 11) +] +let mockConfirmedData: [ExtendedSchedule] = [ + ExtendedSchedule(schedule: Schedule(id: 1, + name: "뉴스탹유ㅏㄴ", + category: .hobby, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-05T10:00:00", + startTime: "2024-09-05T10:00:00", + endTime: "2024-09-05T18:00:00", + availableMembers: [], + unavailableMembers: [], + groupMemberCount: 24, + availableMemberCount: 11), + ExtendedSchedule(schedule: Schedule(id: 2, + name: "뉴스탹2", + category: .important, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-05T10:00:00", + startTime: "2024-09-05T10:00:00", + endTime: "2024-09-05T18:00:00", + availableMembers: [], + unavailableMembers: [], + groupMemberCount: 23, + availableMemberCount: 12) +] diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift new file mode 100644 index 0000000..9b51460 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift @@ -0,0 +1,134 @@ +// +// GroupDetailViewController.swift +// Noostak_iOS +// +// Created by 오연서 on 1/31/25. +// + +import UIKit +import ReactorKit +import RxSwift +import RxCocoa +import RxDataSources + +final class GroupDetailViewController: UIViewController, View { + // MARK: - Properties + var disposeBag = DisposeBag() + private let rootView = GroupDetailView() + + // MARK: - Init + init(reactor: GroupDetailReactor) { + 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() + setUpCollectionView() + } + + private func setUpFoundation() { + self.view.backgroundColor = .white + } + + private func setUpCollectionView() { + rootView.inProgressCollectionView.register(InProgressCVC.self, forCellWithReuseIdentifier: InProgressCVC.identifier) + rootView.confirmedCollectionView.register(ConfirmedCVC.self, forCellWithReuseIdentifier: ConfirmedCVC.identifier) + rootView.inProgressCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + rootView.confirmedCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + DispatchQueue.main.async { + self.rootView.layoutIfNeeded() + } + } + + // MARK: - Bind Reactor + func bind(reactor: GroupDetailReactor) { + rootView.segmentedControl.rx.selectedSegmentIndex + .distinctUntilChanged() + .flatMapLatest { index -> Observable in + if index == 0 { + return Observable.concat([ + .just(.selectSegment(index)), + .just(.loadInProgressData) + ]) + } else { + return Observable.concat([ + .just(.selectSegment(index)), + .just(.loadConfirmedData) + ]) + } + } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .subscribe(onNext: { [weak self] state in + guard let self = self else { return } + + let isProgress = (state.selectedSegmentIndex == 0) + self.rootView.inProgressCollectionView.isHidden = !isProgress + self.rootView.confirmedCollectionView.isHidden = isProgress + + let inProgressEmpty = state.inProgressCellReactors.isEmpty + let confirmedEmpty = state.confirmedCellReactors.isEmpty + + self.rootView.defaultLabel.isHidden = !(isProgress ? inProgressEmpty : confirmedEmpty) + self.rootView.defaultLabel.text = isProgress ? "현재 진행중인 약속이 없어요" : "아직 확정된 약속이 없어요" + self.rootView.layoutIfNeeded() + }) + .disposed(by: disposeBag) + + // 진행 중 바인딩 + reactor.state.map { $0.inProgressCellReactors } + .observe(on: MainScheduler.instance) + .do(onNext: { _ in + self.rootView.inProgressCollectionView.reloadData() + }) + .bind(to: rootView.inProgressCollectionView.rx.items(cellIdentifier: InProgressCVC.identifier, cellType: InProgressCVC.self)) { index, reactor, cell in + cell.reactor = reactor + } + .disposed(by: disposeBag) + + // 확정된 약속 바인딩 + reactor.state.map { $0.confirmedCellReactors } + .do(onNext: { _ in self.rootView.confirmedCollectionView.reloadData() }) + .bind(to: rootView.confirmedCollectionView.rx.items(cellIdentifier: ConfirmedCVC.identifier, cellType: ConfirmedCVC.self)) { index, reactor, cell in + cell.reactor = reactor + } + .disposed(by: disposeBag) + } +} + +extension GroupDetailViewController: UICollectionViewDelegateFlowLayout { + /// 순서가 왜바껴쓸까.. + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + switch collectionView { + case rootView.inProgressCollectionView: + return CGSize(width: UIScreen.main.bounds.width - 28, height: 105) + case rootView.confirmedCollectionView: + return CGSize(width: UIScreen.main.bounds.width - 32, height: 72) + default: + return CGSize() + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + switch collectionView { + case rootView.inProgressCollectionView: + return 11 + case rootView.confirmedCollectionView: + return 2 + default: + return 0 + } + } +} From 9e7e2f4712ce9646b2ad2776a9358af7ee424460 Mon Sep 17 00:00:00 2001 From: luckyyy Date: Tue, 4 Feb 2025 01:30:09 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Feat/#30]=20=EC=9B=90=EB=B3=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Application/SceneDelegate.swift | 2 +- .../Presentation/Dummy/ViewController.swift | 160 +----------------- 2 files changed, 2 insertions(+), 160 deletions(-) diff --git a/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift b/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift index bb91354..e78d7eb 100644 --- a/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift +++ b/Noostak_iOS/Noostak_iOS/Application/SceneDelegate.swift @@ -16,7 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - let vc = GroupDetailViewController(reactor: GroupDetailReactor()) + let vc = ViewController() window.rootViewController = vc self.window = window window.makeKeyAndVisible() diff --git a/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift index 6928d53..601af3c 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift @@ -6,168 +6,10 @@ // import UIKit -import SnapKit -import RxSwift -import RxCocoa -import RxDataSources - -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 mockDateList2: [String] = [ - "2024-09-05T10:00:00", - "2024-09-06T10:00:00", - "2024-09-09T10:00:00" -] -let mockStartTime: String = "2024-09-05T08:00:00" -let mockEndTime: String = "2024-09-05T10:00:00" -let mockEndTime2: String = "2024-09-05T17:00:00" -let participants: Int = 5 -let mockMemberStartTime: [String] = [ - "2024-09-05T08:00:00", // 1명 - "2024-09-05T09:00:00", - "2024-09-05T10:00:00", - "2024-09-05T11:00:00", - "2024-09-05T11:00:00", - "2024-09-05T12:00:00", - "2024-09-05T12:00:00", - "2024-09-06T11:00:00", - "2024-09-06T11:00:00", - "2024-09-06T11:00:00", - "2024-09-06T11:00:00", - "2024-09-06T12:00:00", // 5명 - "2024-09-06T12:00:00", - "2024-09-06T12:00:00", - "2024-09-06T12:00:00", - "2024-09-06T12:00:00", - "2024-09-06T13:00:00", // 3명 - "2024-09-06T13:00:00", - "2024-09-06T13:00:00", - "2024-09-06T14:00:00", // 2명 - "2024-09-06T14:00:00", - "2024-09-06T14:00:00", - "2024-09-06T15:00:00", // 1명 - "2024-09-06T16:00:00", // 4명 - "2024-09-06T16:00:00", - "2024-09-06T16:00:00", - "2024-09-06T16:00:00", - "2024-09-05T15:00:00", // 1명 - "2024-09-05T16:00:00", // 4명 - "2024-09-05T16:00:00", - "2024-09-05T16:00:00", - "2024-09-05T16:00:00", // 0명 - "2024-09-05T18:00:00", // 3명 - "2024-09-05T18:00:00", - "2024-09-05T18:00:00", - "2024-09-05T19:00:00", // 5명 - "2024-09-05T19:00:00", - "2024-09-05T19:00:00", - "2024-09-05T20:00:00", // 2명 - "2024-09-05T20:00:00", - "2024-09-05T21:00:00" // 1명 -] -let dateHeaders: [String] = NSTDateUtility.dateList(mockDateList) -let timeHeaders: [String] = NSTDateUtility.timeList(mockStartTime, mockEndTime) -let dateHeaders2: [String] = NSTDateUtility.dateList(mockDateList2) -let timeHeaders2: [String] = NSTDateUtility.timeList(mockStartTime, mockEndTime2) -let totalRows = timeHeaders.count + 1 -let totalColumns = dateHeaders.count + 1 -let totalRows2 = timeHeaders2.count + 1 -let totalColumns2 = dateHeaders2.count + 1 final class ViewController: UIViewController { - private lazy var schedulePicker1 = SchedulePicker(timeHeaders: timeHeaders, dateHeaders: dateHeaders, mode: .editMode) - private lazy var schedulePicker2 = SchedulePicker(timeHeaders: timeHeaders2, dateHeaders: dateHeaders2, mode: .readMode) - private let disposeBag = DisposeBag() - override func viewDidLoad() { super.viewDidLoad() - setupView() - bindCollectionView() - } - - private func setupView() { - view.backgroundColor = .white - view.addSubview(schedulePicker1) - view.addSubview(schedulePicker2) - - schedulePicker1.translatesAutoresizingMaskIntoConstraints = false - schedulePicker2.translatesAutoresizingMaskIntoConstraints = false - - schedulePicker1.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.top.equalToSuperview() - $0.height.equalTo(55 + totalRows * 32) - } - schedulePicker2.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.top.equalTo(schedulePicker1.snp.bottom).offset(10) - $0.height.equalTo(UIScreen.main.bounds.height - 170) - } - } - - private func bindCollectionView() { - let items = Observable.just([ - SectionModel(model: "Section 1", items: Array(repeating: "", count: totalRows * totalColumns)) - ]) - let items2 = Observable.just([ - SectionModel(model: "Section 1", items: Array(repeating: "", count: totalRows2 * totalColumns2)) - ]) - - let dataSource = RxCollectionViewSectionedReloadDataSource>( - configureCell: { [weak self] _, collectionView, indexPath, _ in - guard let self = self, - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: SchedulePickerCell.identifier, for: indexPath - ) as? SchedulePickerCell else { - return UICollectionViewCell() - } - cell.configureHeader(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders) - cell.configureTableRoundness(for: indexPath, dateHeaders: dateHeaders, timeHeaders: timeHeaders) - self.schedulePicker1.configureCellBackground(cell, for: indexPath, participants: participants) - return cell - } - ) - - let dataSource2 = RxCollectionViewSectionedReloadDataSource>( - configureCell: { [weak self] _, collectionView, indexPath, _ in - guard let self = self, - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: SchedulePickerCell.identifier, for: indexPath - ) as? SchedulePickerCell else { - return UICollectionViewCell() - } - cell.configureHeader(for: indexPath, dateHeaders: dateHeaders2, timeHeaders: timeHeaders2) - cell.configureTableRoundness(for: indexPath, dateHeaders: dateHeaders2, timeHeaders: timeHeaders2) - self.schedulePicker2.configureCellBackground(cell, for: indexPath, participants: participants) - self.schedulePicker2.updateCellAvailability(with: mockDateList, startTimes: mockMemberStartTime) - return cell - } - ) - items - .bind(to: schedulePicker1.rx.items(dataSource: dataSource)) - .disposed(by: disposeBag) - items2 - .bind(to: schedulePicker2.rx.items(dataSource: dataSource2)) - .disposed(by: disposeBag) - - schedulePicker1.rx.itemSelected - .subscribe(onNext: { [weak self] indexPath in - guard let self = self else { return } - let row = indexPath.item / totalColumns - let column = indexPath.item % totalColumns - - guard row > 0, column > 0 else { - self.schedulePicker1.deselectItem(at: indexPath, animated: true) // 첫 번째 행과 첫 번째 열은 선택 불가 - return - } - self.schedulePicker1.addSelectedCell(at: indexPath) - }) - .disposed(by: disposeBag) + // Do any additional setup after loading the view. } } From d6240f11aaff9a33a424f2be86407c23d2b29822 Mon Sep 17 00:00:00 2001 From: luckyyy Date: Tue, 4 Feb 2025 02:13:40 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Feat/#30]=20=EB=94=94=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroupDetail/View/Cell/ConfirmedCVC.swift | 14 ++++++------ .../GroupDetail/View/Cell/InProgressCVC.swift | 7 +++--- .../View/Cell/ScheduleCellReactor.swift | 22 ------------------- .../GroupDetail/View/GroupDetailView.swift | 9 ++++---- .../ViewController/GroupDetailReactor.swift | 15 ------------- .../GroupDetailViewController.swift | 8 ++----- 6 files changed, 18 insertions(+), 57 deletions(-) delete mode 100644 Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift index 06d9933..b4166b4 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift @@ -48,13 +48,13 @@ final class ConfirmedCVC: UICollectionViewCell, View { } scheduleTitleLabel.do { - $0.font = .PretendardStyle.b4_sb.font + $0.font = .PretendardStyle.b1_sb.font $0.textColor = .appGray900 } timeLabel.do { - $0.font = .PretendardStyle.b5_r.font - $0.textColor = .appGray800 + $0.font = .PretendardStyle.c3_r.font + $0.textColor = .appGray700 } divider.do { @@ -65,23 +65,23 @@ final class ConfirmedCVC: UICollectionViewCell, View { // MARK: setUpLayout private func setUpLayout() { chip.snp.makeConstraints { - $0.top.equalToSuperview().offset(20) + $0.top.equalToSuperview().offset(15) $0.leading.equalToSuperview().offset(6) $0.size.equalTo(13) } scheduleTitleLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(15) + $0.top.equalToSuperview().offset(10) $0.leading.equalTo(chip.snp.trailing).offset(9) } timeLabel.snp.makeConstraints { - $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(1) + $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(4) $0.leading.equalTo(scheduleTitleLabel) } divider.snp.makeConstraints { - $0.top.equalTo(timeLabel.snp.bottom).offset(15) + $0.bottom.equalToSuperview() $0.horizontalEdges.equalToSuperview() $0.height.equalTo(1) } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift index 11c5972..f6d11cf 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift @@ -96,12 +96,13 @@ final class InProgressCVC: UICollectionViewCell, View { } moveButton.snp.makeConstraints { - $0.top.trailing.equalToSuperview().inset(22) + $0.centerY.equalTo(scheduleTitleLabel) + $0.trailing.equalToSuperview().inset(22) $0.size.equalTo(18) } timeLabel.snp.makeConstraints { - $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(10) + $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(13) $0.leading.equalTo(scheduleTitleLabel) } @@ -129,7 +130,7 @@ extension InProgressCVC { let schedule = reactor.currentState.schedule scheduleTitleLabel.text = schedule.schedule.name timeLabel.text = NSTDateUtility.durationList(schedule.startTime, schedule.endTime) - availabilityLabel.text = "\(schedule.availableMemberCount) / \(schedule.groupMemberCount)" + availabilityLabel.text = "\(schedule.availableMemberCount)명/ \(schedule.groupMemberCount)명" progressBar.snp.makeConstraints { $0.top.equalTo(timeLabel.snp.bottom).offset(10) $0.leading.equalTo(backgroundProgressBar) diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift deleted file mode 100644 index 7bc3cb9..0000000 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ScheduleCellReactor.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ScheduleCellReactor.swift -// Noostak_iOS -// -// Created by 오연서 on 2/3/25. -// - -import ReactorKit -import RxSwift - -final class ScheduleCellReactor: Reactor { - typealias Action = NoAction - struct State { - let schedule: ExtendedSchedule - } - - let initialState: State - - init(schedule: ExtendedSchedule) { - self.initialState = State(schedule: schedule) - } -} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift index 35a5170..7fd2b5a 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift @@ -31,9 +31,10 @@ final class GroupDetailView: UIView { // MARK: Init override init(frame: CGRect) { - let layout = UICollectionViewFlowLayout() - self.inProgressCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - self.confirmedCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + let inProgressLayout = UICollectionViewFlowLayout() + let confirmedLayout = UICollectionViewFlowLayout() + self.inProgressCollectionView = UICollectionView(frame: .zero, collectionViewLayout: inProgressLayout) + self.confirmedCollectionView = UICollectionView(frame: .zero, collectionViewLayout: confirmedLayout) super.init(frame: frame) setUpFoundation() setUpHierarchy() @@ -50,7 +51,7 @@ final class GroupDetailView: UIView { private func setUpHierarchy() { [profileImageView, groupNameLabel, shareButton, groupMemberButton, scheduleListLabel, segmentedControl, underlineView, selectedUnderlineView, - defaultLabel, inProgressCollectionView, confirmedCollectionView].forEach { + inProgressCollectionView, confirmedCollectionView, defaultLabel].forEach { self.addSubview($0) } } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift index f2b5d79..f8d2093 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift @@ -46,21 +46,6 @@ final class GroupDetailReactor: Reactor { return Observable.just(.setConfirmedData(reactors)) } } - -// func mutate(action: Action) -> Observable { -// switch action { -// case .selectSegment(let index): -// return Observable.just(.setSelectedSegment(index)) -// -// case .loadInProgressData: -// return Observable.just(.setInProgressData(mockInProgressData.map { InProgressCellReactor(schedule: $0) })) -// .observe(on: MainScheduler.instance) -// -// case .loadConfirmedData: -// return Observable.just(.setConfirmedData(mockConfirmedData.map { ConfirmedCellReactor(schedule: $0) })) -// .observe(on: MainScheduler.instance) -// } -// } func reduce(state: State, mutation: Mutation) -> State { var newState = state diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift index 9b51460..1732304 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift @@ -78,12 +78,9 @@ final class GroupDetailViewController: UIViewController, View { self.rootView.inProgressCollectionView.isHidden = !isProgress self.rootView.confirmedCollectionView.isHidden = isProgress - let inProgressEmpty = state.inProgressCellReactors.isEmpty - let confirmedEmpty = state.confirmedCellReactors.isEmpty - - self.rootView.defaultLabel.isHidden = !(isProgress ? inProgressEmpty : confirmedEmpty) + let noData = isProgress ? state.inProgressCellReactors.isEmpty : state.confirmedCellReactors.isEmpty self.rootView.defaultLabel.text = isProgress ? "현재 진행중인 약속이 없어요" : "아직 확정된 약속이 없어요" - self.rootView.layoutIfNeeded() + self.rootView.defaultLabel.isHidden = !noData }) .disposed(by: disposeBag) @@ -109,7 +106,6 @@ final class GroupDetailViewController: UIViewController, View { } extension GroupDetailViewController: UICollectionViewDelegateFlowLayout { - /// 순서가 왜바껴쓸까.. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { switch collectionView { case rootView.inProgressCollectionView: From 1fcba7534da856035fb2998e2dbf25525b02ded2 Mon Sep 17 00:00:00 2001 From: luckyyy Date: Sun, 9 Feb 2025 00:06:15 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[Feat/#30]=20PR=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents.json | 0 .../Vector.svg | 0 .../Global/Utils/NSTDateUtility.swift | 46 ++++++++----------- .../ConfirmedCellReactor.swift | 0 .../GroupDetailReactor.swift | 0 .../InProgressCellReactor.swift | 0 .../GroupDetail/View/Cell/InProgressCVC.swift | 8 ++-- .../GroupDetail/View/GroupDetailView.swift | 26 +++++------ .../GroupDetailViewController.swift | 23 +++------- 9 files changed, 41 insertions(+), 62 deletions(-) rename Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/{ic_vector.imageset => ic_arrow_right.imageset}/Contents.json (100%) rename Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/{ic_vector.imageset => ic_arrow_right.imageset}/Vector.svg (100%) rename Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/{View/Cell => Reactor}/ConfirmedCellReactor.swift (100%) rename Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/{ViewController => Reactor}/GroupDetailReactor.swift (100%) rename Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/{View/Cell => Reactor}/InProgressCellReactor.swift (100%) diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Contents.json similarity index 100% rename from Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Contents.json rename to Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Contents.json diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Vector.svg similarity index 100% rename from Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_vector.imageset/Vector.svg rename to Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Vector.svg diff --git a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift index f473d86..2797acb 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift @@ -53,6 +53,8 @@ public extension NSTDateUtility { case yyyyMM case EE case HH + case HHmm + case EEMMdd case MMddEE var format: String { @@ -69,8 +71,12 @@ public extension NSTDateUtility { return "EE" case .HH: return "HH" - case .MMddEE: + case .HHmm: + return "HH:mm" + case .EEMMdd: return "EE\nMM/dd" + case .MMddEE: + return "M월 d일 (EE)" } } } @@ -91,7 +97,7 @@ extension NSTDateUtility { ///타임테이블 뷰 : "요일 월/일" static func dateList(_ dateStrings: [String]) -> [String] { let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 - let displayFormatter = NSTDateUtility(format: .MMddEE) // 출력 형식 + let displayFormatter = NSTDateUtility(format: .EEMMdd) // 출력 형식 return dateStrings.compactMap { dateString in switch formatter.date(from: dateString) { @@ -129,33 +135,19 @@ extension NSTDateUtility { return result } - ///약속상세뷰 : "9월 7일 (일) 10:00~12:00" static func durationList(_ startTime: String, _ endTime: String) -> String { let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "ko_KR") - dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") - - // 날짜 포맷: "9월 7일 (일)" - dateFormatter.dateFormat = "M월 d일 (E)" - - // 시간 포맷: "10:00" - let timeFormatter = DateFormatter() - timeFormatter.locale = Locale(identifier: "ko_KR") - timeFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") - timeFormatter.dateFormat = "HH:mm" - - switch (formatter.date(from: startTime), formatter.date(from: endTime)) { - case (.success(let start), .success(let end)): - let dateString = dateFormatter.string(from: start) // "9월 7일 (일)" - let startTimeString = timeFormatter.string(from: start) // "10:00" - let endTimeString = timeFormatter.string(from: end) // "12:00" - - return "\(dateString) \(startTimeString)~\(endTimeString)" - - default: - print("Failed to parse start or end time.") - return "" + let dateFormatter = NSTDateUtility(format: .MMddEE) // "9월 7일 (일)" + let timeFormatter = NSTDateUtility(format: .HHmm) // "10:00" + + let startDateResult = formatter.date(from: startTime) + let endDateResult = formatter.date(from: endTime) + + guard case .success(let startDate) = startDateResult, + case .success(let endDate) = endDateResult else { + return "Invalid date format" } + return "\(dateFormatter.string(from: startDate)) \(timeFormatter.string(from: startDate))~\(timeFormatter.string(from: endDate))" } + } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/ConfirmedCellReactor.swift similarity index 100% rename from Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCellReactor.swift rename to Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/ConfirmedCellReactor.swift diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift similarity index 100% rename from Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailReactor.swift rename to Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/InProgressCellReactor.swift similarity index 100% rename from Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCellReactor.swift rename to Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/InProgressCellReactor.swift diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift index f6d11cf..6f7acc7 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift @@ -24,7 +24,7 @@ final class InProgressCVC: UICollectionViewCell, View { private let availabilityLabel = UILabel() private let backgroundProgressBar = UIView() private let progressBar = UIView() - + // MARK: Init override init(frame: CGRect) { super.init(frame: frame) @@ -60,7 +60,7 @@ final class InProgressCVC: UICollectionViewCell, View { } moveButton.do { - $0.setImage(.icVector, for: .normal) + $0.setImage(.icArrowRight, for: .normal) } timeLabel.do { @@ -130,11 +130,11 @@ extension InProgressCVC { let schedule = reactor.currentState.schedule scheduleTitleLabel.text = schedule.schedule.name timeLabel.text = NSTDateUtility.durationList(schedule.startTime, schedule.endTime) - availabilityLabel.text = "\(schedule.availableMemberCount)명/ \(schedule.groupMemberCount)명" + availabilityLabel.text = "\(schedule.availableMemberCount)명/\(schedule.groupMemberCount)명" progressBar.snp.makeConstraints { $0.top.equalTo(timeLabel.snp.bottom).offset(10) $0.leading.equalTo(backgroundProgressBar) - $0.width.equalTo(50) + $0.width.equalTo((Int(self.frame.width) - 32) * schedule.availableMemberCount/schedule.groupMemberCount) $0.height.equalTo(4) } } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift index 7fd2b5a..478f568 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift @@ -12,7 +12,7 @@ import RxSwift import RxCocoa final class GroupDetailView: UIView { - + // MARK: Properties private let disposeBag = DisposeBag() @@ -26,15 +26,11 @@ final class GroupDetailView: UIView { private let underlineView = UIView() private let selectedUnderlineView = UIView() let defaultLabel = UILabel() - let inProgressCollectionView: UICollectionView - let confirmedCollectionView: UICollectionView - + var inProgressCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + var confirmedCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + // MARK: Init override init(frame: CGRect) { - let inProgressLayout = UICollectionViewFlowLayout() - let confirmedLayout = UICollectionViewFlowLayout() - self.inProgressCollectionView = UICollectionView(frame: .zero, collectionViewLayout: inProgressLayout) - self.confirmedCollectionView = UICollectionView(frame: .zero, collectionViewLayout: confirmedLayout) super.init(frame: frame) setUpFoundation() setUpHierarchy() @@ -42,16 +38,16 @@ final class GroupDetailView: UIView { setUpLayout() bindSegmentControl() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: setUpHierarchy private func setUpHierarchy() { [profileImageView, groupNameLabel, shareButton, groupMemberButton, scheduleListLabel, segmentedControl, underlineView, selectedUnderlineView, - inProgressCollectionView, confirmedCollectionView, defaultLabel].forEach { + inProgressCollectionView, confirmedCollectionView, defaultLabel].forEach { self.addSubview($0) } } @@ -82,12 +78,12 @@ final class GroupDetailView: UIView { config.baseForegroundColor = .appGray800 config.attributedTitle = AttributedString("그룹 멤버 8", attributes: AttributeContainer([.font: UIFont.PretendardStyle.b2_r.font])) - config.image = .icVector + config.image = .icArrowRight config.imagePlacement = .trailing config.imagePadding = 5 config.contentInsets = .zero $0.configuration = config - + } scheduleListLabel.do { @@ -126,12 +122,12 @@ final class GroupDetailView: UIView { inProgressCollectionView.do { $0.isHidden = false } - + confirmedCollectionView.do { $0.isHidden = true } } - + // MARK: setUpLayout private func setUpLayout() { profileImageView.snp.makeConstraints { diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift index 1732304..bb6478e 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift @@ -45,9 +45,6 @@ final class GroupDetailViewController: UIViewController, View { rootView.confirmedCollectionView.register(ConfirmedCVC.self, forCellWithReuseIdentifier: ConfirmedCVC.identifier) rootView.inProgressCollectionView.rx.setDelegate(self).disposed(by: disposeBag) rootView.confirmedCollectionView.rx.setDelegate(self).disposed(by: disposeBag) - DispatchQueue.main.async { - self.rootView.layoutIfNeeded() - } } // MARK: - Bind Reactor @@ -55,17 +52,10 @@ final class GroupDetailViewController: UIViewController, View { rootView.segmentedControl.rx.selectedSegmentIndex .distinctUntilChanged() .flatMapLatest { index -> Observable in - if index == 0 { - return Observable.concat([ - .just(.selectSegment(index)), - .just(.loadInProgressData) - ]) - } else { - return Observable.concat([ - .just(.selectSegment(index)), - .just(.loadConfirmedData) - ]) - } + Observable.concat([ + .just(.selectSegment(index)), + .just(index == 0 ? .loadInProgressData : .loadConfirmedData) + ]) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -90,7 +80,7 @@ final class GroupDetailViewController: UIViewController, View { .do(onNext: { _ in self.rootView.inProgressCollectionView.reloadData() }) - .bind(to: rootView.inProgressCollectionView.rx.items(cellIdentifier: InProgressCVC.identifier, cellType: InProgressCVC.self)) { index, reactor, cell in + .bind(to: rootView.inProgressCollectionView.rx.items(cellIdentifier: InProgressCVC.identifier, cellType: InProgressCVC.self)) { _, reactor, cell in cell.reactor = reactor } .disposed(by: disposeBag) @@ -98,13 +88,14 @@ final class GroupDetailViewController: UIViewController, View { // 확정된 약속 바인딩 reactor.state.map { $0.confirmedCellReactors } .do(onNext: { _ in self.rootView.confirmedCollectionView.reloadData() }) - .bind(to: rootView.confirmedCollectionView.rx.items(cellIdentifier: ConfirmedCVC.identifier, cellType: ConfirmedCVC.self)) { index, reactor, cell in + .bind(to: rootView.confirmedCollectionView.rx.items(cellIdentifier: ConfirmedCVC.identifier, cellType: ConfirmedCVC.self)) { _, reactor, cell in cell.reactor = reactor } .disposed(by: disposeBag) } } +// MARK: - CollectionViewDelegateFlowLayout extension GroupDetailViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { switch collectionView {