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))"
+ }
+}