diff --git a/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift b/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift index f786419..bc2ebf8 100644 --- a/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift +++ b/Noostak_iOS/Noostak_iOS/Domain/Entity/Schedule.swift @@ -48,6 +48,8 @@ struct ExtendedSchedule { let startTime: String ///약속 종료시각(1순위, 확정) let endTime: String + ///나의 가능 여부 + let myInfo: MemberStatus ///가능한 친구 let availableMembers: [User] ///불가능한 친구 diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/MemberAvailabilityChip.swift b/Noostak_iOS/Noostak_iOS/Global/Components/MemberAvailabilityChip.swift index 62b26dd..9e75202 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Components/MemberAvailabilityChip.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Components/MemberAvailabilityChip.swift @@ -29,6 +29,13 @@ final class MemberAvailabilityChip: UIView { setUpLayout() } + func update(name: String, status: MemberStatus) { + self.chipLabel.text = name + self.status = status + setUpUI() + setUpLayout() + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Cell/MemberAvailabilityCVC.swift b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Cell/MemberAvailabilityCVC.swift new file mode 100644 index 0000000..6637b43 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Cell/MemberAvailabilityCVC.swift @@ -0,0 +1,50 @@ +// +// MemberAvailabilityCVC.swift +// Noostak_iOS +// +// Created by 오연서 on 2/11/25. +// + +import UIKit +import SnapKit +import Then +import ReactorKit + +final class MemberAvailabilityCVC: UICollectionViewCell, View { + + // MARK: Properties + static let identifier = "MemberAvailabilityCVC" + var disposeBag = DisposeBag() + + // MARK: Views + var chip = MemberAvailabilityChip(name: "", status: .available) + + // MARK: Init + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: setUpHierarchy + private func setUpHierarchy() { + self.addSubview(chip) + } + + // MARK: setUpLayout + private func setUpLayout() { + chip.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} + +extension MemberAvailabilityCVC { + func bind(reactor: MemberAvailabilityCellReactor) { + self.chip.update(name: reactor.currentState.user.name, status: reactor.currentState.status) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Reactor/MemberAvailabilityCellReactor.swift b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Reactor/MemberAvailabilityCellReactor.swift new file mode 100644 index 0000000..7d4fc00 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/Reactor/MemberAvailabilityCellReactor.swift @@ -0,0 +1,23 @@ +// +// MemberAvailabilityCellReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/11/25. +// + +import ReactorKit +import RxSwift + +final class MemberAvailabilityCellReactor: Reactor { + typealias Action = NoAction + struct State { + let user: User + let status: MemberStatus + } + + let initialState: State + + init(user: User, status: MemberStatus) { + self.initialState = State(user: user, status: status) + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/ScheduleInfoView.swift b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/ScheduleInfoView.swift new file mode 100644 index 0000000..65f8ee8 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleInfoView/ScheduleInfoView.swift @@ -0,0 +1,219 @@ +// +// ScheduleInfoView.swift +// Noostak_iOS +// +// Created by 오연서 on 2/11/25. +// + +import UIKit +import Then +import SnapKit +import RxSwift +import RxCocoa +import RxDataSources +import ReactorKit + +enum ScheduleState { + case inProgress + case confirmed +} + +final class ScheduleInfoView: UIView { + // MARK: Properties + var disposeBag = DisposeBag() + private var state: ScheduleState + + // MARK: Views + let scheduleDurationLabel = UILabel() + let likeButton = LikeButton() + private let scheduleInfoView = UIView() + private let scheduleTimeTitleLabel = UILabel() + let scheduleTimeLabel = UILabel() + private let scheduleCategoryLabel = UILabel() + var scheduleCategoryChip = ScheduleCategoryButton(category: .hobby, buttonType: .ReadOnly) + let availableLabel = UILabel() + var availableCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + let unavailableLabel = UILabel() + var unavailableCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + // MARK: Init + init(state: ScheduleState) { + self.state = state + super.init(frame: .zero) + setUpFoundation() + setUpHierarchy() + setUpUI() + setUpLayout() + updateUI(isInProgress: state == .inProgress) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: setUpHierarchy + private func setUpHierarchy() { + self.addSubview(scheduleInfoView) + [scheduleDurationLabel, likeButton, scheduleTimeTitleLabel, scheduleTimeLabel, + scheduleCategoryLabel, scheduleCategoryChip, + availableLabel, availableCollectionView, + unavailableLabel, unavailableCollectionView].forEach { + scheduleInfoView.addSubview($0) + } + } + + private func setUpFoundation() { + self.backgroundColor = .appWhite + } + + // MARK: setUpUI + private func setUpUI() { + scheduleDurationLabel.do { + $0.font = .PretendardStyle.t4_b.font + $0.textColor = .appBlack + } + + likeButton.do { + $0.isHidden = true + } + + scheduleInfoView.do { + $0.layer.cornerRadius = 20 + $0.layer.borderColor = UIColor.appGray100.cgColor + $0.layer.borderWidth = 1 + } + + scheduleTimeTitleLabel.do { + $0.text = "약속 시간" + $0.font = .PretendardStyle.c3_r.font + $0.textColor = .appBlack + } + + scheduleTimeLabel.do { + $0.font = .PretendardStyle.b4_sb.font + $0.textColor = .appBlack + } + + scheduleCategoryLabel.do { + $0.text = "약속 유형" + $0.font = .PretendardStyle.c3_r.font + $0.textColor = .appBlack + } + + availableLabel.do { + $0.font = .PretendardStyle.c3_r.font + $0.textColor = .appBlack + } + + unavailableLabel.do { + $0.font = .PretendardStyle.c3_r.font + $0.textColor = .appBlack + } + } + + // MARK: setUpLayout + private func setUpLayout() { + scheduleInfoView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.bottom.equalTo(unavailableCollectionView.snp.bottom).offset(16) + } + + scheduleDurationLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.leading.equalToSuperview().offset(16) + } + + likeButton.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.trailing.equalToSuperview().inset(16) + $0.height.equalTo(30) + $0.width.equalTo(50) + } + + scheduleTimeTitleLabel.snp.makeConstraints { + $0.top.leading.equalToSuperview().offset(16) + } + + scheduleTimeLabel.snp.makeConstraints { + $0.centerY.equalTo(scheduleTimeTitleLabel) + $0.trailing.equalToSuperview().inset(16) + } + + scheduleCategoryLabel.snp.makeConstraints { + $0.top.equalTo(scheduleTimeTitleLabel.snp.bottom).offset(26) + $0.leading.equalTo(scheduleTimeTitleLabel) + } + + scheduleCategoryChip.snp.makeConstraints { + $0.centerY.equalTo(scheduleCategoryLabel) + $0.trailing.equalTo(scheduleTimeLabel) + $0.height.equalTo(30) + $0.width.equalTo(53) + } + + self.state == .inProgress ? + availableLabel.snp.makeConstraints { + $0.top.equalTo(scheduleDurationLabel.snp.bottom).offset(16) + $0.leading.equalTo(scheduleDurationLabel) + } : availableLabel.snp.makeConstraints { + $0.top.equalTo(scheduleCategoryLabel.snp.bottom).offset(26) + $0.leading.equalTo(scheduleTimeTitleLabel) + } + + availableCollectionView.snp.makeConstraints { + $0.top.equalTo(availableLabel.snp.bottom).offset(10) + $0.horizontalEdges.equalToSuperview().inset(16) + } + + unavailableLabel.snp.makeConstraints { + $0.top.equalTo(availableCollectionView.snp.bottom).offset(20) + $0.leading.equalTo(scheduleTimeTitleLabel) + } + + unavailableCollectionView.snp.makeConstraints { + $0.top.equalTo(unavailableLabel.snp.bottom).offset(10) + $0.horizontalEdges.equalToSuperview().inset(16) + } + } + + private func updateUI(isInProgress: Bool) { + [scheduleDurationLabel, likeButton].forEach { $0.isHidden = !isInProgress } + [scheduleTimeTitleLabel, scheduleTimeLabel, scheduleCategoryLabel, scheduleCategoryChip].forEach { $0.isHidden = isInProgress } + } +} + +// MARK: - 셀 좌측 정렬 +final class LeftAlignedFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil } + + var rowAttributes: [UICollectionViewLayoutAttributes] = [] + var previousY: CGFloat = -1 + var rowStartX: CGFloat = 0 + + for attribute in attributes { + let frame = attribute.frame + let currentY = frame.origin.y + + if currentY != previousY { + alignRow(rowAttributes, rowStartX: rowStartX) + rowAttributes.removeAll() + rowStartX = sectionInset.left + } + rowAttributes.append(attribute) + previousY = currentY + } + alignRow(rowAttributes, rowStartX: rowStartX) // 마지막 줄 정렬 + return attributes + } + + func alignRow(_ rowAttributes: [UICollectionViewLayoutAttributes], rowStartX: CGFloat) { + guard !rowAttributes.isEmpty else { return } + + var currentX = rowStartX + for attribute in rowAttributes { + attribute.frame.origin.x = currentX + currentX += attribute.frame.width + minimumInteritemSpacing + } + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/Contents.json new file mode 100644 index 0000000..c4a28e6 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_profile_camera.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/ic_profile_camera.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/ic_profile_camera.svg new file mode 100644 index 0000000..a62ff66 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/icn_profile_camera.imageset/ic_profile_camera.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift index d075e30..e32bad4 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift @@ -56,6 +56,7 @@ public extension NSTDateUtility { case HHmm case EEMMdd case MMddEE + case MMddHHmm var format: String { switch self { @@ -77,6 +78,8 @@ public extension NSTDateUtility { return "EE\nMM/dd" case .MMddEE: return "M월 d일 (EE)" + case .MMddHHmm: + return "MM/dd HH:mm" } } } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift index f8d2093..9a435be 100644 --- a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift @@ -73,6 +73,7 @@ let mockInProgressData: [ExtendedSchedule] = [ date: "2024-09-05T10:00:00", startTime: "2024-09-05T10:00:00", endTime: "2024-09-05T18:00:00", + myInfo: .available, availableMembers: [], unavailableMembers: [], groupMemberCount: 24, @@ -86,6 +87,7 @@ let mockInProgressData: [ExtendedSchedule] = [ date: "2024-09-05T10:00:00", startTime: "2024-09-05T10:00:00", endTime: "2024-09-05T18:00:00", + myInfo: .available, availableMembers: [], unavailableMembers: [], groupMemberCount: 23, @@ -99,6 +101,7 @@ let mockInProgressData: [ExtendedSchedule] = [ date: "2024-09-05T10:00:00", startTime: "2024-09-05T10:00:00", endTime: "2024-09-05T18:00:00", + myInfo: .available, availableMembers: [], unavailableMembers: [], groupMemberCount: 24, @@ -114,6 +117,7 @@ let mockConfirmedData: [ExtendedSchedule] = [ date: "2024-09-05T10:00:00", startTime: "2024-09-05T10:00:00", endTime: "2024-09-05T18:00:00", + myInfo: .available, availableMembers: [], unavailableMembers: [], groupMemberCount: 24, @@ -127,6 +131,7 @@ let mockConfirmedData: [ExtendedSchedule] = [ date: "2024-09-05T10:00:00", startTime: "2024-09-05T10:00:00", endTime: "2024-09-05T18:00:00", + myInfo: .available, availableMembers: [], unavailableMembers: [], groupMemberCount: 23, diff --git a/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/Reactor/ScheduleConfirmedReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/Reactor/ScheduleConfirmedReactor.swift new file mode 100644 index 0000000..381ce66 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/Reactor/ScheduleConfirmedReactor.swift @@ -0,0 +1,54 @@ +// +// ScheduleConfirmedReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/11/25. +// + +import ReactorKit +import RxSwift + +final class ScheduleConfirmedReactor: Reactor { + enum Action { + case loadSchedule + } + + enum Mutation { + case setSchedule(ExtendedSchedule) + } + + struct State { + var schedule: ExtendedSchedule + var myInfo: MemberStatus + var availableMembers: [User] + var unavailableMembers: [User] + } + + let initialState: State + + init() { + self.initialState = State(schedule: mockExtendedSchedule, + myInfo: mockExtendedSchedule.myInfo, + availableMembers: mockExtendedSchedule.availableMembers, + unavailableMembers: mockExtendedSchedule.unavailableMembers) + } + + func mutate(action: Action) -> Observable { + switch action { + case .loadSchedule: + return Observable.just(.setSchedule(initialState.schedule)) + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setSchedule(let schedule): + newState.schedule = schedule + newState.myInfo = schedule.myInfo + newState.availableMembers = schedule.availableMembers + newState.unavailableMembers = schedule.unavailableMembers + } + return newState + } + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/View/ScheduleConfirmedView.swift b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/View/ScheduleConfirmedView.swift new file mode 100644 index 0000000..d7fdb3d --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/View/ScheduleConfirmedView.swift @@ -0,0 +1,111 @@ +// +// ScheduleConfirmedView.swift +// Noostak_iOS +// +// Created by 오연서 on 2/10/25. +// + +import UIKit +import Then +import SnapKit +import RxSwift +import RxCocoa + +final class ScheduleConfirmedView: UIView { + + // MARK: Properties + private let disposeBag = DisposeBag() + + // MARK: Views + private let scrollView = UIScrollView() + private let contentView = UIView() + private let scheduleInfoLabel = UILabel() + let scheduleInfoView = ScheduleInfoView(state: .confirmed) + + // 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() { + self.addSubview(scrollView) + scrollView.addSubview(contentView) + [scheduleInfoLabel, scheduleInfoView].forEach { + contentView.addSubview($0) + } + } + + private func setUpFoundation() { + self.backgroundColor = .appWhite + } + + // MARK: setUpUI + private func setUpUI() { + scheduleInfoLabel.do { + $0.text = "약속 정보" + $0.font = .PretendardStyle.t4_b.font + $0.textColor = .appBlack + } + + scheduleInfoView.do { + $0.layer.cornerRadius = 20 + $0.layer.borderColor = UIColor.appGray100.cgColor + $0.layer.borderWidth = 1 + } + } + + // 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(scheduleInfoView.snp.bottom) + } + + scheduleInfoLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(12) + $0.leading.equalToSuperview().offset(16) + } + + scheduleInfoView.snp.makeConstraints { + $0.top.equalTo(scheduleInfoLabel.snp.bottom).offset(12) + $0.horizontalEdges.equalToSuperview().inset(16) + } + } +} + +let mockExtendedSchedule = ExtendedSchedule(schedule: Schedule(id: 1, + name: "누탁", + category: .hobby, + selectionDates: [], + selectionStartTime: Date(), + selectionEndTime: Date()), + date: "2024-09-07T00:00:00", + startTime: "2024-09-07T11:00:00", + endTime: "2024-09-07T14:00:00", + myInfo: .unavailable, + availableMembers: [User(name: "안녕", userImage: ""), + User(name: "안녕안", userImage: ""), + User(name: "안녕안녕", userImage: "")], + unavailableMembers: [User(name: "알료", userImage: ""), + User(name: "언넝", userImage: ""), + User(name: "언넝언넝언넝", userImage: ""), + User(name: "언넝", userImage: ""), + User(name: "언넝언넝", userImage: "") + ], + groupMemberCount: 10, + availableMemberCount: 3) diff --git a/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/ViewController/ScheduleConfirmedViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/ViewController/ScheduleConfirmedViewController.swift new file mode 100644 index 0000000..908ca60 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/ScheduleConfirmed/ViewController/ScheduleConfirmedViewController.swift @@ -0,0 +1,180 @@ +// +// ScheduleConfirmedViewController.swift +// Noostak_iOS +// +// Created by 오연서 on 2/11/25. +// + +import UIKit +import ReactorKit +import RxSwift +import RxCocoa +import RxDataSources + +final class ScheduleConfirmedViewController: UIViewController, View { + // MARK: - Properties + var disposeBag = DisposeBag() + private let rootView = ScheduleConfirmedView() + + // MARK: - Init + init(reactor: ScheduleConfirmedReactor) { + 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() + bindCollectionViewHeight() + } + + private func setUpFoundation() { + self.view.backgroundColor = .white + } + + private func setUpCollectionView() { + let availableFlowLayout = LeftAlignedFlowLayout() + availableFlowLayout.minimumInteritemSpacing = 10 + availableFlowLayout.minimumLineSpacing = 10 + + let unavailableFlowLayout = LeftAlignedFlowLayout() + unavailableFlowLayout.minimumInteritemSpacing = 10 + unavailableFlowLayout.minimumLineSpacing = 10 + + rootView.scheduleInfoView.availableCollectionView.setCollectionViewLayout(availableFlowLayout, animated: false) + rootView.scheduleInfoView.unavailableCollectionView.setCollectionViewLayout(unavailableFlowLayout, animated: false) + + rootView.scheduleInfoView.availableCollectionView.register(MemberAvailabilityCVC.self, forCellWithReuseIdentifier: MemberAvailabilityCVC.identifier) + rootView.scheduleInfoView.unavailableCollectionView.register(MemberAvailabilityCVC.self, forCellWithReuseIdentifier: MemberAvailabilityCVC.identifier) + rootView.scheduleInfoView.availableCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + rootView.scheduleInfoView.unavailableCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + } + + private func bindCollectionViewHeight() { + rootView.scheduleInfoView.availableCollectionView.snp.makeConstraints { make in + make.height.equalTo(0) + } + rootView.scheduleInfoView.unavailableCollectionView.snp.makeConstraints { make in + make.height.equalTo(0) + } + + rootView.scheduleInfoView.availableCollectionView.rx.observe(CGSize.self, "contentSize") + .subscribe(onNext: { [weak self] size in + guard let height = size?.height, height > 0, let self = self else { return } + self.rootView.scheduleInfoView.availableCollectionView.snp.updateConstraints { + $0.height.equalTo(height) + } + self.rootView.layoutIfNeeded() + }) + .disposed(by: disposeBag) + + rootView.scheduleInfoView.unavailableCollectionView.rx.observe(CGSize.self, "contentSize") + .subscribe(onNext: { [weak self] size in + guard let height = size?.height, height > 0, let self = self else { return } + self.rootView.scheduleInfoView.unavailableCollectionView.snp.updateConstraints { + $0.height.equalTo(height) + } + self.rootView.layoutIfNeeded() + }) + .disposed(by: disposeBag) + } + + func bind(reactor: ScheduleConfirmedReactor) { + let myStatus = reactor.currentState.schedule.myInfo + var availableMembers = reactor.currentState.availableMembers + var unavailableMembers = reactor.currentState.unavailableMembers + + myStatus == .available ? availableMembers.insert(User(name: "나", userImage: ""), at: 0) : + unavailableMembers.insert(User(name: "나", userImage: ""), at: 0) + + let availableDataSource = RxCollectionViewSectionedReloadDataSource>( + configureCell: { _, collectionView, indexPath, user in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MemberAvailabilityCVC.identifier, for: indexPath) as! MemberAvailabilityCVC + let status: MemberStatus = (indexPath.row == 0 && myStatus == .available) ? .myself : .available + cell.reactor = MemberAvailabilityCellReactor(user: user, status: status) + return cell + } + ) + + let unavailableDataSource = RxCollectionViewSectionedReloadDataSource>( + configureCell: { _, collectionView, indexPath, user in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MemberAvailabilityCVC.identifier, for: indexPath) as! MemberAvailabilityCVC + let status: MemberStatus = (indexPath.row == 0 && myStatus == .unavailable) ? .myself : .unavailable + cell.reactor = MemberAvailabilityCellReactor(user: user, status: status) + return cell + } + ) + + reactor.state.map { _ in [SectionModel(model: "Available", items: availableMembers)] } + .bind(to: rootView.scheduleInfoView.availableCollectionView.rx.items(dataSource: availableDataSource)) + .disposed(by: disposeBag) + + reactor.state.map { _ in [SectionModel(model: "Unavailable", items: unavailableMembers)] } + .bind(to: rootView.scheduleInfoView.unavailableCollectionView.rx.items(dataSource: unavailableDataSource)) + .disposed(by: disposeBag) + + reactor.state.map { $0.schedule } + .subscribe(onNext: { [weak self] schedule in + guard let self = self else { return } + self.rootView.scheduleInfoView.scheduleTimeLabel.text = "\(scheduleStartTime(schedule.startTime))" + self.rootView.scheduleInfoView.scheduleCategoryChip = ScheduleCategoryButton(category: schedule.schedule.category, buttonType: .ReadOnly) + self.rootView.scheduleInfoView.availableLabel.text = "가능한 친구 \(schedule.availableMembers.count)" + self.rootView.scheduleInfoView.unavailableLabel.text = "가능한 친구 \(schedule.unavailableMembers.count)" + + }) + .disposed(by: disposeBag) + } +} + +// MARK: - CollectionViewDelegateFlowLayout +extension ScheduleConfirmedViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + guard let reactor = self.reactor else { return CGSize(width: 39, height: 30) } + + let isAvailableCollection = (collectionView == rootView.scheduleInfoView.availableCollectionView) + let myStatus = reactor.currentState.schedule.myInfo + let isFirstCellMyself = (indexPath.row == 0) && ((isAvailableCollection && myStatus == .available) || (!isAvailableCollection && myStatus == .unavailable)) + let members = isAvailableCollection ? reactor.currentState.availableMembers : reactor.currentState.unavailableMembers + let user: User + + if isFirstCellMyself { + user = User(name: "나", userImage: "") + return CGSize(width: 39, height: 30) + } else { + let adjustedIndex = (isAvailableCollection && myStatus == .available) || (!isAvailableCollection && myStatus == .unavailable) ? indexPath.row - 1 : indexPath.row + user = members[adjustedIndex] + } + let attributes = [NSAttributedString.Key.font: UIFont.PretendardStyle.c3_r.font] + let estimatedFrame = (user.name as NSString).boundingRect( + with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30), + options: .usesLineFragmentOrigin, + attributes: attributes, + context: nil + ) + return CGSize(width: max(39, estimatedFrame.width + 18), height: 30) + } +} + +// MARK: - TimeFormatter +extension ScheduleConfirmedViewController { + func scheduleStartTime (_ startTime: String) -> String { + let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) + let timeFormatter = NSTDateUtility(format: .MMddHHmm) + + let startDateResult = formatter.date(from: startTime) + + guard case .success(let startDate) = startDateResult else { + return "Invalid date format" + } + return "\(timeFormatter.string(from: startDate))" + } +}