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_arrow_right.imageset/Contents.json b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Contents.json
new file mode 100644
index 0000000..ba0cba8
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.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_arrow_right.imageset/Vector.svg b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Vector.svg
new file mode 100644
index 0000000..9901a0e
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Global/Resources/Assets.xcassets/ic_arrow_right.imageset/Vector.svg
@@ -0,0 +1,3 @@
+
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/Utils/NSTDateUtility.swift b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift
index e3e029d..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)"
}
}
}
@@ -88,9 +94,10 @@ public extension NSTDateUtility {
}
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) {
@@ -102,7 +109,8 @@ extension NSTDateUtility {
}
}
}
-
+
+ ///타임테이블 뷰 : "00시"
static func timeList(_ startTime: String, _ endTime: String) -> [String] {
let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식
var result: [String] = []
@@ -126,4 +134,20 @@ extension NSTDateUtility {
}
return result
}
+
+ static func durationList(_ startTime: String, _ endTime: String) -> String {
+ let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식
+ 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/Dummy/ViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift
index 8ad6f11..601af3c 100644
--- a/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift
+++ b/Noostak_iOS/Noostak_iOS/Presentation/Dummy/ViewController.swift
@@ -7,11 +7,9 @@
import UIKit
-class ViewController: UIViewController {
-
+final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
-
}
diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/ConfirmedCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/ConfirmedCellReactor.swift
new file mode 100644
index 0000000..09a4a95
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/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/Reactor/GroupDetailReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift
new file mode 100644
index 0000000..f8d2093
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/GroupDetailReactor.swift
@@ -0,0 +1,134 @@
+//
+// 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 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/Reactor/InProgressCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/InProgressCellReactor.swift
new file mode 100644
index 0000000..434f07e
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/Reactor/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/ConfirmedCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/ConfirmedCVC.swift
new file mode 100644
index 0000000..b4166b4
--- /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.b1_sb.font
+ $0.textColor = .appGray900
+ }
+
+ timeLabel.do {
+ $0.font = .PretendardStyle.c3_r.font
+ $0.textColor = .appGray700
+ }
+
+ divider.do {
+ $0.backgroundColor = .appGray200
+ }
+ }
+
+ // MARK: setUpLayout
+ private func setUpLayout() {
+ chip.snp.makeConstraints {
+ $0.top.equalToSuperview().offset(15)
+ $0.leading.equalToSuperview().offset(6)
+ $0.size.equalTo(13)
+ }
+
+ scheduleTitleLabel.snp.makeConstraints {
+ $0.top.equalToSuperview().offset(10)
+ $0.leading.equalTo(chip.snp.trailing).offset(9)
+ }
+
+ timeLabel.snp.makeConstraints {
+ $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(4)
+ $0.leading.equalTo(scheduleTitleLabel)
+ }
+
+ divider.snp.makeConstraints {
+ $0.bottom.equalToSuperview()
+ $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/InProgressCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift
new file mode 100644
index 0000000..6f7acc7
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/Cell/InProgressCVC.swift
@@ -0,0 +1,141 @@
+//
+// 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(.icArrowRight, 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.centerY.equalTo(scheduleTitleLabel)
+ $0.trailing.equalToSuperview().inset(22)
+ $0.size.equalTo(18)
+ }
+
+ timeLabel.snp.makeConstraints {
+ $0.top.equalTo(scheduleTitleLabel.snp.bottom).offset(13)
+ $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((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
new file mode 100644
index 0000000..478f568
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/View/GroupDetailView.swift
@@ -0,0 +1,214 @@
+//
+// 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()
+ var inProgressCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
+ var confirmedCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
+
+ // MARK: Init
+ override init(frame: CGRect) {
+ 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,
+ inProgressCollectionView, confirmedCollectionView, defaultLabel].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 = .icArrowRight
+ 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/GroupDetailViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift
new file mode 100644
index 0000000..bb6478e
--- /dev/null
+++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupDetail/ViewController/GroupDetailViewController.swift
@@ -0,0 +1,121 @@
+//
+// 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)
+ }
+
+ // MARK: - Bind Reactor
+ func bind(reactor: GroupDetailReactor) {
+ rootView.segmentedControl.rx.selectedSegmentIndex
+ .distinctUntilChanged()
+ .flatMapLatest { index -> Observable in
+ Observable.concat([
+ .just(.selectSegment(index)),
+ .just(index == 0 ? .loadInProgressData : .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 noData = isProgress ? state.inProgressCellReactors.isEmpty : state.confirmedCellReactors.isEmpty
+ self.rootView.defaultLabel.text = isProgress ? "현재 진행중인 약속이 없어요" : "아직 확정된 약속이 없어요"
+ self.rootView.defaultLabel.isHidden = !noData
+ })
+ .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)) { _, 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)) { _, 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 {
+ 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
+ }
+ }
+}