diff --git a/Noostak_iOS/Noostak_iOS/Domain/Entity/Group.swift b/Noostak_iOS/Noostak_iOS/Domain/Entity/Group.swift index 7b7bfbc..6385d25 100644 --- a/Noostak_iOS/Noostak_iOS/Domain/Entity/Group.swift +++ b/Noostak_iOS/Noostak_iOS/Domain/Entity/Group.swift @@ -14,8 +14,10 @@ struct Group { let code: String ///그룹 멤버수 let membersCount: Int + ///그룹 이미지 + let groupImage: String ///방장 - let leader: User + let host: User ///멤버 let members: [User] } diff --git a/Noostak_iOS/Noostak_iOS/Domain/Entity/User.swift b/Noostak_iOS/Noostak_iOS/Domain/Entity/User.swift index b1a6978..2895b40 100644 --- a/Noostak_iOS/Noostak_iOS/Domain/Entity/User.swift +++ b/Noostak_iOS/Noostak_iOS/Domain/Entity/User.swift @@ -9,7 +9,7 @@ struct User { ///유저 이름 let name: String ///유저 프로필사진 - let image: String? + let userImage: String? } struct UserToken { diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/LikeButton.swift b/Noostak_iOS/Noostak_iOS/Global/Components/LikeButton.swift index b63f623..404e790 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Components/LikeButton.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Components/LikeButton.swift @@ -61,7 +61,7 @@ final class LikeButton: UIButton { } countLabel.do { - $0.font = .PretendardStyle.c2_sb.font + $0.font = .PretendardStyle.c3_sb.font } self.do { diff --git a/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleCategoryButton.swift b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleCategoryButton.swift index a71e20b..300b5ec 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleCategoryButton.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Components/ScheduleCategoryButton.swift @@ -45,7 +45,7 @@ final class ScheduleCategoryButton: UIButton { self.backgroundColor = category.displayColor self.setTitleColor(category == .other ? .appGray800 : .appWhite, for: .normal) self.layer.borderColor = UIColor.appWhite.cgColor - self.titleLabel?.font = .PretendardStyle.c2_sb.font + self.titleLabel?.font = .PretendardStyle.c3_sb.font self.layer.borderWidth = 1 self.layer.cornerRadius = 15 } diff --git a/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift b/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift index 82cadf0..7b42672 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Extension/UIFont+.swift @@ -61,9 +61,10 @@ extension UIFont { case b4_r case b5_r case c1_b - case c2_sb + case c3_sb case c3_r case c4_r + case c5_r var font: UIFont { switch self { @@ -86,9 +87,10 @@ extension UIFont { case .b4_r: return UIFont.pretendard(.regular, size: 15) case .b5_r: return UIFont.pretendard(.regular, size: 14) case .c1_b: return UIFont.pretendard(.bold, size: 13) - case .c2_sb: return UIFont.pretendard(.semibold, size: 13) + case .c3_sb: return UIFont.pretendard(.semibold, size: 13) case .c3_r: return UIFont.pretendard(.regular, size: 13) case .c4_r: return UIFont.pretendard(.regular, size: 11) + case .c5_r: return UIFont.pretendard(.regular, size: 10) } } @@ -97,7 +99,7 @@ extension UIFont { switch self { 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: + .b4_sb_1percent, .b4_r, .b5_r, .c1_b, .c3_sb, .c3_r, .c4_r, .c5_r: return 140 } } diff --git a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift index 2797acb..d075e30 100644 --- a/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift +++ b/Noostak_iOS/Noostak_iOS/Global/Utils/NSTDateUtility.swift @@ -94,49 +94,8 @@ public extension NSTDateUtility { } extension NSTDateUtility { - ///타임테이블 뷰 : "요일 월/일" - static func dateList(_ dateStrings: [String]) -> [String] { - let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 - let displayFormatter = NSTDateUtility(format: .EEMMdd) // 출력 형식 - - return dateStrings.compactMap { dateString in - switch formatter.date(from: dateString) { - case .success(let date): - return displayFormatter.string(from: date) - case .failure(let error): - print("Failed to parse date \(dateString): \(error.localizedDescription)") - return nil - } - } - } - - ///타임테이블 뷰 : "00시" - static func timeList(_ startTime: String, _ endTime: String) -> [String] { - let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 - var result: [String] = [] - - switch (formatter.date(from: startTime), formatter.date(from: endTime)) { - case (.success(let start), .success(let end)): - let calendar = Calendar.current - var current = start - - while current <= end { - result.append(NSTDateUtility(format: .HH).string(from: current)) // 출력 형식 - if let nextHour = calendar.date(byAdding: .hour, value: 1, to: current) { - current = nextHour - } else { - break - } - } - default: - print("Failed to parse start or end time.") - return [] - } - return result - } - static func durationList(_ startTime: String, _ endTime: String) -> String { - let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) // ISO 8601 형식 + let formatter = NSTDateUtility(format: .yyyyMMddTHHmmss) let dateFormatter = NSTDateUtility(format: .MMddEE) // "9월 7일 (일)" let timeFormatter = NSTDateUtility(format: .HHmm) // "10:00" @@ -149,5 +108,4 @@ extension NSTDateUtility { } return "\(dateFormatter.string(from: startDate)) \(timeFormatter.string(from: startDate))~\(timeFormatter.string(from: endDate))" } - } diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberCellReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberCellReactor.swift new file mode 100644 index 0000000..8bf07b1 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberCellReactor.swift @@ -0,0 +1,41 @@ +// +// GroupMemberCellReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/9/25. +// + +import ReactorKit +import RxSwift + +final class GroupMemberCellReactor: Reactor { + typealias Action = NoAction + struct State { + let user: User + } + + let initialState: State + + init(user: User) { + self.initialState = State(user: user) + } +} + +let mockMemberData = Group(id: 1, + name: "누트탁", + code: "AKFJS", + membersCount: 10, + groupImage: "df", + host: User(name: "오연서", userImage: "1"), + members: [ + User(name: "오연서", userImage: "1"), + User(name: "오옹서", userImage: "1"), + User(name: "오앵서", userImage: "1"), + User(name: "오용서", userImage: "1"), + User(name: "오잉서", userImage: "1"), + User(name: "오옹서", userImage: "1"), + User(name: "오앵서", userImage: "1"), + User(name: "오용아", userImage: "1"), + User(name: "오러서", userImage: "1"), + User(name: "오엉서", userImage: "1")] +) diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberReactor.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberReactor.swift new file mode 100644 index 0000000..16b3df9 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/Reactor/GroupMemberReactor.swift @@ -0,0 +1,46 @@ +// +// GroupMemberReactor.swift +// Noostak_iOS +// +// Created by 오연서 on 2/9/25. +// + +import ReactorKit +import RxSwift +import RxDataSources + +final class GroupMemberReactor: Reactor { + enum Action { + case loadGroupData + } + + enum Mutation { + case setGroup(Group) + } + + struct State { + var group: Group + } + + let initialState: State + + init() { + self.initialState = State(group: mockMemberData) + } + + func mutate(action: Action) -> Observable { + switch action { + case .loadGroupData: + return Observable.just(.setGroup(currentState.group)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setGroup(let group): + newState.group = group + } + return newState + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/Cell/GroupMemberCVC.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/Cell/GroupMemberCVC.swift new file mode 100644 index 0000000..83b29d3 --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/Cell/GroupMemberCVC.swift @@ -0,0 +1,75 @@ +// +// GroupMemberCVC.swift +// Noostak_iOS +// +// Created by 오연서 on 2/9/25. +// + +import UIKit +import SnapKit +import Then +import ReactorKit + +final class GroupMemberCVC: UICollectionViewCell, View { + + // MARK: Properties + static let identifier = "GroupMemberCVC" + var disposeBag = DisposeBag() + + // MARK: Views + private let memberProfile = UIImageView() + private let memberName = UILabel() + + // 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() { + [memberProfile, memberName].forEach { + self.addSubview($0) + } + } + + // MARK: setUpUI + private func setUpUI() { + memberProfile.do { + $0.image = .imgProfileFilled + $0.layer.cornerRadius = 30.5 + } + + memberName.do { + $0.font = .PretendardStyle.c3_sb.font + $0.textColor = .appGray900 + } + } + + // MARK: setUpLayout + private func setUpLayout() { + memberProfile.snp.makeConstraints { + $0.top.centerX.equalToSuperview() + $0.size.equalTo(61) + } + + memberName.snp.makeConstraints { + $0.top.equalTo(memberProfile.snp.bottom).offset(4) + $0.centerX.equalTo(memberProfile) + } + } +} + +extension GroupMemberCVC { + func bind(reactor: GroupMemberCellReactor) { + let user = reactor.currentState.user + memberProfile.image = .imgProfileFilled //api 연결시 변경 + memberName.text = user.name + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/GroupMemberView.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/GroupMemberView.swift new file mode 100644 index 0000000..ed4fd1d --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/View/GroupMemberView.swift @@ -0,0 +1,174 @@ +// +// GroupMemberView.swift +// Noostak_iOS +// +// Created by 오연서 on 2/9/25. +// + +import UIKit +import Then +import SnapKit +import RxSwift +import RxCocoa + +final class GroupMemberView: UIView { + + // MARK: Properties + private let disposeBag = DisposeBag() + + // MARK: Views + let groupProfileImageView = UIImageView() + let groupNameLabel = UILabel() + private lazy var shareButton = UIButton() + let groupMemberTotalLabel = UILabel() + let groupHostLabel = UILabel() + let groupHostProfile = UIImageView() + let groupHostName = UILabel() + private let divider = UIView() + let groupMemberLabel = UILabel() + var groupMemberCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + let defaultLabel = UILabel() + + // 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() { + [groupProfileImageView, groupNameLabel, shareButton, groupMemberTotalLabel, groupHostLabel, + groupHostProfile, groupHostName, divider, groupMemberLabel, groupMemberCollectionView, defaultLabel].forEach { + self.addSubview($0) + } + } + + private func setUpFoundation() { + self.backgroundColor = .appWhite + } + // MARK: setUpUI + private func setUpUI() { + groupProfileImageView.do { + $0.layer.cornerRadius = 7.14 + $0.image = .icGroupDefault + } + + groupNameLabel.do { + $0.font = .PretendardStyle.h1_b.font + $0.textColor = .appGray900 + } + + shareButton.do { + $0.setImage(.icShare, for: .normal) + } + + groupMemberTotalLabel.do { + $0.font = .PretendardStyle.b2_r.font + $0.textColor = .appGray800 + } + + groupHostLabel.do { + $0.text = "방장" + $0.font = .PretendardStyle.b4_sb.font + $0.textColor = .appGray700 + } + + groupHostProfile.do { + $0.layer.cornerRadius = 36 + $0.image = .imgProfileFilled + } + + groupHostName.do { + $0.font = .PretendardStyle.c3_sb.font + $0.textColor = .appGray900 + } + + divider.do { + $0.backgroundColor = .appGray200 + } + + groupMemberLabel.do { + $0.font = .PretendardStyle.b4_sb.font + $0.textColor = .appGray700 + } + + defaultLabel.do { + $0.text = "아직 그룹 멤버가 없어요\n멤버를 초대하고 그룹을 만들어보세요" + $0.font = .PretendardStyle.b2_r.font + $0.textColor = .appGray900 + $0.textAlignment = .center + $0.numberOfLines = 2 + $0.isHidden = true + } + } + + // MARK: setUpLayout + private func setUpLayout() { + groupProfileImageView.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide).offset(21) + $0.leading.equalToSuperview().inset(16) + $0.size.equalTo(40) + } + + groupNameLabel.snp.makeConstraints { + $0.centerY.equalTo(groupProfileImageView) + $0.leading.equalTo(groupProfileImageView.snp.trailing).offset(8) + } + + shareButton.snp.makeConstraints { + $0.top.equalTo(groupProfileImageView) + $0.trailing.equalToSuperview().inset(16) + $0.size.equalTo(24) + } + + groupMemberTotalLabel.snp.makeConstraints { + $0.top.equalTo(groupProfileImageView.snp.bottom).offset(9) + $0.leading.equalTo(groupProfileImageView) + } + + groupHostLabel.snp.makeConstraints { + $0.top.equalTo(groupMemberTotalLabel.snp.bottom).offset(22) + $0.leading.equalTo(groupMemberTotalLabel) + } + + groupHostProfile.snp.makeConstraints { + $0.top.equalTo(groupHostLabel.snp.bottom).offset(14) + $0.leading.equalTo(groupMemberTotalLabel) + $0.size.equalTo(72) + } + + groupHostName.snp.makeConstraints { + $0.top.equalTo(groupHostProfile.snp.bottom).offset(4) + $0.centerX.equalTo(groupHostProfile) + } + + divider.snp.makeConstraints { + $0.top.equalTo(groupHostName.snp.bottom).offset(24) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(1.5) + } + + groupMemberLabel.snp.makeConstraints { + $0.top.equalTo(divider.snp.bottom).offset(20) + $0.leading.equalTo(groupMemberTotalLabel) + } + + groupMemberCollectionView.snp.makeConstraints { + $0.top.equalTo(groupMemberLabel.snp.bottom).offset(14) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.bottom.equalToSuperview() + } + + defaultLabel.snp.makeConstraints { + $0.top.equalTo(groupMemberLabel.snp.bottom).offset(39) + $0.centerX.equalToSuperview() + } + } +} diff --git a/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/ViewController/GroupMemberViewController.swift b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/ViewController/GroupMemberViewController.swift new file mode 100644 index 0000000..d95277a --- /dev/null +++ b/Noostak_iOS/Noostak_iOS/Presentation/GroupMember/ViewController/GroupMemberViewController.swift @@ -0,0 +1,96 @@ +// +// GroupMemberViewController.swift +// Noostak_iOS +// +// Created by 오연서 on 2/9/25. +// + +import UIKit +import ReactorKit +import RxSwift +import RxCocoa +import RxDataSources + +final class GroupMemberViewController: UIViewController, View { + // MARK: - Properties + var disposeBag = DisposeBag() + private let rootView = GroupMemberView() + + // MARK: - Init + init(reactor: GroupMemberReactor) { + 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.groupMemberCollectionView.register(GroupMemberCVC.self, forCellWithReuseIdentifier: GroupMemberCVC.identifier) + rootView.groupMemberCollectionView.rx.setDelegate(self).disposed(by: disposeBag) + } + + // MARK: - Bind Reactor + func bind(reactor: GroupMemberReactor) { + let dataSource = RxCollectionViewSectionedReloadDataSource>( + configureCell: { _, collectionView, indexPath, user in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GroupMemberCVC.identifier, for: indexPath) as! GroupMemberCVC + let cellReactor = GroupMemberCellReactor(user: user) + cell.reactor = cellReactor + return cell + } + ) + + reactor.state.map { [SectionModel(model: "Members", items: $0.group.members)] } + .do(onNext: { [weak self] sections in + let isEmpty = sections.first?.items.isEmpty ?? true + self?.rootView.defaultLabel.isHidden = !isEmpty + }) + .bind(to: rootView.groupMemberCollectionView.rx.items(dataSource: dataSource)) + .disposed(by: disposeBag) + + reactor.state.map { $0.group } + .subscribe(onNext: { [weak self] group in + guard let self = self else { return } + self.rootView.groupProfileImageView.image = .imgProfileFilled // api 연결시 변경 + self.rootView.groupNameLabel.text = group.name + self.rootView.groupMemberTotalLabel.text = "그룹 멤버 \(group.membersCount)" + self.rootView.groupMemberLabel.text = "멤버 (\(group.membersCount)/50)" + }) + .disposed(by: disposeBag) + + reactor.state.map { $0.group.host } + .subscribe(onNext: { [weak self] host in + guard let self = self else { return } + self.rootView.groupHostName.text = host.name + self.rootView.groupHostProfile.image = .imgProfileFilled // api 연결시 변경 + }) + .disposed(by: disposeBag) + } +} + +// MARK: - CollectionViewDelegateFlowLayout +extension GroupMemberViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 61, height: 83) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } +}