diff --git a/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinator.swift b/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinator.swift index b7a50235..6c9de6ff 100644 --- a/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinator.swift +++ b/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinator.swift @@ -6,21 +6,20 @@ // import Foundation -import ComposableArchitecture -import TCACoordinators + import Feature import Domain import Shared +import ComposableArchitecture +import TCACoordinators @Reducer struct MainTabCoordinator { - enum Tab: Hashable { - case home - case moim - } - enum Action { + + enum Action: BindableAction { + case binding(BindingAction) case home(HomeCoordinator.Action) case moim(MoimCoordinator.Action) @@ -31,9 +30,12 @@ struct MainTabCoordinator { @ObservableState struct State: Equatable { - static let intialState = State(home: .initialState, moim: .initialState) + static let intialState = State(currentTab: .home, home: .initialState, moim: .initialState) + + var currentTab: Tab var home: HomeCoordinator.State var moim: MoimCoordinator.State + @Shared(.inMemory(SharedKeys.categories.rawValue)) var categories: [NamoCategory] = [] } @@ -41,7 +43,8 @@ struct MainTabCoordinator { @Dependency(\.categoryUseCase) var categoryUseCase var body: some ReducerOf { - // 탭은 Navigatin을 가지지 않고 각 Coordinator를 Scope로 설정 + BindingReducer() + Scope(state: \.home, action: \.home) { HomeCoordinator() } diff --git a/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinatorView.swift b/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinatorView.swift index 91efcf77..388ced46 100644 --- a/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinatorView.swift +++ b/Namo_SwiftUI/Projects/App/Sources/MainTab/MainTabCoordinatorView.swift @@ -6,22 +6,29 @@ // import SwiftUI + +import Feature +import SharedDesignSystem + import ComposableArchitecture import TCACoordinators -import Feature struct MainTabCoordinatorView: View { - let store: StoreOf - + @Perception.Bindable var store: StoreOf + var body: some View { WithPerceptionTracking { - TabView { + TabView(selection: $store.currentTab) { HomeCoordinatorView(store: store.scope(state: \.home, action: \.home)) - .tabItem { Text("홈") } - + .tag(Tab.home) + MoimCoordinatorView(store: store.scope(state: \.moim, action: \.moim)) - .tabItem { Text("모임") } + .tag(Tab.group) + } + .overlay(alignment: .bottom) { + NamoTabView(currentTab: $store.currentTab) } + .edgesIgnoringSafeArea(.bottom) .onAppear { store.send(.viewOnAppear) } diff --git a/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinator.swift b/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinator.swift index 5b8b5c89..c08b05a0 100644 --- a/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinator.swift +++ b/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinator.swift @@ -6,9 +6,11 @@ // import Foundation + +import SharedUtil + import ComposableArchitecture import TCACoordinators -import SharedUtil @Reducer(state: .equatable) enum AppScreen { @@ -51,7 +53,7 @@ struct AppCoordinator { // mainTab 이동 - 로그인 체크 결과 goToMainScreen 시 case .router(.routeAction(_, action: .onboarding(.goToMainScreen))): - state.routes = [.root(.mainTab(.init(home: .initialState, moim: .initialState)), embedInNavigationView: true)] + state.routes = [.root(.mainTab(.init(currentTab: .home, home: .initialState, moim: .initialState)), embedInNavigationView: true)] return .none // Notification 에러 핸들링 diff --git a/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinatorView.swift b/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinatorView.swift index 758d1c59..a58eb858 100644 --- a/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinatorView.swift +++ b/Namo_SwiftUI/Projects/App/Sources/Root/AppCoordinatorView.swift @@ -13,29 +13,31 @@ import SharedDesignSystem import SharedUtil struct AppCoordinatorView: View { - let store: StoreOf + @Perception.Bindable var store: StoreOf @State private var cancellables = Set() var body: some View { - TCARouter(store.scope(state: \.routes, action: \.router)) { screen in - switch screen.case { - case let .mainTab(store): - MainTabCoordinatorView(store: store) - case let .onboarding(store): - OnboardingCoordinatorView(store: store) + WithPerceptionTracking { + TCARouter(store.scope(state: \.routes, action: \.router)) { screen in + switch screen.case { + case let .mainTab(store): + MainTabCoordinatorView(store: store) + case let .onboarding(store): + OnboardingCoordinatorView(store: store) + } } - } - .namoAlertView( - isPresented: Binding(get: { store.showAlert }, set: { store.send(.changeShowAlert(show: $0)) }), - title: store.alertTitle, - content: store.alertContent, - confirmAction: { - store.send(.doAlertConfirmAction) - } - ) - .onReceive(NotificationCenter.default.publisher(for: .networkError)) { notification in - if let error = notification.userInfo?["error"] as? NetworkErrorNotification { - store.send(.handleNotiError(error: error)) +// .namoAlertView( +// isPresented: Binding(get: { store.showAlert }, set: { store.send(.changeShowAlert(show: $0)) }), +// title: store.alertTitle, +// content: store.alertContent, +// confirmAction: { +// store.send(.doAlertConfirmAction) +// } +// ) + .onReceive(NotificationCenter.default.publisher(for: .networkError)) { notification in + if let error = notification.userInfo?["error"] as? NetworkErrorNotification { + store.send(.handleNotiError(error: error)) + } } } } diff --git a/Namo_SwiftUI/Projects/Domain/Auth/Sources/AuthManager.swift b/Namo_SwiftUI/Projects/Domain/Auth/Sources/AuthManager.swift index ec2954e1..9fa1eba2 100644 --- a/Namo_SwiftUI/Projects/Domain/Auth/Sources/AuthManager.swift +++ b/Namo_SwiftUI/Projects/Domain/Auth/Sources/AuthManager.swift @@ -80,6 +80,7 @@ public extension AuthManager { try KeyChainManager.addItem(key: "refreshToken", value: result.refreshToken) // 3. userId 키체인 저장 + try KeyChainManager.addItem(key: "userId", value: String(result.userId)) // 4. 약관 동의, 필요 정보 작성 여부 저장 diff --git a/Namo_SwiftUI/Projects/Domain/Friend/Sources/Mapper/FriendMapper.swift b/Namo_SwiftUI/Projects/Domain/Friend/Sources/Mapper/FriendMapper.swift index 070376b3..cb29b426 100644 --- a/Namo_SwiftUI/Projects/Domain/Friend/Sources/Mapper/FriendMapper.swift +++ b/Namo_SwiftUI/Projects/Domain/Friend/Sources/Mapper/FriendMapper.swift @@ -84,3 +84,5 @@ extension FriendCategoryDTO { return FriendCategory(categoryName: categoryName, colorId: colorId) } } + + diff --git a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Mapper/MoimScheduleMapper.swift b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Mapper/MoimScheduleMapper.swift index 2ba6669f..a60b5ea3 100644 --- a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Mapper/MoimScheduleMapper.swift +++ b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Mapper/MoimScheduleMapper.swift @@ -6,7 +6,9 @@ // import Foundation + import CoreNetwork +import DomainFriend import SharedUtil public extension MoimScheduleListResponseDTO { @@ -23,14 +25,14 @@ public extension MoimScheduleListResponseDTO { public extension MoimSchedule { func toDto() -> MoimScheduleRequestDTO { return .init(title: title, - imageUrl: nil, + imageUrl: imageUrl, period: PeriodDto(startDate: startDate.dateToISO8601(), endDate: endDate.dateToISO8601()), - location: LocationDto(longitude: 0.0, - latitude: 0.0, + location: LocationDto(longitude: longitude, + latitude: latitude, locationName: locationName, kakaoLocationId: kakaoLocationId), - participants: [11]) + participants: participants.map { $0.userId }) } } @@ -40,8 +42,8 @@ public extension MoimSchedule { imageUrl: imageUrl, period: PeriodDto(startDate: startDate.dateToISO8601(), endDate: startDate.dateToISO8601()), - location: LocationDto(longitude: 0.0, - latitude: 0.0, + location: LocationDto(longitude: longitude, + latitude: latitude, locationName: locationName, kakaoLocationId: kakaoLocationId), participantsToAdd: [], @@ -74,3 +76,14 @@ public extension ParticipantsDto { isOwner: isOwner) } } + +public extension Friend { + func toParticipant() -> Participant { + .init(participantId: 0, + userId: memberId, + isGuest: false, + nickname: nickname, + colorId: favoriteColorId, + isOwner: false) + } +} diff --git a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/MoimSchedule.swift b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/MoimSchedule.swift index 89e3a61d..77d05850 100644 --- a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/MoimSchedule.swift +++ b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/MoimSchedule.swift @@ -8,7 +8,9 @@ import Foundation import CoreNetwork -public struct MoimSchedule: Decodable, Hashable { +import SharedUtil + +public struct MoimSchedule: Decodable, Hashable, Equatable { public init(scheduleId: Int, title: String, imageUrl: String, @@ -30,16 +32,30 @@ public struct MoimSchedule: Decodable, Hashable { self.kakaoLocationId = kakaoLocationId self.participants = participants } + + public init() { + self.scheduleId = 0 + self.title = "" + self.imageUrl = "" + self.startDate = .now + self.endDate = .now + self.longitude = 0.0 + self.latitude = 0.0 + self.locationName = "" + self.kakaoLocationId = "" + self.participants = [] + } + public let scheduleId: Int - public let title: String - public let imageUrl: String - public let startDate: Date - public let endDate: Date - public let longitude: Double - public let latitude: Double - public let locationName: String - public let kakaoLocationId: String - public let participants: [Participant] + public var title: String + public var imageUrl: String + public var startDate: Date + public var endDate: Date + public var longitude: Double + public var latitude: Double + public var locationName: String + public var kakaoLocationId: String + public var participants: [Participant] } public struct Participant: Decodable, Hashable { @@ -66,6 +82,14 @@ public struct Participant: Decodable, Hashable { public extension MoimSchedule { var isOwner: Bool { - participants.firstIndex(where: { $0.isOwner && $0.userId == UserDefaults.standard.integer(forKey: "userId")}) != nil + do { + guard let readUserId = try? KeyChainManager.readItem(key: "userId"), + let userId = Int(readUserId) else { + return false + } + return participants.firstIndex(where: { $0.isOwner && $0.userId == userId}) != nil + } catch { + return false + } } } diff --git a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/ScheduleItem.swift b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/ScheduleItem.swift index 450674ef..f461afa3 100644 --- a/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/ScheduleItem.swift +++ b/Namo_SwiftUI/Projects/Domain/Moim/Interface/Sources/Model/ScheduleItem.swift @@ -7,7 +7,7 @@ import Foundation -public struct MoimScheduleItem: Decodable, Hashable { +public struct MoimScheduleItem: Decodable, Hashable, Identifiable { public init(meetingScheduleId: Int, title: String, startDate: String, @@ -21,6 +21,7 @@ public struct MoimScheduleItem: Decodable, Hashable { self.participantCount = participantCount self.participantNicknames = participantNicknames } + public var id: Int { meetingScheduleId } public let meetingScheduleId: Int public let title: String public let startDate: String diff --git a/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Coordinator/ScheduleEditCoordinatorView.swift b/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Coordinator/ScheduleEditCoordinatorView.swift index 8bb0571f..cc3b6418 100644 --- a/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Coordinator/ScheduleEditCoordinatorView.swift +++ b/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Coordinator/ScheduleEditCoordinatorView.swift @@ -9,6 +9,7 @@ import SwiftUI import ComposableArchitecture import TCACoordinators +import SharedDesignSystem public struct ScheduleEditCoordinatorView: View { let store: StoreOf diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Example/Sources/FeautreMoimExampleSample.swift b/Namo_SwiftUI/Projects/Feature/Moim/Example/Sources/FeautreMoimExampleSample.swift index f4f07b2e..4c715f47 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Example/Sources/FeautreMoimExampleSample.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Example/Sources/FeautreMoimExampleSample.swift @@ -7,7 +7,7 @@ import ComposableArchitecture @main struct FeautureMoimExampleApp: App { var body: some Scene { - WindowGroup { + WindowGroup { MoimCoordinatorView(store: .init(initialState: .initialState, reducer: { MoimCoordinator() })) diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/FriendInvite/FriendInviteStoreInterface.swift b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/FriendInvite/FriendInviteStoreInterface.swift new file mode 100644 index 00000000..bbcf1e94 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/FriendInvite/FriendInviteStoreInterface.swift @@ -0,0 +1,66 @@ +// +// FriendInviteStoreInterface.swift +// FeatureMoim +// +// Created by 권석기 on 11/3/24. +// + +import Foundation + +import DomainFriendInterface +import DomainFriend + +import ComposableArchitecture + +/** + Reducer for FriendInvite(친구초대) Feature + */ +@Reducer +public struct FriendInviteStore { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + public init() {} + + /// 검색어 + public var searchText = "" + + /// 친구목록 + public var friendList: [Friend] = [] + + /// 추가한 친구목록 + public var addedFriend: [Friend] = [] + } + + public enum Action: BindableAction { + + /// 바인딩액션 + case binding(BindingAction) + + /// 검색결과탭 + case searchButtonTapped + + /// 검색결과 응답 + case searchResponse(FriendResponse) + + /// 친구 추가 + case addFriend(Friend) + + /// 초대친구 삭제 + case removeFriend(memberId: Int) + + /// 뒤로가기 + case backButtonTapped + } + + public var body: some ReducerOf { + BindingReducer() + + reducer + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimEdit/MoimEditInterface.swift b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimEdit/MoimEditInterface.swift index c31517f0..1e45c15a 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimEdit/MoimEditInterface.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimEdit/MoimEditInterface.swift @@ -8,82 +8,68 @@ import SwiftUI import UIKit import PhotosUI -import ComposableArchitecture + import DomainMoimInterface +import DomainPlaceSearchInterface +import DomainFriend +import ComposableArchitecture +/** + Reducer for MoimEdit(모임 편집) Feature +*/ @Reducer -/// 모임 생성/편집/조회 public struct MoimEditStore { - /// 편집 여부 - public enum Mode: Equatable { - case view - case edit - case compose - } private let reducer: Reduce public init(reducer: Reduce) { self.reducer = reducer } + /// 편집 여부 + public enum Mode: Equatable { + case view, edit, compose + } + + @ObservableState public struct State: Equatable { - /// 타이틀 - @BindingState public var title: String = "" - /// 커버이미지 - @BindingState public var coverImageItem: PhotosPickerItem? - - /// 시작 날짜 - @BindingState public var startDate: Date = .now - - /// 종료 날짜 - @BindingState public var endDate: Date = .now - - /// 시작 날짜 선택 캘린더 보임여부 - @BindingState public var isStartPickerPresented: Bool = false - - /// 종료 날짜 선택 캘린더 보임여부 - @BindingState public var isEndPickerPresented: Bool = false - - /// 삭제 알림 보임여부 - @BindingState public var isAlertPresented: Bool = false + /// 편집 여부 + public var mode: Mode = .compose - /// 모임일정 id - public var moimScheduleId: Int = 0 + /// 모임일정 + public var moimSchedule: MoimSchedule - /// 커버이미지 url - public var imageUrl: String = "" + /// 커버이미지 + public var coverImageItem: PhotosPickerItem? /// 커버이미지 public var coverImage: UIImage? - /// 모임장소 좌표(위도) - public var latitude = 0.0 - - /// 모임장소 좌표(경도)* - public var longitude = 0.0 - - /// 모임장소명 - public var locationName = "" - - /// 카카오 locationId - public var kakaoLocationId = "" - - /// 참석자 정보 - public var participants: [Participant] = [] - - /// 방장 여부 - public var isOwner: Bool = false + /// 시작 날짜 선택 캘린더 보임여부 + public var isStartPickerPresented: Bool = false - /// 편집 여부 - public var mode: Mode = .compose + /// 종료 날짜 선택 캘린더 보임여부 + public var isEndPickerPresented: Bool = false - public init() {} + /// 삭제 알림 보임여부 + public var isAlertPresented: Bool = false + + /// 모임일정 조회, 편집 initializer + public init(moimSchedule: MoimSchedule) { + self.moimSchedule = moimSchedule + mode = self.moimSchedule.isOwner ? .edit : .view + } + + /// 모임일정 생성 initializer + public init() { + self.moimSchedule = .init() + } } public enum Action: BindableAction, Equatable { + /// 바인딩액션 처리 case binding(BindingAction) @@ -99,6 +85,9 @@ public struct MoimEditStore { /// 모임생성 버튼탭 case createButtonTapped + /// 생성확인 + case createButtonConfirm + /// 취소버튼 탭 case cancleButtonTapped @@ -108,7 +97,20 @@ public struct MoimEditStore { /// 삭제확인 case deleteButtonConfirm + /// 삭제처리 완료 + case deleteConfirm + + /// 지도검색 이동 case goToKakaoMapView + + /// 친구초대 이동 + case goToFriendInvite + + /// 친구캘린더 이동 + case goToFriendCalendar + + /// 위치정보 업데이트 + case locationUpdated(LocationInfo) } public var body: some ReducerOf { diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimList/MoimListStoreInterface.swift b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimList/MoimListStoreInterface.swift index 01c6e7df..3b112a51 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimList/MoimListStoreInterface.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimList/MoimListStoreInterface.swift @@ -6,9 +6,14 @@ // import Foundation -import ComposableArchitecture + import DomainMoimInterface +import ComposableArchitecture + +/** + Reducer for MoimList(일정 목록조회) Feature + */ @Reducer public struct MoimListStore { @@ -18,16 +23,46 @@ public struct MoimListStore { self.reducer = reducer } + public enum Filter { + case allSchedules, hidePastSchedules + } + @ObservableState public struct State: Equatable { - public init() {} - public var moimList: [MoimScheduleItem] = [] + public init() { + self.moimList = .init() + } + public var moimList: IdentifiedArrayOf + + public var filter: Filter = .allSchedules + + public var filteredList: IdentifiedArrayOf { + switch filter { + case .allSchedules: moimList + case .hidePastSchedules: [] + } + } } public enum Action { + + /// viewOnAppear case viewOnAppear - case moimListResponse([MoimScheduleItem]) + + /// 모임결과 응답 + case moimListResponse(IdentifiedArrayOf) + + /// 모임셀 선택 case moimCellSelected(meetingScheduleId: Int) + + /// 일정 생성 + case presentComposeSheet + + /// 일정 조회 + case presentDetailSheet(MoimSchedule) + + /// 필터옵션 토글 + case toggleFilterOption } public var body: some ReducerOf { diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimRequest/MoimRequestStoreInterface.swift b/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimRequest/MoimRequestStoreInterface.swift deleted file mode 100644 index 26cdb8b6..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Interface/Sources/MoimRequest/MoimRequestStoreInterface.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// MoimRequestStoreInterface.swift -// FeatureMoim -// -// Created by 권석기 on 9/25/24. -// - -import Foundation -import ComposableArchitecture - -@Reducer -public struct MoimRequestStore { - private let reducer: Reduce - - public init(reducer: Reduce) { - self.reducer = reducer - } - - public struct State: Equatable { - public init() {} - } - - public enum Action { - case backButtonTap - } - - public var body: some ReducerOf { - reducer - } -} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Project.swift b/Namo_SwiftUI/Projects/Feature/Moim/Project.swift index 8e3c4156..7298f144 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Project.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Project.swift @@ -23,8 +23,8 @@ let targets: [Target] = [ factory: .init( dependencies: [ .feature(interface: .Moim), - .feature(implements: .Friend), - .feature(implements: .PlaceSearch) + .feature(interface: .PlaceSearch), + .feature(implements: .Friend) ] ) ), diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinator.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinator.swift new file mode 100644 index 00000000..8a3f1ea0 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinator.swift @@ -0,0 +1,115 @@ +// +// MoimCoordinator.swift +// FeatureMoim +// +// Created by 권석기 on 9/21/24. +// + +import Foundation + +import FeaturePlaceSearchInterface +import FeatureMoimInterface +import FeatureFriend + +import ComposableArchitecture +import TCACoordinators + +/** + Root Coordinator for Moim(모임) Tab Feature + + Coordinator Structure: + + MoimCoordinator (루트 코디네이터) + ├─ MainTab (메인 탭 화면) + │ └─ MoimList (모임 목록) + │ + ├─ MoimEditCoordinator (모임 수정 코디네이터) + │ └─ Sheet Presentation + │ ├─ Create Mode (새 모임 생성) + │ └─ Edit Mode (기존 모임 수정) + │ + └─ Notification (알림 화면) + └─ Push Presentation + + Flow: + 1. 메인탭에서 모임 생성/수정 -> MoimEditCoordinator (Sheet) + 2. 메인탭에서 알림 버튼 -> Notification (Push) + */ + +@Reducer(state: .equatable) +public enum MoimScreen { + + /// 메인탭(모임, 친구) + case mainTab(MainViewStore) + + /// 모임 일정 + case moimEdit(MoimEditCoordinator) + + /// 친구 요청 + case notification +} + +@Reducer +public struct MoimCoordinator { + public init() {} + + @ObservableState + public struct State: Equatable { + public static let initialState = State( + routes: [.root(.mainTab(.initialState), embedInNavigationView: true)], + mainTabStore: .initialState + ) + + /// 라우팅 리스트 + var routes: [Route] + + /// Sheet 보임여부 + var isPresentedSheet: Bool = false + + /// 메인탭 스토어 + var mainTabStore: MainViewStore.State + + } + + public enum Action { + case router(IndexedRouterActionOf) + case mainTabAction(MainViewStore.Action) + } + + public var body: some ReducerOf { + Scope(state: \.mainTabStore, action: \.mainTabAction) { + MainViewStore() + } + + Reduce { state, action in + switch action { + // MARK: - 일정생성 Navigation + case .router(.routeAction(_, action: .mainTab(.moimListAction(.presentComposeSheet)))): + state.isPresentedSheet = true + state.routes.presentCover(.moimEdit(.init())) + return .none + // MARK: - 일정조회 Navigation + case let .router(.routeAction(_, action: .mainTab(.moimListAction(.presentDetailSheet(moimSchedule))))): + state.routes.presentCover(.moimEdit(.init(moimEditStore: .init(moimSchedule: moimSchedule)))) + state.isPresentedSheet = true + return .none + // MARK: - 일정 생성, 편집 취소 + case .router(.routeAction(_, action: .moimEdit(.moimEditAction(.cancleButtonTapped)))): + return .send(.mainTabAction(.moimListAction(.viewOnAppear))) + // MARK: - 리스트 업데이트후 Navigation + // TODO: 현재는 상태를 끌어올려서 경로를 초기화하는 방식 추후 더좋은방법 고민 + case .mainTabAction(.moimListAction(.moimListResponse)): + state.isPresentedSheet = false + state.routes = [.root(.mainTab(state.mainTabStore), embedInNavigationView: true)] + return .none + // MARK: - 친구요청 Navigation + case .router(.routeAction(_, action: .mainTab(.notificationButtonTap))): + state.routes.push(.notification) + return .none + default: + return .none + } + } + .forEachRoute(\.routes, action: \.router) + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinatorView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinatorView.swift new file mode 100644 index 00000000..a4c3c37b --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimCoordinatorView.swift @@ -0,0 +1,39 @@ +// +// MoimCoordinatorView.swift +// FeatureMoim +// +// Created by 권석기 on 9/21/24. +// + +import SwiftUI + +import FeatureFriend +import SharedDesignSystem + +import ComposableArchitecture +import TCACoordinators + +public struct MoimCoordinatorView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + TCARouter(store.scope(state: \.routes, action: \.router)) { screen in + switch screen.case { + case let .mainTab(store): + MainView(store: store) + case let .moimEdit(store): + MoimEditCoordinatorView(store: store) + case .notification: + Text("친구요청") + } + } + .overlay(store.isPresentedSheet ? Color.black.opacity(0.3).ignoresSafeArea(.all) : nil) + } + } +} + diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinator.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinator.swift new file mode 100644 index 00000000..08788401 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinator.swift @@ -0,0 +1,159 @@ +// +// MoimEditCoordinator.swift +// FeatureMoim +// +// Created by 권석기 on 11/1/24. +// + +import Foundation + +import DomainMoimInterface +import DomainFriend +import FeatureMoimInterface +import FeaturePlaceSearchInterface +import FeatureFriendInterface +import FeatureFriend +import SharedDesignSystem + +import ComposableArchitecture +import TCACoordinators + +/** + MoimEdit(모임 편집) 코디네이터 + + Coordinator Structure: + + MoimEditCoordinator (모임 편집 코디네이터) + ├─ CreateMoim (모임 생성/수정 화면) + │ ├─ KakaoMap (장소 검색) + │ │ └─ Push Presentation + │ │ ├─ 지도 검색 + │ │ └─ POI 선택 + │ │ + │ └─ FriendInvite (친구 초대) + │ └─ Push Presentation + │ + State Management: + - moimEditStore: 모임 생성/수정 관련 상태 + - placeSearchStore: 카카오맵 장소 검색 관련 상태 + + Navigation Flow: + 1. CreateMoim -> KakaoMap (Push) + - 장소 선택 후 데이터 전달 (위치명, 좌표, ID) + 2. CreateMoim -> FriendInvite (Push) + - 친구 선택 후 초대 목록 관리 + 3. 취소/완료 -> Dismiss to Parent Coordinator + */ + +@Reducer(state: .equatable) +public enum MoimEditScreen { + case createMoim(MoimEditStore) + case kakaoMap(PlaceSearchStore) + case friendInvite(FriendInviteStore) + case friendCalendar +} + +@Reducer +public struct MoimEditCoordinator { + public init() {} + + @ObservableState + public struct State: Equatable { + + public init(moimEditStore: MoimEditStore.State) { + self.moimEditStore = moimEditStore + self.placeSearchStore = .init() + self.friendInviteStore = .init() + self.routes = [.root(.createMoim(moimEditStore), embedInNavigationView: true)] + } + + public init() { + self.moimEditStore = .init() + self.placeSearchStore = .init() + self.friendInviteStore = .init() + self.routes = [.root(.createMoim(.init()), embedInNavigationView: true)] + } + + var routes: [Route] + + var moimEditStore: MoimEditStore.State + var placeSearchStore: PlaceSearchStore.State + var friendInviteStore: FriendInviteStore.State + } + + public enum Action { + case router(IndexedRouterActionOf) + case moimEditAction(MoimEditStore.Action) + case placeSearchAction(PlaceSearchStore.Action) + case friendInviteAction(FriendInviteStore.Action) + } + + public var body: some ReducerOf { + Scope(state: \.moimEditStore, action: \.moimEditAction) { + MoimEditStore() + } + Scope(state: \.placeSearchStore, action: \.placeSearchAction) { + PlaceSearchStore() + } + Scope(state: \.friendInviteStore, action: \.friendInviteAction) { + FriendInviteStore() + } + + Reduce { state, action in + switch action { + //MARK: - 장소검색 Navigation + case .router(.routeAction(_, action: .createMoim(.goToKakaoMapView))): + state.routes.push(.kakaoMap(state.placeSearchStore)) + return .none + // MARK: - 모임생성 수정 완료/취소/삭제 + case .router(.routeAction(_, action: .createMoim(.cancleButtonTapped))), + .router(.routeAction(_, action: .createMoim(.createButtonConfirm))), + .router(.routeAction(_, action: .createMoim(.deleteConfirm))): + return .send(.moimEditAction(.cancleButtonTapped)) + // MARK: - 장소선택 완료/취소 + case .router(.routeAction(_, action: .kakaoMap(.backButtonTapped))): + if case var .createMoim(editStore) = state.routes[0].screen { + editStore.moimSchedule.locationName = state.placeSearchStore.locationName + editStore.moimSchedule.latitude = state.placeSearchStore.y + editStore.moimSchedule.longitude = state.placeSearchStore.x + editStore.moimSchedule.kakaoLocationId = state.placeSearchStore.id + state.routes = [.root(.createMoim(editStore), embedInNavigationView: true)] + } + return .none + // MARK: - 검색결과 업데이트 + case let .router(.routeAction(_, action: .kakaoMap(.responsePlaceList(placeList)))): + return .send(.placeSearchAction(.responsePlaceList(placeList))) + // MARK: - 장소선택 + case let .router(.routeAction(_, action: .kakaoMap(.poiTapped(poiID)))): + guard let place = state.placeSearchStore.placeList.filter({ $0.id == poiID }).first else { return .none } + return .send(.placeSearchAction(.locationUpdated(place))) + // MARK: - 친구초대 Navigation + case .router(.routeAction(_, action: .createMoim(.goToFriendInvite))): + state.routes.push(.friendInvite(state.friendInviteStore)) + return .none + // MARK: - 친구 초대 뒤로가기 + case .router(.routeAction(_, action: .friendInvite(.backButtonTapped))): + if case var .createMoim(editStore) = state.routes[0].screen { + editStore.moimSchedule.participants = state.friendInviteStore.addedFriend.map { $0.toParticipant() } + state.routes = [.root(.createMoim(editStore), embedInNavigationView: true)] + } + return .none + // MARK: - 친구 초대 + case let .router(.routeAction(_, action: .friendInvite(.addFriend(friend)))): + return .send(.friendInviteAction(.addFriend(friend))) + // MARK: - 초대친구 제거 + case let .router(.routeAction(_, action: .friendInvite(.removeFriend(memberId)))): + return .send(.friendInviteAction(.removeFriend(memberId: memberId))) + // MARK: - 친구캘린더 Navigation + case .router(.routeAction(_, action: .createMoim(.goToFriendCalendar))): + state.routes.push(.friendCalendar) + return .none + default: + return .none + } + } + .forEachRoute(\.routes, action: \.router) + } +} + + diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinatorView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinatorView.swift new file mode 100644 index 00000000..db49e70f --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Coordinator/MoimEditCoordinatorView.swift @@ -0,0 +1,40 @@ +// +// MoimEditCoordinatorView.swift +// FeatureMoim +// +// Created by 권석기 on 11/1/24. +// + +import SwiftUI + +import SharedDesignSystem +import FeaturePlaceSearchInterface +import FeatureFriend + +import ComposableArchitecture +import TCACoordinators + +public struct MoimEditCoordinatorView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + TCARouter(store.scope(state: \.routes, action: \.router)) { screen in + switch screen.case { + case let .createMoim(store): + MoimScheduleEditView(store: store) + case let .kakaoMap(store): + PlaceSearchView(store: store) + .toolbar(.hidden, for: .navigationBar) + case let .friendInvite(store): + FriendInviteView(store: store) + case .friendCalendar: + Text("친구일정") + } + } + .background(ClearBackground()) + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinator.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinator.swift deleted file mode 100644 index f4057cd3..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinator.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// MoimCoordinator.swift -// FeatureMoim -// -// Created by 권석기 on 9/21/24. -// - -import Foundation -import ComposableArchitecture -import TCACoordinators -import FeaturePlaceSearchInterface -import FeatureMoimInterface -import FeatureFriend - -@Reducer(state: .equatable) -public enum MoimScreen { - // 모임 일정 - case moimSchedule(MainViewStore) - // 모임/친구 요청 - case moimRequest(MoimRequestStore) - // 친구 캘린더 화면 - case friendCalendar(FriendCalendarStore) -} - -@Reducer -public struct MoimCoordinator { - public init() {} - - @ObservableState - public struct State: Equatable { - public static let initialState = State(routes: [.root(.moimSchedule(.initialState), embedInNavigationView: true)], - moimSchedule: .initialState, - moimRequest: .init() - ) - - var routes: [Route] - var moimSchedule: MainViewStore.State - var moimRequest: MoimRequestStore.State - } - - public enum Action { - case router(IndexedRouterActionOf) - case moimSchedule(MainViewStore.Action) - case moimRequest(MoimRequestStore.Action) - case placeSearch(PlaceSearchStore.Action) - } - - public var body: some ReducerOf { - Scope(state: \.moimSchedule, action: \.moimSchedule) { - MainViewStore() - } - Scope(state: \.moimRequest, action: \.moimRequest) { - MoimRequestStore() - } - - Reduce { state, action in - switch action { - // 모임 요청 - case .router(.routeAction(_, action: .moimSchedule(.notificationButtonTap))): - state.routes.push(.moimRequest(.init())) - return .none - // 화면 제거 - case .router(.routeAction(_, action: .moimRequest(.backButtonTap))): - state.routes.goBack() - return .none - - case .router(.routeAction(_, action: .moimSchedule(.navigateToFriendCalendar(let friend)))): - // 친구 캘린더 push - state.routes.push(.friendCalendar(.init(friend: friend))) - return .none - - case .router(.routeAction(_, action: .friendCalendar(.backBtnTapped))): - // 친구 캘린더에서 뒤로가기 - state.routes.pop() - return .none - default: - return .none - } - } - .forEachRoute(\.routes, action: \.router) - } -} - - - diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinatorView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinatorView.swift deleted file mode 100644 index e117d1a1..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Core/MoimCoordinatorView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// MoimCoordinatorView.swift -// FeatureMoim -// -// Created by 권석기 on 9/21/24. -// - -import SwiftUI -import ComposableArchitecture -import FeaturePlaceSearch -import TCACoordinators -import FeatureFriend - -public struct MoimCoordinatorView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - TCARouter(store.scope(state: \.routes, action: \.router)) { screen in - switch screen.case { - case let .moimSchedule(store): - MainView(store: store) - case let .moimRequest(store): - MoimRequestView(store: store) - case let .friendCalendar(store): - FriendCalendarView(store: store) -// case let .kakaoMap(store): -// PlaceSearchView(store: store) -// .toolbar(.hidden, for: .navigationBar) - } - } - } -} - diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteStore.swift new file mode 100644 index 00000000..d41f8050 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteStore.swift @@ -0,0 +1,41 @@ +// +// FriendInviteStore.swift +// FeatureMoim +// +// Created by 권석기 on 11/3/24. +// + +import Foundation + +import ComposableArchitecture + +import FeatureMoimInterface +import DomainFriendInterface + +extension FriendInviteStore { + public init() { + @Dependency(\.friendUseCase) var friendUseCase + + let reducer: Reduce = Reduce { state, action in + switch action { + case .searchButtonTapped: + return .run { [state] send in + let response = try await friendUseCase.getFriends(page: 1, searchTerm: state.searchText) + await send(.searchResponse(response)) + } + case let .searchResponse(response): + state.friendList = response.friendList + return .none + case let .addFriend(friend): + state.addedFriend.append(friend) + return .none + case let .removeFriend(memberId): + guard let index = state.addedFriend.firstIndex(where: { $0.memberId == memberId }) else { return .none } + state.addedFriend.remove(at: index) + return .none + default: + return .none + }} + self.init(reducer: reducer) + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteView.swift index 41695d72..76ffbf33 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteView.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/FriendInviteView.swift @@ -6,58 +6,73 @@ // import SwiftUI + +import FeatureMoimInterface import SharedDesignSystem +import ComposableArchitecture + + public struct FriendInviteView: View { - @State private var text = "" + @Perception.Bindable private var store: StoreOf + @State private var showingFriendInvites = false - public init() {} + public init(store: StoreOf) { + self.store = store + } public var body: some View { - VStack(spacing: 0) { - searchSection - .padding(.top, 16) - .padding(.bottom, 20) - .padding(.horizontal, 25) - + WithPerceptionTracking { VStack(spacing: 0) { - HStack { - Text("초대한 친구") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) + searchSection + .padding(.top, 16) + .padding(.bottom, 20) + .padding(.horizontal, 25) + + VStack(spacing: 0) { + HStack { + Text("초대한 친구") + .font(.pretendard(.bold, size: 15)) + .foregroundStyle(Color.mainText) + + Spacer() + + Button(action: { + withAnimation { + showingFriendInvites.toggle() + } + }, label: { + Image(asset: SharedDesignSystemAsset.Assets.icUp) + .rotationEffect(.degrees(showingFriendInvites ? 0 : 180)) + }) + } + .padding(.horizontal, 25) - Spacer() + if showingFriendInvites { + FriendInvitedListView(store: store) + .padding(.top, 20) + } - Button(action: { - withAnimation { - showingFriendInvites.toggle() - } - }, label: { - Image(asset: SharedDesignSystemAsset.Assets.icUp) - .rotationEffect(.degrees(showingFriendInvites ? 180 : 0)) - }) + FriendSearchListView(store: store) + .padding(.top, 20) } - .padding(.horizontal, 25) + .padding(.top, 20) - if showingFriendInvites { - FriendInviteListView() - } - - } - .padding(.top, 20) - - Spacer() - } - .namoNabBar(center: { - Text("친구 초대하기") - .font(.pretendard(.bold, size: 16)) - .foregroundStyle(.black) - }, left: { - Button(action: {}, label: { - Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) + Spacer() + } + .namoNabBar(center: { + Text("친구 초대하기") + .font(.pretendard(.bold, size: 16)) + .foregroundStyle(.black) + }, left: { + Button(action: { + store.send(.backButtonTapped) + }, label: { + Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) + }) }) - }) + } } } @@ -72,7 +87,7 @@ extension FriendInviteView { TextField( "", - text: $text, + text: $store.searchText, prompt: Text("닉네임 혹은 이름 입력") .font(.pretendard(.regular, size: 15)) .foregroundColor(Color.textPlaceholder) @@ -86,7 +101,9 @@ extension FriendInviteView { } - Button(action: {}, + Button(action: { + store.send(.searchButtonTapped) + }, label: { Text("검색") .font(.pretendard(.bold, size: 15)) @@ -100,7 +117,3 @@ extension FriendInviteView { } } } - -#Preview { - FriendInviteView() -} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendAddItem.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendAddItem.swift index c61f6a65..a5f04547 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendAddItem.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendAddItem.swift @@ -6,24 +6,35 @@ // import SwiftUI + +import DomainFriend import SharedDesignSystem +import Kingfisher + struct FriendAddItem: View { - @Binding var isSelected: Bool + let friend: Friend + let isAdded: Bool var body: some View { VStack { HStack(spacing: 16) { - Image(asset: SharedDesignSystemAsset.Assets.appLogo) - .cornerRadius(10) + KFImage(URL(string: friend.profileImage ?? "")) + .placeholder({ + Image(asset: SharedDesignSystemAsset.Assets.friendDefaultImage) + }) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 48, height: 48) + .clipShape(RoundedRectangle(cornerRadius: 15)) VStack(spacing: 6) { HStack(spacing: 4) { - Text("닉네임") + Text(friend.nickname) .font(.pretendard(.bold, size: 15)) .foregroundStyle(Color.mainText) - Image(asset: SharedDesignSystemAsset.Assets.icHeart) + Image(asset: friend.favoriteFriend ? SharedDesignSystemAsset.Assets.icHeartSelected : SharedDesignSystemAsset.Assets.icHeart) .resizable() .frame(width: 12, height: 12) @@ -31,7 +42,7 @@ struct FriendAddItem: View { } HStack { - Text("친구가 직접 작성한 한 줄 소개 친구가 직접 작성한 한 줄 소개 친구가 직접 작성한 한 줄 소개친구가 직접 작성한 한 줄 소개") + Text(friend.bio) .font(.pretendard(.regular, size: 12)) .foregroundStyle(Color.mainText) .lineLimit(1) @@ -40,7 +51,7 @@ struct FriendAddItem: View { } } - Image(asset: isSelected ? SharedDesignSystemAsset.Assets.icAddedSelected : SharedDesignSystemAsset.Assets.icAdded) + Image(asset: isAdded ? SharedDesignSystemAsset.Assets.icAddedSelected : SharedDesignSystemAsset.Assets.icAdded) } .padding(.leading, 16) .padding(.trailing, 24) @@ -52,6 +63,4 @@ struct FriendAddItem: View { } } -#Preview { - FriendAddItem(isSelected: .constant(false)) -} + diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInviteListView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInviteListView.swift deleted file mode 100644 index 30a7bc28..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInviteListView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// FriendInviteListView.swift -// FeatureMoim -// -// Created by 권석기 on 9/11/24. -// - -import SwiftUI - -struct FriendInviteListView: View { - var body: some View { - ScrollView { - LazyVStack(spacing: 20) { - ForEach(0..<10, id:\.self) { _ in - FriendAddItem(isSelected: .constant(false)) - } - } .padding(.horizontal, 25) - } - .padding(.top, 16) - } -} - -#Preview { - FriendInviteListView() -} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInvitedListView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInvitedListView.swift new file mode 100644 index 00000000..78c44191 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendInvitedListView.swift @@ -0,0 +1,42 @@ +// +// FriendInvitedListView.swift +// FeatureMoim +// +// Created by 권석기 on 11/4/24. +// + +import SwiftUI + +import ComposableArchitecture + +import DomainFriend +import FeatureMoimInterface + +struct FriendInvitedListView: View { + let store: StoreOf + + var body: some View { + WithPerceptionTracking { + LazyVStack { + if store.addedFriend.isEmpty { + Text("아직 초대한 친구가 없습니다.") + .font(.pretendard(.medium, size: 15)) + .foregroundStyle(Color.mainText) + } + + ForEach(store.addedFriend, id: \.self.memberId) { friend in + let isAdded = store.addedFriend.map { $0.memberId }.contains(friend.memberId) + FriendAddItem(friend: friend, isAdded: isAdded) + .onTapGesture { + if isAdded { + store.send(.removeFriend(memberId: friend.memberId)) + } else { + store.send(.addFriend(friend)) + } + } + } + } + .padding(.horizontal, 25) + } + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendSearchListView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendSearchListView.swift new file mode 100644 index 00000000..1b54d695 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendInvite/SubViews/FriendSearchListView.swift @@ -0,0 +1,38 @@ +// +// FriendSearchListView.swift +// FeatureMoim +// +// Created by 권석기 on 11/4/24. +// + +import SwiftUI + +import ComposableArchitecture + +import DomainFriend +import FeatureMoimInterface + +struct FriendSearchListView: View { + let store: StoreOf + + var body: some View { + WithPerceptionTracking { + ScrollView { + LazyVStack { + ForEach(store.friendList, id: \.self.memberId) { friend in + let isAdded = store.addedFriend.map { $0.memberId }.contains(friend.memberId) + FriendAddItem(friend: friend, isAdded: isAdded) + .onTapGesture { + if isAdded { + store.send(.removeFriend(memberId: friend.memberId)) + } else { + store.send(.addFriend(friend)) + } + } + } + } + .padding(.horizontal, 25) + } + } + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendRequest/FriendRequestView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendRequest/FriendRequestView.swift new file mode 100644 index 00000000..527e4f70 --- /dev/null +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/FriendRequest/FriendRequestView.swift @@ -0,0 +1,29 @@ +// +// FriendRequestView.swift +// FeatureMoim +// +// Created by 권석기 on 11/1/24. +// + +import SwiftUI + +import FeatureFriend +import SharedDesignSystem + +import ComposableArchitecture + + +struct FriendRequestView: View { + let store: StoreOf + + var body: some View { + FriendRequestListView(store: store) + .namoNabBar(center: { + Text("새로운 요청") + .font(.pretendard(.bold, size: 16)) + }, left: { + NamoBackButton { + } + }) + } +} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainView.swift index 4f3b900d..f4f6466d 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainView.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainView.swift @@ -6,9 +6,11 @@ // import SwiftUI -import SharedDesignSystem + import FeatureFriend import FeatureMoimInterface +import SharedDesignSystem + import ComposableArchitecture public struct MainView: View { @@ -19,49 +21,41 @@ public struct MainView: View { } public var body: some View { - ZStack { - WithPerceptionTracking { - VStack(spacing: 0) { - TabBarView(currentTab: $store.currentTab, tabBarOptions: ["모임 일정", "친구리스트"]) + WithPerceptionTracking { + VStack(spacing: 0) { + TabBarView(currentTab: $store.currentTab, tabBarOptions: ["모임 일정", "친구리스트"]) + + TabView(selection: $store.currentTab) { + + MoimListView( + store: store.scope( + state: \.moimListStore, + action: \.moimListAction + ) + ) + .tag(0) - TabView(selection: $store.currentTab) { - - // 모임일정 리스트 - MoimListView(store: store.scope(state: \.moimList, action: \.moimList)) - .overlay(alignment: .bottomTrailing) { - FloatingButton { - store.send(.presentComposeSheet) - } - } - .fullScreenCover(isPresented: $store.isSheetPresented, content: { - MoimScheduleEditView(store: store.scope(state: \.moimEdit, action: \.moimEdit)) - .background(ClearBackground()) - }) - .tag(0) - - // 친구 리스트 - FriendListView(store: store.scope(state: \.friendList, action: \.friendList)) - .tag(1) - } - .tabViewStyle(.page(indexDisplayMode: .never)) + FriendListView( + store: store.scope( + state: \.friendListStore, + action: \.friendListAction + ) + ) + .tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + .namoNabBar(left: { + Text("Group Calendar") + .font(.pretendard(.bold, size: 22)) + .foregroundStyle(.black) + }, right: { + Button(action: { + store.send(.notificationButtonTap) + }) { + Image(asset: SharedDesignSystemAsset.Assets.icNotification) } - .namoNabBar(left: { - Text("Group Calendar") - .font(.pretendard(.bold, size: 22)) - .foregroundStyle(.black) - }, right: { - Button(action: { - store.send(.notificationButtonTap) - }) { - Image(asset: SharedDesignSystemAsset.Assets.icNotification) - } - }) - .overlay( - Color.black.opacity(0.5) - .ignoresSafeArea(.all) - .opacity(store.isSheetPresented == true ? 1 : 0) - ) - } + }) } } } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainViewStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainViewStore.swift index 3cde60d7..3c44bfa2 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainViewStore.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/MainViewStore.swift @@ -6,113 +6,57 @@ // import Foundation -import ComposableArchitecture -import FeatureMoimInterface + import DomainMoimInterface -import FeatureFriend import DomainFriend +import FeatureFriend +import FeatureMoimInterface + +import ComposableArchitecture @Reducer public struct MainViewStore { - @Dependency(\.moimUseCase) var moimUseCase public init() {} @ObservableState public struct State: Equatable { - public static let initialState = State(moimList: .init(), - friendList: .init(), - moimEdit: .init()) + public static let initialState = State(moimListStore: .init(), + friendListStore: .init()) // 현재 선택한탭 public var currentTab = 0 - // 일정생성뷰 - public var isSheetPresented = false - // 모임리스트 - var moimList: MoimListStore.State + public var moimListStore: MoimListStore.State // 친구리스트 - var friendList: FriendListStore.State + public var friendListStore: FriendListStore.State - // 모임생성 - var moimEdit: MoimEditStore.State } public enum Action: BindableAction { case binding(BindingAction) - case moimList(MoimListStore.Action) - case moimEdit(MoimEditStore.Action) - case friendList(FriendListStore.Action) - case presentDetailSheet(MoimSchedule) - case notificationButtonTap - case presentComposeSheet - case navigateToFriendCalendar(friend: Friend) + case moimListAction(MoimListStore.Action) + case friendListAction(FriendListStore.Action) + case notificationButtonTap + case refreshMoimList } public var body: some Reducer { BindingReducer() - Scope(state: \.moimList, action: \.moimList) { + Scope(state: \.moimListStore, action: \.moimListAction) { MoimListStore() } - Scope(state: \.friendList, action: \.friendList) { + Scope(state: \.friendListStore, action: \.friendListAction) { FriendListStore() } - Scope(state: \.moimEdit, action: \.moimEdit) { - MoimEditStore() - } Reduce { state, action in switch action { - // sheet가 사라질떄 데이터 로드 - case .binding(\.isSheetPresented): - if state.isSheetPresented == false { - state.moimEdit = .init() - return .send(.moimList(.viewOnAppear)) - } - return .none - // 모임 삭제/생성/취소 - case .moimEdit(.deleteButtonConfirm), - .moimEdit(.createButtonTapped), - .moimEdit(.cancleButtonTapped) - : - state.isSheetPresented = false - return .none - // 모임일정 선택 - case let .moimList(.moimCellSelected(meetingScheduleId)): - return .run { send in - let moimSchedule = try await moimUseCase.getMoimDetail(meetingScheduleId) - await send(.presentDetailSheet(moimSchedule)) - } catch: { error, send in - // 에러 처리 - } - - case .friendList(.gotoFriendCalendar(let friend)): - return .send(.navigateToFriendCalendar(friend: friend)) - // 모임일정 선택후 상태 - case let .presentDetailSheet(moimSchedule): - state.isSheetPresented = true - // TODO: 캡슐화 필요? - state.moimEdit.moimScheduleId = moimSchedule.scheduleId - state.moimEdit.title = moimSchedule.title - state.moimEdit.startDate = moimSchedule.startDate - state.moimEdit.endDate = moimSchedule.endDate - state.moimEdit.latitude = moimSchedule.latitude - state.moimEdit.longitude = moimSchedule.longitude - state.moimEdit.kakaoLocationId = moimSchedule.kakaoLocationId - state.moimEdit.participants = moimSchedule.participants - state.moimEdit.locationName = moimSchedule.locationName - state.moimEdit.imageUrl = moimSchedule.imageUrl - state.moimEdit.isOwner = moimSchedule.isOwner - state.moimEdit.mode = moimSchedule.isOwner ? .edit : .view - return .none - // 모임일정 초기화 - case .presentComposeSheet: - state.moimEdit = .init() - state.isSheetPresented = true - return .none + case .refreshMoimList: + return .send(.moimListAction(.viewOnAppear)) default: return .none } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/SubViews/TabBarView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/Subviews/TabBarView.swift similarity index 97% rename from Namo_SwiftUI/Projects/Feature/Moim/Sources/SubViews/TabBarView.swift rename to Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/Subviews/TabBarView.swift index fe23c6ac..c23beb1e 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/SubViews/TabBarView.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/Main/Subviews/TabBarView.swift @@ -38,7 +38,7 @@ struct TabBarView: View { Color.clear .frame(height: 1) } - } + } .fixedSize() .animation(.spring, value: currentTab) }) diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimEditStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimEditStore.swift index 9504b12b..1e2b9cdd 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimEditStore.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimEditStore.swift @@ -7,18 +7,20 @@ import SwiftUI import UIKit -import ComposableArchitecture -import FeatureMoimInterface + import DomainMoimInterface +import FeatureMoimInterface import SharedUtil +import ComposableArchitecture + extension MoimEditStore { public init() { - @Dependency(\.moimUseCase) var moimUseCase + @Dependency(\.moimUseCase) var moimUseCase let reducer: Reduce = Reduce { state, action in switch action { - case .binding(\.$coverImageItem): + case .binding(\.coverImageItem): return .run { [imageItem = state.coverImageItem] send in if let loaded = try? await imageItem?.loadTransferable(type: Data.self) { guard let uiImage = UIImage(data: loaded) else { @@ -41,20 +43,24 @@ extension MoimEditStore { case .createButtonTapped: return .run { [state = state] send in - if state.mode == .compose { - try await moimUseCase.createMoim(state.makeMoim(), state.coverImage) + if state.mode == .compose { + try await moimUseCase.createMoim(state.moimSchedule, state.coverImage) } else { - try await moimUseCase.editMoim(state.makeMoim(), state.coverImage) + try await moimUseCase.editMoim(state.moimSchedule, state.coverImage) } + await send(.createButtonConfirm) } case .deleteButtonTapped: state.isAlertPresented = true return .none case .deleteButtonConfirm: return .run { [state = state] send in - try await moimUseCase.withdrawMoim(state.moimScheduleId) - } - default: + try await moimUseCase.withdrawMoim(state.moimSchedule.scheduleId) + await send(.deleteConfirm) + } + case .createButtonConfirm: + return .none + default: return .none } } @@ -62,20 +68,7 @@ extension MoimEditStore { } } -extension MoimEditStore.State { - func makeMoim() -> MoimSchedule { - .init(scheduleId: moimScheduleId, - title: title, - imageUrl: imageUrl, - startDate: startDate, - endDate: endDate, - longitude: longitude, - latitude: latitude, - locationName: locationName, - kakaoLocationId: kakaoLocationId, - participants: []) - } -} + diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimScheduleEditView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimScheduleEditView.swift index 5c0e6f79..602df396 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimScheduleEditView.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/MoimScheduleEditView.swift @@ -5,133 +5,122 @@ // Created by 권석기 on 9/11/24. // +// +// MoimCreateView.swift +// FeatureMoim +// +// Created by 권석기 on 9/11/24. +// + import SwiftUI -import SharedDesignSystem import PhotosUI + import ComposableArchitecture +import Kingfisher + import FeatureMoimInterface +import FeaturePlaceSearchInterface import SharedUtil -import Kingfisher +import SharedDesignSystem + public struct MoimScheduleEditView: View { @Perception.Bindable private var store: StoreOf - @ObservedObject private var viewStore: ViewStoreOf + @Perception.Bindable private var placeStore: StoreOf + @State var draw = false public init(store: StoreOf) { self.store = store - self.viewStore = ViewStore(store, observe: { $0 }) + self.placeStore = .init(initialState: PlaceSearchStore.State(), reducer: { + PlaceSearchStore() + }) } /// 편집여부에 따라 보여지는 텍스트 설정 private var title: String { - switch viewStore.mode { - case .compose: - "새 모임 일정" - case .edit: - "모임 일정 편집" - case .view: - "모임 일정" + switch store.mode { + case .compose: "새 모임 일정" + case .edit: "모임 일정 편집" + case .view: "모임 일정" } } /// 편집여부에 따라서 보여지는 버튼 텍스트 설정 private var buttonTitle: String { - switch viewStore.mode { - case .compose: - "생성" - case .edit: - "저장" - case .view: - "" + switch store.mode { + case .compose: "생성" + case .edit: "저장" + case .view: "" } } - private var isVisible: Bool { - return !viewStore.isOwner - } - public var body: some View { - // deleteButton - deleteScheduleButton - .padding(.bottom, 6) - .padding(.top, 10) - .opacity(isVisible ? 0 : 1) - - VStack(spacing: 0) { - WithPerceptionTracking { - // title - headerView - .padding(.horizontal, 20) + WithPerceptionTracking { + VStack { + // deleteButton + DeleteCircleButton { + store.send(.deleteButtonTapped) + } + .offset(y: 20) + .opacity(!store.moimSchedule.isOwner ? 0 : 1) - // content - ScrollView { - VStack(spacing: 30) { - // textField - TextField("내 모임", text: viewStore.$title) - .font(.pretendard(.bold, size: 22)) - .foregroundStyle(Color.mainText) - .padding(.top, 20) - - // imagePicker - imagePickerView - - // 장소, 시간 - settingView - - // 친구 초대 - participantListView - - - // 일정보기 버튼 - showScheduleButton + WithPerceptionTracking { + VStack(spacing: 0) { + // title + headerView + .padding(.horizontal, 20) + // content + ScrollView { + VStack(spacing: 30) { + // textField + TextField("내 모임", text: $store.moimSchedule.title) + .font(.pretendard(.bold, size: 22)) + .foregroundStyle(Color.mainText) + .padding(.top, 20) + + // imagePicker + imagePickerView + + // 장소, 시간 + settingView + + // 친구 초대 + participantListView + + // 일정보기 버튼 + showScheduleButton + } + .padding(.horizontal, 30) + } } - .padding(.horizontal, 30) } + .background(.white) + .clipShape(UnevenRoundedRectangle(cornerRadii: .init( + topLeading: 15, + topTrailing: 15))) + .shadow(radius: 10) } + .edgesIgnoringSafeArea(.bottom) + .namoAlertView(isPresented: $store.isAlertPresented, + title: "모임 일정에서 정말 나가시겠어요?", + content: "모임 일정과 해당 일정의 기록을 더 이상 \n 보실 수 없으며, 방장 권한이 위임됩니다.", + confirmAction: { + store.send(.deleteButtonConfirm) + }) + .background(ClearBackground()) } - .background(.white) - .clipShape(RoundedCorners(radius: 15, corners: [.topLeft, .topRight])) - .shadow( - color: Color.black.opacity(0.15), - radius: 12, - x: 0, - y: 0 - ) - .mask(Rectangle().padding(.top, -20)) - .edgesIgnoringSafeArea(.bottom) - .namoAlertView(isPresented: viewStore.$isAlertPresented, - title: "모임 일정에서 정말 나가시겠어요?", - content: "모임 일정과 해당 일정의 기록을 \n 더 이상 보실 수 없습니다.", - confirmAction: { - viewStore.send(.deleteButtonConfirm) - }) } } extension MoimScheduleEditView { - private var deleteScheduleButton: some View { - Button(action: { - viewStore.send(.deleteButtonTapped) - }, label: { - Circle() - .frame(width: 40, height: 40) - .foregroundStyle(.white) - .overlay { - Image(asset: SharedDesignSystemAsset.Assets.icTrash) - } - .shadow( - color: Color.black.opacity(0.25), - radius: 6 - ) - }) - } - /// 일정 보기 private var showScheduleButton: some View { - Button(action: {}, label: { + Button(action: { + store.send(.goToFriendCalendar) + }, label: { HStack(spacing: 12) { Image(asset: SharedDesignSystemAsset.Assets.icCalendar) Text("초대한 친구 일정 보기") @@ -168,7 +157,7 @@ extension MoimScheduleEditView { Spacer() - if viewStore.mode == .view { + if store.mode == .view { Text("취소") .font(.pretendard(.regular, size: 15)) .foregroundStyle(Color.white) @@ -201,14 +190,14 @@ extension MoimScheduleEditView { Spacer() - PhotosPicker(selection: viewStore.$coverImageItem, matching: .images) { - if let coverImage = viewStore.coverImage { + PhotosPicker(selection: $store.coverImageItem, matching: .images) { + if let coverImage = store.coverImage { Image(uiImage: coverImage) .resizable() .frame(width: 55, height: 55) .cornerRadius(5) - } else if !viewStore.imageUrl.isEmpty { - KFImage(URL(string: viewStore.imageUrl)) + } else if !store.moimSchedule.imageUrl.isEmpty { + KFImage(URL(string: store.moimSchedule.imageUrl)) .placeholder({ Image(asset: SharedDesignSystemAsset.Assets.appLogo) }) @@ -236,16 +225,16 @@ extension MoimScheduleEditView { Spacer() - Text(viewStore.startDate.toYMDEHM()) + Text(store.moimSchedule.startDate.toYMDEHM()) .font(.pretendard(.regular, size: 15)) .foregroundStyle(Color.mainText) .onTapGesture { - viewStore.send(.startPickerTapped) + store.send(.startPickerTapped) } } - if viewStore.isStartPickerPresented { - DatePicker("startTimeDatePicker", selection: viewStore.$startDate) + if store.isStartPickerPresented { + DatePicker("startTimeDatePicker", selection: $store.moimSchedule.startDate) .datePickerStyle(.graphical) .labelsHidden() .tint(Color.mainOrange) @@ -259,39 +248,55 @@ extension MoimScheduleEditView { .foregroundStyle(Color.mainText) Spacer() - Text(viewStore.endDate.toYMDEHM()) + Text(store.moimSchedule.endDate.toYMDEHM()) .font(.pretendard(.regular, size: 15)) .foregroundStyle(Color.mainText) .onTapGesture { - viewStore.send(.endPickerTapped) + store.send(.endPickerTapped) } } - if viewStore.isEndPickerPresented { - DatePicker("endTimeDatePicker", selection: viewStore.$endDate) + if store.isEndPickerPresented { + DatePicker("endTimeDatePicker", selection: $store.moimSchedule.endDate) .datePickerStyle(.graphical) .labelsHidden() .tint(Color.mainOrange) } } - HStack { - Text("장소") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - Spacer() - - Button(action: { - store.send(.goToKakaoMapView) - }) { - HStack(spacing: 8) { - Text(viewStore.locationName) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Image(asset: SharedDesignSystemAsset.Assets.icRight) + VStack(spacing: 20) { + HStack { + Text("장소") + .font(.pretendard(.bold, size: 15)) + .foregroundStyle(Color.mainText) + Spacer() + + Button(action: { + store.send(.goToKakaoMapView) + }) { + HStack(spacing: 8) { + Text(store.moimSchedule.locationName) + .font(.pretendard(.regular, size: 15)) + .foregroundStyle(Color.mainText) + + Image(asset: SharedDesignSystemAsset.Assets.icRight) + } } } + + if !store.moimSchedule.kakaoLocationId.isEmpty { + KakaoMapView(store: placeStore, draw: $draw) + .id(store.moimSchedule.kakaoLocationId) + .onAppear { + placeStore.x = store.moimSchedule.latitude + placeStore.y = store.moimSchedule.longitude + draw = true + } + .onDisappear { draw = false } + .allowsHitTesting(false) + .frame(maxWidth: .infinity, minHeight: 190) + .border(Color.textUnselected, width: 1) + } } } } @@ -305,16 +310,18 @@ extension MoimScheduleEditView { .foregroundStyle(Color.mainText) Spacer() - Button(action: {}) { + Button(action: { + store.send(.goToFriendInvite) + }) { Image(asset: SharedDesignSystemAsset.Assets.icRight) } } - FlexibleGridView(data: viewStore.participants) { participant in - Participant(name: participant.nickname, - color: PalleteColor(rawValue: participant.colorId ?? 1)?.color ?? .clear, - isOwner: participant.isOwner) + FlexibleGridView(data: store.moimSchedule.participants) { participant in + ParticipantCell(name: participant.nickname, + pallete: .init(rawValue: participant.colorId ?? 1) ?? .namoOrange) } + .id(store.moimSchedule.participants.count) } } } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/PlaceSearchStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/PlaceSearchStore.swift deleted file mode 100644 index 2d5f84c4..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/PlaceSearchStore.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// PlaceSearchStore.swift -// FeatureMoim -// -// Created by 권석기 on 10/22/24. -// - -import Foundation diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/SubViews/Participant.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/SubViews/Participant.swift index f17226f1..93a8a0a2 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/SubViews/Participant.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimEdit/SubViews/Participant.swift @@ -6,24 +6,19 @@ // import SwiftUI + import SharedDesignSystem -struct Participant: View { +struct ParticipantCell: View { let name: String - let color: Color - let isOwner: Bool + let pallete: PalleteColor var body: some View { VStack { HStack(spacing: 9) { Circle() .frame(width: 14, height: 14) - .foregroundColor(color) - .overlay { - if isOwner { - Image(asset: SharedDesignSystemAsset.Assets.icCrown) - } - } + .foregroundColor(pallete.color) Text(name) .font(.pretendard(.regular, size: 15)) diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListStore.swift index 15a42d14..963ba641 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListStore.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListStore.swift @@ -6,26 +6,42 @@ // import Foundation -import ComposableArchitecture -import FeatureMoimInterface + import Domain +import FeatureMoimInterface -extension MoimListStore { - public init() { +import ComposableArchitecture + +public extension MoimListStore { + init() { @Dependency(\.moimUseCase) var moimUseCase let reducer: Reduce = Reduce { state, action in switch action { - // 뷰가 로드되면 모임리스트 요청 - case .viewOnAppear: - return .run { send in + // 뷰가 로드되면 모임리스트 요청 + case .viewOnAppear: + return .run { send in let result = try await moimUseCase.getMoimList() - await send(.moimListResponse(result)) + let moimList = IdentifiedArray(uniqueElements: result) + await send(.moimListResponse(moimList)) } - // 모임리스트 요청 결과 스토어 업데이트 - case let .moimListResponse(moimList): + // 모임리스트 요청 결과 스토어 업데이트 + case let .moimListResponse(moimList): state.moimList = moimList return .none + // 모임셀 선택 + case let .moimCellSelected(meetingScheduleId): + return .run { send in + let moimSchedule = try await moimUseCase.getMoimDetail(meetingScheduleId) + await send(.presentDetailSheet(moimSchedule)) + } + case .toggleFilterOption: + if state.filter == .allSchedules { + state.filter = .hidePastSchedules + } else { + state.filter = .allSchedules + } + return .none default: return .none } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListView.swift index a5f0f6c9..b90763cf 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListView.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/MoimListView.swift @@ -6,10 +6,12 @@ // import SwiftUI -import ComposableArchitecture -import SharedDesignSystem -import FeatureMoimInterface + import DomainMoimInterface +import FeatureMoimInterface +import SharedDesignSystem + +import ComposableArchitecture struct MoimListView: View { let store: StoreOf @@ -22,23 +24,46 @@ struct MoimListView: View { WithPerceptionTracking { ZStack { if !store.moimList.isEmpty { - ScrollView { - LazyVStack(spacing: 20) { - ForEach(store.moimList, id: \.meetingScheduleId) { moimSchedule in - MoimScheduleCell(scheduleItem: moimSchedule) - .onTapGesture { - store.send(.moimCellSelected(meetingScheduleId: moimSchedule.meetingScheduleId)) - } + VStack(spacing: 0) { + HStack(spacing: 0) { + Spacer() + + Text("지난 모임 일정 숨기기") + .font(.pretendard(.medium, size: 12)) + .foregroundStyle(Color.textDisabled) + + Image(asset: store.filter == .allSchedules ? SharedDesignSystemAsset.Assets.icCheck : SharedDesignSystemAsset.Assets.icCheckSelected) + .padding(.leading, 8) + .onTapGesture { + store.send(.toggleFilterOption) + } + } + .padding(.horizontal, 25) + .padding(.vertical, 12) + + ScrollView { + LazyVStack(spacing: 20) { + ForEach(store.filteredList) { moimSchedule in + MoimScheduleCell(scheduleItem: moimSchedule) + .onTapGesture { + store.send(.moimCellSelected(meetingScheduleId: moimSchedule.meetingScheduleId)) + } + } } + .padding(.horizontal, 25) } - .padding(20) } } else { EmptyListView(title: "모임 일정이 없습니다.\n 친구와의 모임 약속을 잡아보세요!") } } .frame(maxWidth: .infinity) - .onAppear { + .overlay(alignment: .bottomTrailing) { + FloatingButton { + store.send(.presentComposeSheet) + } + } + .onAppear { store.send(.viewOnAppear) } } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/SubViews/MoimScheduleCell.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/SubViews/MoimScheduleCell.swift index 344a1bce..ba448bba 100644 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/SubViews/MoimScheduleCell.swift +++ b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimList/SubViews/MoimScheduleCell.swift @@ -6,8 +6,10 @@ // import SwiftUI -import SharedDesignSystem + import DomainMoimInterface +import SharedDesignSystem + import Kingfisher struct MoimScheduleCell: View { @@ -21,6 +23,9 @@ struct MoimScheduleCell: View { VStack(spacing: 0) { HStack(spacing: 16) { KFImage(URL(string: scheduleItem.imageUrl)) + .placeholder({ + Image(asset: SharedDesignSystemAsset.Assets.moimDefaultImage) + }) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 48, height: 48) diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/DiaryEditView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/DiaryEditView.swift deleted file mode 100644 index f9451692..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/DiaryEditView.swift +++ /dev/null @@ -1,681 +0,0 @@ -// -// DiaryEditView.swift -// FeatureMoim -// -// Created by 권석기 on 9/13/24. -// - -import SwiftUI -import SharedDesignSystem -import PhotosUI - -public struct DiaryEditView: View { - @State private var showingParticipants = false - @State private var text = "" - @State private var text2 = "" - @State private var text3 = "" - @State private var text4 = "" - @State private var tabItems: [Int] = [0] - @State private var selection = 0 - @State private var showingAlert = false - @State private var showingAlert2 = false - @State private var showingParticipantView = false - @State private var showingCalculateView = false - @State private var showingTagView = false - @State private var participants: [String] = [] - @State private var myTag = "" - @State private var selectedItems: [PhotosPickerItem] = [] - @State private var seletedImages: [UIImage] = [] - - @FocusState private var textEditorFocused: Bool - - private let dummyFriends = ["코코아", "다나", "유즈", "루카", "뚜뚜", "캐슬", "고흐", "연현", "초코", "짱구"] - private let tags = ["없음", "술", "식사", "동창회", "공부"] - - public init() {} - - public var body: some View { - VStack { - ScrollView(showsIndicators: false) { - VStack(spacing: 20) { - dateView - .padding(.horizontal, 30) - participantListView - .padding(.horizontal, 30) - - TabView(selection: $selection) { - - ForEach(tabItems, id: \.self) { index in - if index == tabItems.count - 1 { - diaryEditView - } - else { - activityView - } - } - - } - .frame(height: 430) - .tabViewStyle(.page(indexDisplayMode: .never)) - - HStack { - ForEach(tabItems, id: \.self) { index in - if index == tabItems.count - 1 { - Rectangle() - .cornerRadius(2) - .frame(width: index == selection ? 24 : 10, height: 10) - .foregroundStyle(index == selection ? Color.namoOrange : Color.textUnselected) - .animation(.easeInOut, value: selection) - } else { - Rectangle() - .cornerRadius(index == selection ? 12 : 5) - .frame(width: index == selection ? 24 : 10, height: 10) - .foregroundStyle(index == selection ? Color.namoOrange : Color.textUnselected) - .animation(.easeInOut, value: selection) - } - - } - - - } - - addRecordButton - } - - .padding(.bottom, 25) - } - - Spacer() - - Button(action: {}, label: { - Text("기록 저장") - .font(.pretendard(.bold, size: 15)) - .frame(maxWidth: .infinity, minHeight: 82) - .foregroundColor(.white) - }) - .background(Color.mainOrange) - } - .namoNabBar(center: { - Text("나모 3기 회식") - .font(.pretendard(.bold, size: 22)) - .foregroundStyle(.black) - }, left: { - Button(action: { - showingAlert2 = true - }, label: { - Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) - }) - }) - .namoAlertView(isPresented: $showingAlert, title: "활동을 삭제하시겠어요?", content: "삭제한 모임 활동은 \n 모든 참석자의 기록에서 삭제됩니다.", confirmAction: { - - }) - .namoAlertView(isPresented: $showingAlert2, title: "편집된 내용이 저장되지 않습니다.", content: "정말 나가시겠어요?") - .namoPopupView(isPresented: $showingParticipantView, - title: "활동 참석자", - content: { - selecteParticipantView - }, confirmAction: { - - }) - .namoPopupView(isPresented: $showingCalculateView, - title: "정산 페이지", - content: { - calculateView - }, confirmAction: { - - }) - .namoPopupView(isPresented: $showingTagView, title: "태그", content: { - tagView - }, confirmAction: { - - }) - .edgesIgnoringSafeArea(.bottom) - } -} - -extension DiaryEditView { - private var dateView: some View { - HStack(spacing: 25) { - VStack { - Text("AUG") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Text("07") - .font(.pretendard(.bold, size: 36)) - .foregroundStyle(Color.mainText) - } - .padding(20) - .background { - Circle() - .foregroundColor(.white) - .shadow(color: .black.opacity(0.15), radius: 8) - } - - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 12) { - Text("날짜") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - - Text("2024.08.07 (수) 08:00") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - - HStack(spacing: 12) { - Text("장소") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Text("없음") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - } - - Spacer() - }.padding(.vertical, 20) - } - - private var participantListView: some View { - VStack(spacing: 0) { - HStack { - Text("참석자 (10)") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - Button(action: { - showingParticipants.toggle() - }, label: { - Image(asset: SharedDesignSystemAsset.Assets.icUp) - .rotationEffect(.degrees(showingParticipants ? 180 : 0)) - .animation(.none) - }) - } - - if showingParticipants { - ParticipantListView(friendList: dummyFriends) - } - - } - } - - private var diaryEditView: some View { - VStack(spacing: 0) { - VStack(spacing: 0) { - // header - HStack { - Text("일기장") - .font(.pretendard(.bold, size: 18)) - .foregroundStyle(Color.mainText) - - Spacer() - - Image(asset: SharedDesignSystemAsset.Assets.icPrivate) - .renderingMode(.template) - .foregroundColor(Color.textPlaceholder) - } - - // heart - HStack { - Text("재미도") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - HStack(spacing: 4) { - ForEach(0..<2, id: \.self) { _ in - Image(asset: SharedDesignSystemAsset.Assets.icHeartSelected) - .resizable() - .frame(width: 18, height: 18) - } - Image(asset: SharedDesignSystemAsset.Assets.icHeart) - .resizable() - .frame(width: 18, height: 18) - } - } - .padding(.top, 24) - - // texteditor - VStack(spacing: 10) { - ZStack(alignment: .topLeading) { - TextEditor(text: $text) - .padding(.vertical, 12) - .padding(.horizontal, 16) - .focused($textEditorFocused) - .lineSpacing(5) - .font(.pretendard(.regular, size: 14)) - .foregroundColor(Color.mainText) - .scrollContentBackground(.hidden) - .frame(height: 160) - - if text.isEmpty && !textEditorFocused { - Text("내용 입력") - .font(.pretendard(.bold, size: 14)) - .foregroundStyle(Color.textUnselected) - .padding(.top, 18) - .padding(.bottom, 12) - .padding(.horizontal, 16) - } - } - .background(Color.itemBackground) - .cornerRadius(10) - .padding(.top, 16) - - HStack { - Spacer() - Text("\(text.count) / 200") - .font(.pretendard(.bold, size: 12)) - .foregroundStyle(Color.textUnselected) - } - } - - photoPickerView - .padding(.top, 20) - } - .padding(20) - } - .background(.white) - .cornerRadius(10) - .shadow(color: .black.opacity(0.15), - radius: 6) - .padding(.horizontal, 30) - } - - private var activityView: some View { - VStack(spacing: 0) { - VStack(spacing: 0) { - // textfield - HStack { - TextField("활동 이름", text: $text2) - .font(.pretendard(.bold, size: 18)) - .foregroundStyle(Color.mainText) - - Spacer() - - Button(action: { - showingAlert = true - }, label: { - Image(asset: SharedDesignSystemAsset.Assets.icTrash) - .renderingMode(.template) - .foregroundStyle(Color.textUnselected) - .overlay { - Circle() - .stroke(Color.textUnselected, lineWidth: 1) - .frame(width: 40, height: 40) - } - }) - } - - // form - VStack(spacing: 16) { - HStack { - Text("활동 참석자") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - Spacer() - - Button(action: { - showingParticipantView = true - }) { - HStack(spacing: 8) { - Text(participants.isEmpty ? "없음" : participants.map { $0 }.joined(separator: ",")) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - .lineLimit(1) - - Image(asset: SharedDesignSystemAsset.Assets.icRight) - } - } - } - - HStack { - Text("시작") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - Text("2024.08.07 (수) 08:00 AM") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - - HStack { - Text("종료") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - Text("2024.08.07 (수) 08:00 AM") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - - HStack { - Text("장소") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - Spacer() - - Button(action: {}) { - HStack(spacing: 8) { - Text("없음") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Image(asset: SharedDesignSystemAsset.Assets.icRight) - } - } - } - - HStack { - Text("정산") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - Spacer() - - Button(action: { - showingCalculateView = true - }) { - HStack(spacing: 8) { - Text("총 0원") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Image(asset: SharedDesignSystemAsset.Assets.icRight) - } - } - } - - HStack { - Text("태그") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - Spacer() - - Button(action: { - showingTagView = true - }) { - HStack(spacing: 8) { - Text(myTag.isEmpty ? "없음" : myTag) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Image(asset: SharedDesignSystemAsset.Assets.icRight) - } - } - } - } - .padding(.top, 20) - - // image - photoPickerView - .padding(.top, 47) - } - .padding(20) - }.background(.white) - .cornerRadius(10) - .shadow(color: .black.opacity(0.15), - radius: 6) - .padding(.horizontal, 30) - } - - private var addRecordButton: some View { - Button(action: { - tabItems.append(tabItems.count) - }, label: { - HStack(spacing: 12) { - Image(asset: SharedDesignSystemAsset.Assets.icDiary) - Text("활동 추가") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(.black) - } - .padding(.vertical, 10) - .padding(.horizontal, 20) - .background(.white) - }) - .cornerRadius(20) - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(.black, lineWidth: 1) - ) - } - - private var selecteParticipantView: some View { - VStack { - let item = GridItem(.flexible(), spacing: 0) - let columns = Array(repeating: item, count: 2) - - LazyVGrid(columns: columns, alignment: .leading, spacing: 16) { - ForEach(dummyFriends, id: \.self) { name in - HStack(spacing: 16) { - Image(asset: participants.contains(name) ? SharedDesignSystemAsset.Assets.icCheckCircleSelected : SharedDesignSystemAsset.Assets.icCheckCircle) - .onTapGesture { - if participants.contains(name) { - participants = participants.filter { $0 != name } - } else { - participants.append(name) - } - } - - Text(name) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - } - } - } - .padding(35) - } - - private var calculateView: some View { - VStack(spacing: 24) { - VStack(spacing: 13) { - HStack(spacing: 97) { - Text("총 금액") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - VStack { - TextField("금액 입력", value: $text3, formatter: NumberFormatter()) - .keyboardType(.numberPad) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - .padding(6) - .padding(.leading, 52) - } - .background(Color.mainGray) - .cornerRadius(5) - } - - HStack { - Text("인원 수") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - Image(asset: SharedDesignSystemAsset.Assets.icDivision) - - Spacer() - - Text("총 \(participants.count)명") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - - HStack { - Text("인당 금액") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - - Text("0 원") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - } - - VStack { - let item = GridItem(.flexible(), spacing: 0) - let columns = Array(repeating: item, count: 2) - - LazyVGrid(columns: columns, alignment: .leading, spacing: 16) { - ForEach(dummyFriends, id: \.self) { name in - HStack(spacing: 16) { - Image(asset: participants.contains(name) ? SharedDesignSystemAsset.Assets.icCheckCircleSelected : SharedDesignSystemAsset.Assets.icCheckCircle) - .onTapGesture { - if participants.contains(name) { - participants = participants.filter { $0 != name } - } else { - participants.append(name) - } - } - - Text(name) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - } - } - } - .padding(5) - } - .padding(35) - } - - private var tagView: some View { - VStack(alignment: .leading, spacing: 16) { - ForEach(tags, id: \.self) { tag in - HStack(spacing: 16) { - Image(asset: tag == myTag ? SharedDesignSystemAsset.Assets.icRatioCircleSelected : SharedDesignSystemAsset.Assets.icRatioCircle) - .onTapGesture { - if tag == myTag { - myTag = "" - } else { - myTag = tag - } - } - - Text(tag) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Spacer() - } - } - - HStack(spacing: 12) { - HStack(spacing: 16) { - Image(asset: myTag == "custom" ? SharedDesignSystemAsset.Assets.icRatioCircleSelected : SharedDesignSystemAsset.Assets.icRatioCircle) - .onTapGesture { - if myTag == "custom" { - myTag = "" - } else { - myTag = "custom" - } - } - - Text("직접 설정: ") - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - } - - VStack { - TextField("입력", text: $text4) - .font(.pretendard(.regular, size: 15)) - .foregroundStyle(Color.mainText) - - Rectangle() - .frame(maxWidth: .infinity, maxHeight: 1) - .foregroundStyle(Color.textPlaceholder) - } - } - } - .padding(35) - } - - private var photoPickerView: some View { - HStack { - PhotosPicker(selection: $selectedItems, maxSelectionCount: 3) { - ForEach(Array(seletedImages.enumerated()), id: \.self.element) { (offset, image) in - Image(uiImage: image) - .resizable() - .frame(width: 92, height: 92) - .overlay(alignment: .topTrailing) { - Circle() - .frame(width: 20, height: 20) - .foregroundStyle(.white) - .overlay { - Image(asset: SharedDesignSystemAsset.Assets.icXmark) - } - .offset(x: 10, y: -10) - .onTapGesture { - seletedImages.remove(at: offset) - } - } - } - if seletedImages.count < 3 { - Image(asset: SharedDesignSystemAsset.Assets.noPicture) - .resizable() - .frame(width: 92, height: 92) - } - } - - Spacer() - } - .onChange(of: selectedItems, perform: { value in - loadSelectedPhotos() - }) - } -} - -extension DiaryEditView { - private func loadSelectedPhotos() { - Task { - seletedImages.removeAll() - await withTaskGroup(of: (UIImage?, Error?).self) { groupTask in - for photoItem in selectedItems { - groupTask.addTask { - do { - if let imageData = try await photoItem.loadTransferable(type: Data.self) { - - let image = UIImage(data: imageData) - // 중복이미지 제외 - if seletedImages.contains(where: {$0.pngData() == image?.pngData()}) { - return (nil, nil) - } - return (image, nil) - } - return (nil, nil) - } catch { - return (nil, error) - } - } - } - - for await result in groupTask { - if let error = result.1 { - break - } else if let image = result.0 { - DispatchQueue.main.async { - seletedImages.append(image) - } - } - } - } - } - } -} - - - diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/ParticipantListView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/ParticipantListView.swift deleted file mode 100644 index 3b482372..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRecord/ParticipantListView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ParticipantListView.swift -// FeatureMoim -// -// Created by 권석기 on 9/15/24. -// - -import SwiftUI -import SharedDesignSystem - -struct ParticipantListView: View { - let friendList: [String] - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(alignment: .center) { - Circle() - .frame(width: 42, height: 42) - .foregroundColor(Color.mainText) - .overlay { - Image(asset: SharedDesignSystemAsset.Assets.icCalendar) - .renderingMode(.template) - .foregroundColor(Color.white) - } - - ForEach(0..<10, id: \.self) { _ in - Text("코코아") - .font(.pretendard(.bold, size: 11)) - .foregroundStyle(Color.mainText) - .background { - Circle() - .stroke(Color.mainText, lineWidth: 2) - .frame(width: 42, height: 42) - } - .padding(.horizontal, 8) - } - } - .frame(height: 60) - } - } -} - - diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestStore.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestStore.swift deleted file mode 100644 index f11f8706..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestStore.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MoimRequestStore.swift -// FeatureMoim -// -// Created by 권석기 on 9/25/24. -// - -import Foundation -import ComposableArchitecture -import FeatureMoimInterface - -extension MoimRequestStore { - public init() { - let reducer: Reduce = Reduce { state, action in - switch action { - default: - return .none - } - } - self.init(reducer: reducer) - } -} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestView.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestView.swift deleted file mode 100644 index fffcdbc7..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/MoimRequestView.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// MoimRequestView.swift -// FeatureMoim -// -// Created by 권석기 on 9/11/24. -// - -import SwiftUI -import ComposableArchitecture -import SharedDesignSystem -import FeatureFriend -import FeatureMoimInterface - - -public struct MoimRequestView: View { - - private enum Tab: CaseIterable { - case moimRequest - case friendRequest - - var tabName: String { - switch self { - case .moimRequest: - "모임 요청" - case .friendRequest: - "친구 요청" - } - } - - var index: Int { - switch self { - case .moimRequest: - 0 - case .friendRequest: - 1 - } - } - } - - let store: StoreOf - - @State private var selectedTab: Tab = .moimRequest - @State private var tabSizes: [CGRect] = [] - - private var padding: CGFloat = 10 - - private var isTabSize: Bool { - tabSizes.count == Tab.allCases.count - } - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - VStack { - - } - .namoNabBar(center: { - Text("새로운 요청") - .font(.pretendard(.bold, size: 16)) - .foregroundStyle(.black) - }, left: { - Button(action: { - store.send(.backButtonTap) - }, label: { - Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) - }) - }) - } - -} - diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestItem.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestItem.swift deleted file mode 100644 index 5e4fec61..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestItem.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// MoimRequestItem.swift -// FeatureMoim -// -// Created by 권석기 on 9/11/24. -// - -import SwiftUI -import SharedDesignSystem - -struct MoimRequestItem: View { - var body: some View { - VStack(spacing: 0) { - HStack(spacing: 16) { - Image(asset: SharedDesignSystemAsset.Assets.mongi1) - .frame(width: 48, height: 48) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 4) { - Text("2024.08.07 (수) 12:00") - .font(.pretendard(.regular, size: 12)) - .foregroundStyle(Color.mainText) - - Text("나모 3기 회식") - .font(.pretendard(.bold, size: 15)) - .foregroundStyle(Color.mainText) - .lineLimit(1) - - HStack { - Text("10") - .font(.pretendard(.bold, size: 12)) - .foregroundStyle(Color.mainText) - - Text("코코아, 유즈, 뚜뚜, 고흐, 초코, 다나, 반디, 램프, 연현 let's go") - .font(.pretendard(.regular, size: 12)) - .foregroundStyle(Color.mainText) - .lineLimit(1) - } - - } - - } - .padding(.leading, 16) - .padding(.trailing, 20) - .padding(.vertical, 17) - .background(Color.itemBackground) - - HStack { - Button( - action: { - - }, label: { - Text("수락") - .font(.pretendard(.bold, size: 16)) - .frame(maxWidth: .infinity) - } - ) - .tint(Color.mainOrange) - - Divider() - .background(Color.textUnselected) - .frame(width: 1, height: 32) - - Button( - action: { - - }, label: { - Text("거절") - .font(.pretendard(.bold, size: 16)) - .frame(maxWidth: .infinity) - } - ) - .tint(Color.colorBlack) - } - .background(Color.white) - .frame(height: 52) - - } - .frame(maxWidth: .infinity) - .background() - .clipShape(RoundedRectangle(cornerRadius: 10)) - .shadow(color: .black.opacity(0.1), radius: 4) - } -} - -#Preview { - MoimRequestItem() -} diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestList.swift b/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestList.swift deleted file mode 100644 index a2aafbd5..00000000 --- a/Namo_SwiftUI/Projects/Feature/Moim/Sources/MoimRequest/SubViews/MoimRequestList.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MoimRequestList.swift -// FeatureMoim -// -// Created by 권석기 on 9/11/24. -// - -import SwiftUI - -struct MoimRequestList: View { - var body: some View { - ScrollView { - LazyVStack { - ForEach(0..<10, id: \.self) { index in - MoimRequestItem() - .padding(.top, 20) - } - } - .padding(.horizontal, 25) - } - } -} - -#Preview { - MoimRequestList() -} diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchInterface.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchInterface.swift index 99e285be..cb25446e 100644 --- a/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchInterface.swift +++ b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchInterface.swift @@ -19,10 +19,7 @@ public struct PlaceSearchStore { @ObservableState public struct State: Equatable { - public init() {} - - /// 맵뷰 렌더링 여부 - public var draw: Bool = false + public init() {} /// 고유 id(poiID로 사용) public var id: String = "" @@ -41,6 +38,7 @@ public struct PlaceSearchStore { /// 검색결과 리스트 public var placeList: [LocationInfo] = [] + } public enum Action: BindableAction { @@ -53,13 +51,13 @@ public struct PlaceSearchStore { case responsePlaceList([LocationInfo]) /// 위치핀 탭 - case poiTapped(String) + case poiTapped(String) - /// 뷰나타남 - case viewOnAppear + /// 뒤로가기 + case backButtonTapped - /// 뷰사라짐 - case viewOnDisappear + /// 위치 업데이트 + case locationUpdated(LocationInfo) } public var body: some ReducerOf { diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchStore.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchStore.swift similarity index 75% rename from Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchStore.swift rename to Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchStore.swift index a9fd9ee4..a88d875c 100644 --- a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchStore.swift +++ b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchStore.swift @@ -6,7 +6,7 @@ // import Foundation -import FeaturePlaceSearchInterface + import DomainPlaceSearch import ComposableArchitecture @@ -27,11 +27,13 @@ public extension PlaceSearchStore { case let .poiTapped(poiID): state.id = poiID return .none - case .viewOnAppear: - state.draw = true - return .none - case .viewOnDisappear: - state.draw = false + case let .locationUpdated(locationInfo): + state.locationName = locationInfo.placeName + state.id = locationInfo.id + guard let x = Double(locationInfo.x), + let y = Double(locationInfo.y) else { return .none } + state.x = x + state.y = y return .none default: return .none diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchView.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchView.swift similarity index 92% rename from Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchView.swift rename to Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchView.swift index 7b42a898..9c271f2b 100644 --- a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/PlaceSearchView.swift +++ b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/PlaceSearchView.swift @@ -8,10 +8,10 @@ import SwiftUI import SharedDesignSystem import ComposableArchitecture -import FeaturePlaceSearchInterface public struct PlaceSearchView: View { @Perception.Bindable private var store: StoreOf + @State var draw = false public init(store: StoreOf) { self.store = store @@ -35,9 +35,9 @@ public struct PlaceSearchView: View { } private var mapView: some View { - KakaoMapView(store: store) - .onAppear { store.send(.viewOnAppear) } - .onDisappear { store.send(.viewOnDisappear) } + KakaoMapView(store: store, draw: $draw) + .onAppear { draw = true } + .onDisappear { draw = false } .frame(maxWidth: .infinity, maxHeight: 380) } @@ -104,7 +104,9 @@ public struct PlaceSearchView: View { } private var backButton: some View { - Button(action: {}, label: { + Button(action: { + store.send(.backButtonTapped) + }, label: { Circle() .overlay ( Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/SubViews/KakaoMapView.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/SubViews/KakaoMapView.swift similarity index 73% rename from Namo_SwiftUI/Projects/Feature/placeSearch/Sources/SubViews/KakaoMapView.swift rename to Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/SubViews/KakaoMapView.swift index fb75ae3c..8c92bc00 100644 --- a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/SubViews/KakaoMapView.swift +++ b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/SubViews/KakaoMapView.swift @@ -6,20 +6,22 @@ // import SwiftUI +import Combine + import KakaoMapsSDK -import SharedUtil -import SharedDesignSystem import ComposableArchitecture -import FeaturePlaceSearchInterface + import DomainPlaceSearchInterface -import Combine +import SharedUtil +import SharedDesignSystem public struct KakaoMapView: UIViewRepresentable { let store: StoreOf - + @Binding var draw: Bool - public init(store: StoreOf) { + public init(store: StoreOf, draw: Binding) { self.store = store + self._draw = draw } public func makeUIView(context: Context) -> some KMViewContainer { @@ -32,14 +34,19 @@ public struct KakaoMapView: UIViewRepresentable { } public func updateUIView(_ uiView: UIViewType, context: Context) { - if store.draw { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - context.coordinator.controller?.prepareEngine() - context.coordinator.controller?.activateEngine() + guard let controller = context.coordinator.controller else { return } + if draw { + if !controller.isEngineActive { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + context.coordinator.controller?.prepareEngine() + context.coordinator.controller?.activateEngine() + } + } + } else { + if controller.isEngineActive { + context.coordinator.controller?.pauseEngine() + context.coordinator.controller?.resetEngine() } - } - else { - context.coordinator.controller?.resetEngine() } } @@ -62,6 +69,11 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent super.init() } + deinit { + controller?.pauseEngine() + controller?.resetEngine() + } + // MARK: - Helper Methods public func addViews() { let defaultPosition: MapPoint = MapPoint(longitude: 127.108678, latitude: 37.402001) @@ -84,6 +96,8 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent // 검색리스트 구독 store.publisher .placeList + .dropFirst() + .receive(on: DispatchQueue.main) .sink { [weak self] placeList in guard let firstLocation = placeList.first, let longitude = Double(firstLocation.x), @@ -99,18 +113,34 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent // 검색ID 구독 store.publisher .id + .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] poiID in self?.changePoiStyle(poiID: poiID) self?.showInfoWindow(poiID: poiID) }) .store(in: &cancellables) + + // 검색결과x 좌표만 있는경우 + Publishers + .CombineLatest(store.publisher.x, store.publisher.y) + .filter { [weak self] (x, y) in + guard let self = self else { return false } + return store.placeList.isEmpty + } + .filter { (x, y) in x != 0 && y != 0 } + .sink(receiveValue: { [weak self] (x, y) in + self?.createPoiStyle() + self?.createLabelLayer() + self?.createPoi(longitude: y, latitude: x) + }) + .store(in: &cancellables) } /// Poi 상단에 나타나는 infoWindow를 보여줍니다. private func showInfoWindow(poiID: String) { + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } guard let index = store.placeList.firstIndex(where: {$0.id == poiID}) else { return } let curPlace = store.placeList[index] - let mapView: KakaoMap = controller?.getView("mapview") as! KakaoMap guard let longitude = Double(curPlace.x), let latitude = Double(curPlace.y) else { return } let guiManager = mapView.getGuiManager() @@ -120,14 +150,14 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent let infoWindow = InfoWindow("infoWindow") let bodyImage = GuiImage("bgImage") - + bodyImage.image = UIImage.getDesignSystemImage(named: "ic_info_window") bodyImage.imageStretch = GuiEdgeInsets(top: 9, left: 9, bottom: 9, right: 9) // tailImage let tailImage = GuiImage("tailImage") - + tailImage.image = UIImage.getDesignSystemImage(named: "ic_info_window_tail") //bodyImage의 child로 들어갈 layout. let layout: GuiLayout = GuiLayout("layout") @@ -159,9 +189,29 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent infoWindow.show() } + private func createPoi(longitude: Double, latitude: Double) { + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } + let manager = mapView.getLabelManager() + let layer = manager.getLabelLayer(layerID: "PoiLayer") + + let poiOption = PoiOptions(styleID: "selectedStyle", poiID: UUID().uuidString) + + poiOption.rank = 0 + poiOption.clickable = true + + let poi1 = layer?.addPoi(option:poiOption, at: MapPoint(longitude: longitude, latitude: latitude)) + + poi1?.show() + + layer?.showAllPois() + + moveCamera(longitude: longitude, latitude: latitude, durationInMillis: 0) + + } + /// poiStyle 변경 private func changePoiStyle(poiID: String) { - let mapView: KakaoMap = controller?.getView("mapview") as! KakaoMap + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } let manager = mapView.getLabelManager() let layer = manager.getLabelLayer(layerID: "PoiLayer") @@ -180,7 +230,7 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent /// 카메라 이동 private func moveCamera(longitude: Double, latitude: Double, durationInMillis: UInt = 1500) { - let mapView: KakaoMap = controller?.getView("mapview") as! KakaoMap + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } let cameraUpdate = CameraUpdate.make(cameraPosition: CameraPosition(target: MapPoint(longitude: longitude, latitude: latitude), height: 200, rotation: 0, tilt: 0)) mapView.animateCamera(cameraUpdate: cameraUpdate, options: CameraAnimationOptions( @@ -191,16 +241,16 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent /// LabelLayer 생성 private func createLabelLayer() { - let view: KakaoMap = controller?.getView("mapview") as! KakaoMap - let manager = view.getLabelManager() + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } + let manager = mapView.getLabelManager() let layerOption = LabelLayerOptions(layerID: "PoiLayer", competitionType: .none, competitionUnit: .symbolFirst, orderType: .rank, zOrder: 1000) let _ = manager.addLabelLayer(option: layerOption) } /// poiStyle 설정 func createPoiStyle() { - let view = controller?.getView("mapview") as! KakaoMap - let manager = view.getLabelManager() + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } + let manager = mapView.getLabelManager() // 검색 시 기본 핀 @@ -217,8 +267,8 @@ public class KakaoMapCoordinator: NSObject, MapControllerDelegate, KakaoMapEvent /// poi 생성 func createPois(_ placeList: [LocationInfo]) { - let view = controller?.getView("mapview") as! KakaoMap - let manager = view.getLabelManager() + guard let mapView: KakaoMap = controller?.getView("mapview") as? KakaoMap else { return } + let manager = mapView.getLabelManager() let layer = manager.getLabelLayer(layerID: "PoiLayer") let pois = layer?.getAllPois().map { $0.map { return $0.itemID} } ?? [] diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/SubViews/PlaceCell.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/SubViews/PlaceCell.swift similarity index 100% rename from Namo_SwiftUI/Projects/Feature/placeSearch/Sources/SubViews/PlaceCell.swift rename to Namo_SwiftUI/Projects/Feature/placeSearch/Interface/Sources/SubViews/PlaceCell.swift diff --git a/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/sample.swift b/Namo_SwiftUI/Projects/Feature/placeSearch/Sources/sample.swift new file mode 100644 index 00000000..e69de29b diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Contents.json b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Contents.json index 0f6ad003..c41cf85f 100644 --- a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Contents.json +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "isSelected=yes, shape=square.png", + "filename" : "ic_check.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "isSelected=yes, shape=square@2x.png", + "filename" : "Rectangle 65@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "isSelected=yes, shape=square@3x.png", + "filename" : "Rectangle 65@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@2x.png new file mode 100644 index 00000000..75234e0f Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@2x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@3x.png new file mode 100644 index 00000000..a3758f8d Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/Rectangle 65@3x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/ic_check.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/ic_check.png new file mode 100644 index 00000000..07ed9f10 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/ic_check.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/Contents.json b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/Contents.json new file mode 100644 index 00000000..0f6ad003 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "isSelected=yes, shape=square.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "isSelected=yes, shape=square@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "isSelected=yes, shape=square@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square.png similarity index 100% rename from Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square.png rename to Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square.png diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square@2x.png similarity index 100% rename from Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square@2x.png rename to Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square@2x.png diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square@3x.png similarity index 100% rename from Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check.imageset/isSelected=yes, shape=square@3x.png rename to Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_check_selected.imageset/isSelected=yes, shape=square@3x.png diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Contents.json b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Contents.json index 45f8d788..3cc9cc3d 100644 --- a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Contents.json +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "Property=Group.png", + "filename" : "Property=Group, Selected=false.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Property=Group@2x.png", + "filename" : "Property=Group, Selected=false@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Property=Group@3x.png", + "filename" : "Property=Group, Selected=false@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false.png new file mode 100644 index 00000000..2a17f225 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@2x.png new file mode 100644 index 00000000..50435c51 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@2x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@3x.png new file mode 100644 index 00000000..3646b069 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group, Selected=false@3x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group.png deleted file mode 100644 index dabdc0c4..00000000 Binary files a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group.png and /dev/null differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@2x.png deleted file mode 100644 index 9c73633f..00000000 Binary files a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@2x.png and /dev/null differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@3x.png deleted file mode 100644 index 25ef5ec6..00000000 Binary files a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewIcon/ic_group.imageset/Property=Group@3x.png and /dev/null differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/Contents.json b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/Contents.json new file mode 100644 index 00000000..a6c70487 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "profile_mask.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "profile_mask@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "profile_mask@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask.png new file mode 100644 index 00000000..8e6ba209 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@2x.png new file mode 100644 index 00000000..cc0b2e48 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@2x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@3x.png new file mode 100644 index 00000000..033e1fc9 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/friendDefaultImage.imageset/profile_mask@3x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Contents.json b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Contents.json new file mode 100644 index 00000000..688fbff0 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "moimDefaultImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@2x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@2x.png new file mode 100644 index 00000000..e383203b Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@2x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@3x.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@3x.png new file mode 100644 index 00000000..def06c77 Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/Group@3x.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/moimDefaultImage.png b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/moimDefaultImage.png new file mode 100644 index 00000000..5c24e88a Binary files /dev/null and b/Namo_SwiftUI/Projects/Shared/DesignSystem/Resources/Assets.xcassets/NewImage/moimDefaultImage.imageset/moimDefaultImage.png differ diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/ClearBackgournd.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/ClearBackgournd.swift index df6060fc..4f35aec7 100644 --- a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/ClearBackgournd.swift +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/ClearBackgournd.swift @@ -30,5 +30,6 @@ class ClearBackgroundView: UIView { return } parentView.backgroundColor = .clear + } } diff --git a/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Components/DeleteCircleButton.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/DeleteCircleButton.swift similarity index 55% rename from Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Components/DeleteCircleButton.swift rename to Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/DeleteCircleButton.swift index 463f31a9..1e75f14f 100644 --- a/Namo_SwiftUI/Projects/Feature/Home/Sources/ScheduleEdit/Components/DeleteCircleButton.swift +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/DeleteCircleButton.swift @@ -7,20 +7,22 @@ import SwiftUI -import SharedDesignSystem - -struct DeleteCircleButton: View { - - let action: () -> Void +public struct DeleteCircleButton: View { + + private let action: () -> Void + + public init(action: @escaping () -> Void) { + self.action = action + } - var body: some View { + public var body: some View { ZStack { Circle() .fill(.white) .frame(width: 40, height: 40) .shadow(radius: 2) - Image(asset: SharedDesignSystemAsset.Assets.icDeleteSchedule) + Image(asset: SharedDesignSystemAsset.Assets.icDeleteSchedule) .resizable() .frame(width: 24, height: 24) diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/Dialog/NamoDialogModifier.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/Dialog/NamoDialogModifier.swift index 66c3af76..356694fd 100644 --- a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/Dialog/NamoDialogModifier.swift +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/Dialog/NamoDialogModifier.swift @@ -85,7 +85,8 @@ public struct NamoDialogModifier: ViewModifier { .background(ClearBackground()) }) .transaction { transaction in - transaction.disablesAnimations = true + transaction.animation = nil + transaction.disablesAnimations = true } } } diff --git a/Namo_SwiftUI/Projects/Feature/Moim/Sources/SubViews/FlexibleGridView.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/FlexibleGridView.swift similarity index 100% rename from Namo_SwiftUI/Projects/Feature/Moim/Sources/SubViews/FlexibleGridView.swift rename to Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/FlexibleGridView.swift diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoBackButton.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoBackButton.swift new file mode 100644 index 00000000..acffa7c5 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoBackButton.swift @@ -0,0 +1,27 @@ +// +// NamoBackButton.swift +// SharedDesignSystem +// +// Created by 권석기 on 11/1/24. +// + +import SwiftUI + +public struct NamoBackButton: View { + let action: () -> () + + public init(action: @escaping () -> ()) { + self.action = action + } + public var body: some View { + Button(action: { + action() + }, label: { + Image(asset: SharedDesignSystemAsset.Assets.icArrowLeftThick) + }) + } +} + +#Preview { + NamoBackButton(action: {}) +} diff --git a/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoTabView.swift b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoTabView.swift new file mode 100644 index 00000000..10b305a6 --- /dev/null +++ b/Namo_SwiftUI/Projects/Shared/DesignSystem/Sources/Components/NamoTabView.swift @@ -0,0 +1,69 @@ +// +// NamoTabView.swift +// SharedDesignSystem +// +// Created by 권석기 on 11/6/24. +// + +import SwiftUI + +public enum Tab: CaseIterable { + case home, group + + var iconName: SharedDesignSystemImages { + switch self { + case .home: + .init(name: "ic_home") + case .group: + .init(name: "ic_group") + } + } + + var selectedIconName: SharedDesignSystemImages { + switch self { + case .home: + .init(name: "ic_home_selected") + case .group: + .init(name: "ic_group_selected") + } + } +} + +public struct NamoTabView: View { + @Binding var currentTab: Tab + + public init(currentTab: Binding) { + self._currentTab = currentTab + } + + public var body: some View { + VStack { + HStack(spacing: 0) { + Spacer() + + ForEach(Tab.allCases, id: \.hashValue) { tab in + Image(asset: currentTab == tab ? tab.selectedIconName : tab.iconName) + .frame(maxWidth: .infinity) + .onTapGesture { + currentTab = tab + } + } + + Spacer() + } + .padding(.top, 10) + + Spacer() + } + .padding(.horizontal, 25) + .frame(height: 80) + .background( + Rectangle() + .fill(Color.white) + .shadow(color: .black.opacity(0.15), radius: 6, x: 0, y: 0) + ) + } + +} + + diff --git a/Namo_SwiftUI/graph.png b/Namo_SwiftUI/graph.png index b5fa9cd9..3cdfbf16 100644 Binary files a/Namo_SwiftUI/graph.png and b/Namo_SwiftUI/graph.png differ