diff --git a/KuringApp/KuringApp.xcodeproj/project.pbxproj b/KuringApp/KuringApp.xcodeproj/project.pbxproj index cc297382..40ec3255 100644 --- a/KuringApp/KuringApp.xcodeproj/project.pbxproj +++ b/KuringApp/KuringApp.xcodeproj/project.pbxproj @@ -21,18 +21,23 @@ A9B4F0142ABCA86500354C00 /* NoticeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F0132ABCA86500354C00 /* NoticeApp.swift */; }; A9B4F0162ABCA93400354C00 /* NoticeApp.Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F0152ABCA93400354C00 /* NoticeApp.Path.swift */; }; A9B4F0182ABCA9AF00354C00 /* NoticeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F0172ABCA9AF00354C00 /* NoticeDetailView.swift */; }; - A9B4F01A2ABCAF9800354C00 /* NoticeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F0192ABCAF9800354C00 /* NoticeList.swift */; }; + A9B4F01A2ABCAF9800354C00 /* NoticeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F0192ABCAF9800354C00 /* NoticeContentView.swift */; }; A9B4F01D2ABCB4CE00354C00 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B4F01C2ABCB4CE00354C00 /* SearchView.swift */; }; A9DAFA542AB1F04B0064F748 /* KuringApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAFA532AB1F04B0064F748 /* KuringApp.swift */; }; A9DAFA562AB1F04B0064F748 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAFA552AB1F04B0064F748 /* ContentView.swift */; }; A9DAFA582AB1F04C0064F748 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9DAFA572AB1F04C0064F748 /* Assets.xcassets */; }; A9DAFA5B2AB1F04C0064F748 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9DAFA5A2AB1F04C0064F748 /* Preview Assets.xcassets */; }; B11DBDCC2ACB370500501CA8 /* StaffRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11DBDCB2ACB370500501CA8 /* StaffRow.swift */; }; + B1CBFA662AC7113C00C1E0ED /* NoticeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */; }; CA52DF392AD58F4B009B9272 /* NoticeAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52DF382AD58F4B009B9272 /* NoticeAppTests.swift */; }; CA640C192AD7FEFD002836E0 /* SubscriptionSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA640C182AD7FEFD002836E0 /* SubscriptionSegment.swift */; }; - CA640C1B2AD8064B002836E0 /* NoticeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA640C1A2AD8064B002836E0 /* NoticeProvider.swift */; }; CA640C1D2AD84E10002836E0 /* SubscriptionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA640C1C2AD84E10002836E0 /* SubscriptionViewTests.swift */; }; CA640C1F2AD8525D002836E0 /* SubscriptionAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA640C1E2AD8525D002836E0 /* SubscriptionAppTests.swift */; }; + CAD5A4272B10723500DED0D5 /* NoticeTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A4262B10723500DED0D5 /* NoticeTypePicker.swift */; }; + CAD5A4292B10724100DED0D5 /* NoticeTypeColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A4282B10724100DED0D5 /* NoticeTypeColumn.swift */; }; + CAD5A42B2B10750800DED0D5 /* DepartmentSelectorLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A42A2B10750800DED0D5 /* DepartmentSelectorLink.swift */; }; + CAD5A42D2B1077C200DED0D5 /* NoticeContentView.NoDepartment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A42C2B1077C200DED0D5 /* NoticeContentView.NoDepartment.swift */; }; + CAD5A42F2B11DBAF00DED0D5 /* NoticeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A42E2B11DBAF00DED0D5 /* NoticeList.swift */; }; DF331DAB2AC917E100D0BB08 /* kuring_app.png in Resources */ = {isa = PBXBuildFile; fileRef = DF331DA72AC917E000D0BB08 /* kuring_app.png */; }; DF331DAC2AC917E100D0BB08 /* kuring_app_classic.png in Resources */ = {isa = PBXBuildFile; fileRef = DF331DA82AC917E000D0BB08 /* kuring_app_classic.png */; }; DF331DAD2AC917E100D0BB08 /* kuring_app_sketch.png in Resources */ = {isa = PBXBuildFile; fileRef = DF331DA92AC917E000D0BB08 /* kuring_app_sketch.png */; }; @@ -63,7 +68,7 @@ A9B4F0132ABCA86500354C00 /* NoticeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeApp.swift; sourceTree = ""; }; A9B4F0152ABCA93400354C00 /* NoticeApp.Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeApp.Path.swift; sourceTree = ""; }; A9B4F0172ABCA9AF00354C00 /* NoticeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeDetailView.swift; sourceTree = ""; }; - A9B4F0192ABCAF9800354C00 /* NoticeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeList.swift; sourceTree = ""; }; + A9B4F0192ABCAF9800354C00 /* NoticeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeContentView.swift; sourceTree = ""; }; A9B4F01C2ABCB4CE00354C00 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; A9DAFA502AB1F04B0064F748 /* KuringApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KuringApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; A9DAFA532AB1F04B0064F748 /* KuringApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KuringApp.swift; sourceTree = ""; }; @@ -72,12 +77,17 @@ A9DAFA5A2AB1F04C0064F748 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; A9DAFA662AB1F09B0064F748 /* KuringModulePackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = KuringModulePackage; path = ../KuringModulePackage; sourceTree = ""; }; B11DBDCB2ACB370500501CA8 /* StaffRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaffRow.swift; sourceTree = ""; }; + B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRow.swift; sourceTree = ""; }; CA52DF2F2AD58DF2009B9272 /* KuringAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KuringAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CA52DF382AD58F4B009B9272 /* NoticeAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeAppTests.swift; sourceTree = ""; }; CA640C182AD7FEFD002836E0 /* SubscriptionSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSegment.swift; sourceTree = ""; }; - CA640C1A2AD8064B002836E0 /* NoticeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeProvider.swift; sourceTree = ""; }; CA640C1C2AD84E10002836E0 /* SubscriptionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionViewTests.swift; sourceTree = ""; }; CA640C1E2AD8525D002836E0 /* SubscriptionAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppTests.swift; sourceTree = ""; }; + CAD5A4262B10723500DED0D5 /* NoticeTypePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeTypePicker.swift; sourceTree = ""; }; + CAD5A4282B10724100DED0D5 /* NoticeTypeColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeTypeColumn.swift; sourceTree = ""; }; + CAD5A42A2B10750800DED0D5 /* DepartmentSelectorLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepartmentSelectorLink.swift; sourceTree = ""; }; + CAD5A42C2B1077C200DED0D5 /* NoticeContentView.NoDepartment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeContentView.NoDepartment.swift; sourceTree = ""; }; + CAD5A42E2B11DBAF00DED0D5 /* NoticeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeList.swift; sourceTree = ""; }; DF062D4A2AC87B6D00FC48C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; DF331DA72AC917E000D0BB08 /* kuring_app.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = kuring_app.png; sourceTree = ""; }; DF331DA82AC917E000D0BB08 /* kuring_app_classic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = kuring_app_classic.png; sourceTree = ""; }; @@ -135,7 +145,6 @@ A965B7A82AC074BD0026ECDC /* WillBeRemoved */ = { isa = PBXGroup; children = ( - CA640C1A2AD8064B002836E0 /* NoticeProvider.swift */, ); path = WillBeRemoved; sourceTree = ""; @@ -172,7 +181,13 @@ isa = PBXGroup; children = ( A9B4F0172ABCA9AF00354C00 /* NoticeDetailView.swift */, - A9B4F0192ABCAF9800354C00 /* NoticeList.swift */, + A9B4F0192ABCAF9800354C00 /* NoticeContentView.swift */, + CAD5A42E2B11DBAF00DED0D5 /* NoticeList.swift */, + CAD5A42C2B1077C200DED0D5 /* NoticeContentView.NoDepartment.swift */, + B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */, + CAD5A4262B10723500DED0D5 /* NoticeTypePicker.swift */, + CAD5A4282B10724100DED0D5 /* NoticeTypeColumn.swift */, + CAD5A42A2B10750800DED0D5 /* DepartmentSelectorLink.swift */, ); path = NoticeList; sourceTree = ""; @@ -375,15 +390,20 @@ A965B7AB2AC0750E0026ECDC /* SubscriptionView.swift in Sources */, A91455002ACF3A3D00B82A8E /* SetttingsApp.Path.swift in Sources */, A91455062ACF451E00B82A8E /* AppIcons.swift in Sources */, - A9B4F01A2ABCAF9800354C00 /* NoticeList.swift in Sources */, + A9B4F01A2ABCAF9800354C00 /* NoticeContentView.swift in Sources */, A9DAFA562AB1F04B0064F748 /* ContentView.swift in Sources */, - CA640C1B2AD8064B002836E0 /* NoticeProvider.swift in Sources */, + CAD5A42F2B11DBAF00DED0D5 /* NoticeList.swift in Sources */, A9B4F01D2ABCB4CE00354C00 /* SearchView.swift in Sources */, + CAD5A42D2B1077C200DED0D5 /* NoticeContentView.NoDepartment.swift in Sources */, + CAD5A4272B10723500DED0D5 /* NoticeTypePicker.swift in Sources */, A965B7A32AC013060026ECDC /* DepartmentEditor.swift in Sources */, A965B7A52AC013BF0026ECDC /* DepartmentSelector.swift in Sources */, B11DBDCC2ACB370500501CA8 /* StaffRow.swift in Sources */, A9B4F0162ABCA93400354C00 /* NoticeApp.Path.swift in Sources */, + CAD5A42B2B10750800DED0D5 /* DepartmentSelectorLink.swift in Sources */, A9B4F0182ABCA9AF00354C00 /* NoticeDetailView.swift in Sources */, + CAD5A4292B10724100DED0D5 /* NoticeTypeColumn.swift in Sources */, + B1CBFA662AC7113C00C1E0ED /* NoticeRow.swift in Sources */, A965B7AF2AC084D20026ECDC /* SubscriptionApp.swift in Sources */, A91455032ACF44CE00B82A8E /* KuringIcon.swift in Sources */, CA640C192AD7FEFD002836E0 /* SubscriptionSegment.swift in Sources */, diff --git a/KuringApp/KuringApp/ContentView.swift b/KuringApp/KuringApp/ContentView.swift index 477970ec..3e3bd737 100644 --- a/KuringApp/KuringApp/ContentView.swift +++ b/KuringApp/KuringApp/ContentView.swift @@ -14,7 +14,7 @@ struct ContentView: View { NoticeAppView( store: Store( initialState: NoticeAppFeature.State( - noticeList: NoticeListFeature.State(notices: [.random]) + noticeList: NoticeListFeature.State() ), reducer: { NoticeAppFeature() } ) diff --git a/KuringApp/KuringApp/Info.plist b/KuringApp/KuringApp/Info.plist index 949b68b4..f3cf4cf8 100644 --- a/KuringApp/KuringApp/Info.plist +++ b/KuringApp/KuringApp/Info.plist @@ -3,38 +3,43 @@ CFBundleIcons + + CFBundleAlternateIcons + + kuring_app + + CFBundleIconFiles + + kuring_app + + + kuring_app_classic + + CFBundleIconFiles + + kuring_app_classic + + + kuring_app_blueprint + + CFBundleIconFiles + + kuring_app_blueprint + + + kuring_app_sketch + + CFBundleIconFiles + + kuring_app_sketch + + + + + NSAppTransportSecurity - CFBundleAlternateIcons - - kuring_app - - CFBundleIconFiles - - kuring_app - - - kuring_app_classic - - CFBundleIconFiles - - kuring_app_classic - - - kuring_app_blueprint - - CFBundleIconFiles - - kuring_app_blueprint - - - kuring_app_sketch - - CFBundleIconFiles - - kuring_app_sketch - - - + NSAllowsArbitraryLoads + diff --git a/KuringApp/KuringApp/NoticeApp.swift b/KuringApp/KuringApp/NoticeApp.swift index ed5a2788..e9c55f87 100644 --- a/KuringApp/KuringApp/NoticeApp.swift +++ b/KuringApp/KuringApp/NoticeApp.swift @@ -7,17 +7,33 @@ import Model import SwiftUI +import NoticeListFeature import ComposableArchitecture struct NoticeAppFeature: Reducer { struct State: Equatable { - var path = StackState() + // MARK: 네비게이션 + + /// 루트 var noticeList = NoticeListFeature.State() + /// 스택 네비게이션 + var path = StackState() + /// 트리 네비게이션 - ``SubscriptionAppFeature`` + @PresentationState var changeSubscription: SubscriptionAppFeature.State? } enum Action { - case path(StackAction) + /// 루트(``NoticeListFeature``) 액션 case noticeList(NoticeListFeature.Action) + + /// 스택 네비게이션 액션 (``NoticeAppFeature/Path``) + case path(StackAction) + + /// 구독 변경 버튼을 탭한 경우 + case changeSubscriptionButtonTapped + + /// ``SubscriptionAppFeature`` 의 Presentation 액션 + case changeSubscription(PresentationAction) } var body: some ReducerOf { @@ -46,13 +62,25 @@ struct NoticeAppFeature: Reducer { return .none } - case .noticeList: + case .changeSubscription(.presented(.subscriptionView(.subscriptionResponse))): + /// ``SubscriptionAppFeature`` 액션 + state.changeSubscription = nil + return .none + + case .changeSubscriptionButtonTapped: + state.changeSubscription = SubscriptionAppFeature.State() + return .none + + case .noticeList, .changeSubscription: return .none } } .forEach(\.path, action: /Action.path) { Path() } + .ifLet(\.$changeSubscription, action: /Action.changeSubscription) { + SubscriptionAppFeature() + } } } @@ -61,12 +89,46 @@ struct NoticeAppView: View { var body: some View { NavigationStackStore(self.store.scope(state: \.path, action: { .path($0) })) { - NoticeList( - store: self.store.scope( - state: \.noticeList, - action: { .noticeList($0) } + WithViewStore(self.store, observe: { $0 }) { viewStore in + NoticeContentView( + store: self.store.scope( + state: \.noticeList, + action: { .noticeList($0) } + ) ) - ) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Image("appIconLabel", bundle: Bundle.noticeList) + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + // TODO: - to SearchView + } label: { + Image(systemName: "magnifyingglass") + .foregroundStyle(Color.black) + } + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + // MARK: 푸시 알림 선택 진입 + Button { + viewStore.send(.changeSubscriptionButtonTapped) + } label: { + Image(systemName: "bell") + .foregroundStyle(.black) + } + } + } + .sheet( + store: self.store.scope( + state: \.$changeSubscription, + action: { .changeSubscription($0) } + ) + ) { store in + SubscriptionApp(store: store) + } + } } destination: { state in switch state { case .detail: @@ -97,7 +159,6 @@ struct NoticeAppView: View { } } } - } } @@ -105,7 +166,7 @@ struct NoticeAppView: View { NoticeAppView( store: Store( initialState: NoticeAppFeature.State( - noticeList: NoticeListFeature.State(notices: [.random]) + noticeList: NoticeListFeature.State() ), reducer: { NoticeAppFeature() } ) diff --git a/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift b/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift new file mode 100644 index 00000000..979318d4 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift @@ -0,0 +1,47 @@ +// +// DepartmentSelectorLink.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI + +struct DepartmentSelectorLink: View { + let department: NoticeProvider + @Binding var isLoading: Bool + let action: () -> Void + + var body: some View { + HStack { + Text(department.korName) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(Color.black.opacity(0.8)) + + Spacer() + + if isLoading { + ProgressView() + } else { + Image(systemName: "chevron.right") + } + } + .contentShape(Rectangle()) + .padding(.horizontal, 20) + .padding(.vertical, 16) + .onTapGesture { + guard !isLoading else { return } + action() + } + } +} + +#Preview { + DepartmentSelectorLink( + department: .init(name: "산업디자인학과", hostPrefix: "kuid", korName: "산업디자인학과", category: .학과), + isLoading: .constant(false) + ) { + // 액션 정의. 예) `viewStore.send(.changeDepartmentButtonTapped)` + } +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeContentView.NoDepartment.swift b/KuringApp/KuringApp/NoticeList/NoticeContentView.NoDepartment.swift new file mode 100644 index 00000000..bb49da3d --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeContentView.NoDepartment.swift @@ -0,0 +1,73 @@ +// +// NoticeContentView.NoDepartment.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI +import ComposableArchitecture + +extension NoticeContentView { + @ViewBuilder + func NoDepartmentView() -> some View { + VStack(spacing: 0) { + Spacer() + + Text("아직 추가된 학과가 없어요.\n관심 학과를 추가하고 공지를 확인해 보세요!") + .font(.system(size: 15, weight: .medium)) + .multilineTextAlignment(.center) + .foregroundColor(.black.opacity(0.36)) + + Spacer() + + NavigationLink( + state: NoticeAppFeature.Path.State.departmentEditor( + // TODO: - Mock 데이터 추후 제거 + DepartmentEditorFeature.State( + myDepartments: IdentifiedArray(uniqueElements: NoticeProvider.departments), + results: [ + NoticeProvider( + name: "education", + hostPrefix: "edu", + korName: "교직과", + category: .학과 + ), + ] + ) + ) + ) { + OverlayButton("학과 추가하기") + .padding(.horizontal, 20) + } + } + } + + // TODO: 디자인 시스템 분리 + /// 상단에 블러가 존재하는 버튼 + @ViewBuilder + func OverlayButton(_ title: String) -> some View { + HStack(alignment: .center, spacing: 10) { + Spacer() + + Text(title) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(Color.white) + + Spacer() + } + .padding(.horizontal, 50) + .padding(.vertical, 16) + .frame(height: 50, alignment: .center) + .background(Color.accentColor) + .cornerRadius(100) + .background { + LinearGradient( + gradient: Gradient(colors: [.white.opacity(0.1), .white]), + startPoint: .top, endPoint: .bottom + ) + .offset(x: 0, y: -32) + } + } +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeContentView.swift b/KuringApp/KuringApp/NoticeList/NoticeContentView.swift new file mode 100644 index 00000000..17d69602 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeContentView.swift @@ -0,0 +1,226 @@ +// +// NoticeContentView.swift +// KuringApp +// +// Created by Jaesung Lee on 2023/09/22. +// + +import Model +import SwiftUI +import Network +import ComposableArchitecture + +struct NoticeListFeature: Reducer { + struct State: Equatable { + // MARK: 네비게이션 + @PresentationState var changeDepartment: DepartmentSelectorFeature.State? + + /// 현재 공지리스트를 제공하는 `NoticeProvider` 값 + /// + /// - IMPORTANT: 추가한 학과가 있으면 추가한 학과의 첫번째 값이 초기값으로 세팅되고 없으면 `.학사` + @BindingState var provider: NoticeProvider = NoticeProvider.departments.first ?? NoticeProvider.학사 + + /// 현재 공지를 가져오는 중인지 알려주는 Bool 값 + @BindingState var isLoading = false + + // TODO: 공지 데이터 저장 관련 로직은 전부 디펜던시로 옮기기 + /// 공지 + var noticeDictionary: [NoticeProvider: NoticeInfo] = [:] + + // TODO: 공지 데이터 저장 관련 로직은 전부 디펜던시로 옮기기 + struct NoticeInfo: Equatable { + /// 공지 + var notices: [Notice] = [] + /// 공지 페이지 + var page: Int = 0 + /// 공지를 더 가져올 수 있는지 여부 + var hasNextList: Bool = true + /// 한번 요청 시 가져올 수 있는 공지 사항 개수 최댓값 + let loadLimit = 20 + } + } + + enum Action: BindableAction { + case binding(BindingAction) + + /// `onAppear` 이 호출된 경우 + case onAppear + + /// 학과 변경하기 버튼을 탭한 경우 + case changeDepartmentButtonTapped + + /// ``DepartmentSelectorFeature`` 의 Presentation 액션 + case changeDepartment(PresentationAction) + + /// ``NoticeAppFeature`` 으로 액션을 전달하기 위한 델리게이트 + case delegate(Delegate) + + /// 공지를 가져오기 위한 네트워크 요청을 하는 경우 + case fetchNotices + + /// 네트워크 요청에 대한 응답을 받은 경우 + case noticesResponse(TaskResult<(NoticeProvider, [Notice])>) + + /// 북마크 버튼을 탭한 경우 + /// - Parameter notice: 북마크 액션 대상인 공지 + case bookmarkTapped(Notice) + + /// ``NoticeAppFeature`` 에 전달 될 액션 종류 + enum Delegate { + /// 학과 편집 버튼을 선택한 경우 + case editDepartment + } + } + + enum CancelID { + case fetchNotices + } + + @Dependency(\.kuringLink) var kuringLink + + var body: some ReducerOf { + BindingReducer() + + Reduce { state, action in + switch action { + case .binding(\.$provider): + return .send(.fetchNotices) + + case .onAppear: + return .send(.fetchNotices) + + case .changeDepartmentButtonTapped: + state.changeDepartment = DepartmentSelectorFeature.State( + currentDepartment: state.provider, + addedDepartment: IdentifiedArray(uniqueElements: NoticeProvider.departments) // TODO: Dependency + ) + return .none + + case let .changeDepartment(.presented(.delegate(delegate))): + switch delegate { + case .editDepartment: + state.changeDepartment = nil + return .send(.delegate(.editDepartment)) + } + + case .changeDepartment(.presented(.selectDepartment)): + /// ``DepartmentSelectorFeature`` 액션 + guard let selectedDepartment = state.changeDepartment?.currentDepartment else { + return .none + } + state.provider = selectedDepartment + return .none + + case .changeDepartment(.dismiss): + return .send(.fetchNotices) + + case .fetchNotices: + if state.provider == .emptyDepartment { + return .none + } + state.isLoading = true + + return .run { [provider = state.provider, noticeDictionary = state.noticeDictionary] send in + // TODO: 공지 데이터 저장 관련 로직은 전부 디펜던시로 옮기기 + let retrievalInfo = noticeDictionary[provider] ?? State.NoticeInfo() + + let department: String? = if provider.category == .학과 { + provider.hostPrefix + } else { + nil + } + await send( + .noticesResponse( + TaskResult { + let notices = try await kuringLink.fetchNotices( + retrievalInfo.loadLimit, + provider.category == .학과 ? "dep" : provider.hostPrefix, // TODO: korean name 도 쓸 거 고려해서 문자열 말고 좀 더 나은걸로 + department, + retrievalInfo.page + ) + return (provider, notices) + } + ) + ) + } + .cancellable(id: CancelID.fetchNotices, cancelInFlight: true) + + case let .noticesResponse(.success((noticeType, notices))): + if state.noticeDictionary[noticeType] == nil { + state.noticeDictionary[noticeType] = State.NoticeInfo() + } + guard var noticeInfo = state.noticeDictionary[noticeType] else { return .none } + + noticeInfo.hasNextList = notices.count >= noticeInfo.loadLimit + noticeInfo.page += 1 + noticeInfo.notices += notices + + state.noticeDictionary[noticeType] = noticeInfo + state.isLoading = false + return .none + + case let .noticesResponse(.failure(error)): + print(error.localizedDescription) + state.isLoading = false + return .none + + case let .bookmarkTapped(notice): + print("공지#\(notice.articleId)을 북마크 했습니다.") + return .none + + case .binding, .delegate, .changeDepartment: + return .none + } + } + .ifLet(\.$changeDepartment, action: /Action.changeDepartment) { + DepartmentSelectorFeature() + } + } + + func save() { } +} + +struct NoticeContentView: View { + let store: StoreOf + + /// - NOTE: NoticeList 만 제외하고 나머지는 NotiecApp 단으로 옮겨야 하는가? + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 0) { + NoticeCategoryPicker(selection: viewStore.$provider) + + if viewStore.provider == .emptyDepartment { + NoDepartmentView() + } else { + NoticeList(store: self.store) + } + } + .onAppear { + viewStore.send(.onAppear) + } + } + .sheet( + store: self.store.scope( + state: \.$changeDepartment, + action: { .changeDepartment($0) } + ) + ) { store in + NavigationStack { + DepartmentSelector(store: store) + .navigationTitle("Department Selector") + } + } + } +} + +#Preview { + NavigationStack { + NoticeContentView( + store: Store( + initialState: NoticeListFeature.State(), + reducer: { NoticeListFeature() } + ) + ) + } +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeList.swift b/KuringApp/KuringApp/NoticeList/NoticeList.swift index 3e8c0ddd..9828ceb1 100644 --- a/KuringApp/KuringApp/NoticeList/NoticeList.swift +++ b/KuringApp/KuringApp/NoticeList/NoticeList.swift @@ -1,167 +1,63 @@ // -// NoticeList.swift +// NoticeContentView.NoticeList.swift // KuringApp // -// Created by Jaesung Lee on 2023/09/22. +// Created by 이재성 on 11/25/23. // -import Model import SwiftUI import ComposableArchitecture -struct NoticeListFeature: Reducer { - struct State: Equatable { - var notices: IdentifiedArrayOf = [] - - var currentDepartment: NoticeProvider? - @PresentationState var changeDepartment: DepartmentSelectorFeature.State? - - // TODO: (고민포인트) AppFeature 단으로 (부모 리듀서) 로 옮길 필요는 없을까? - 도메인에 대한 고민 - @PresentationState var changeSubscription: SubscriptionAppFeature.State? - } - - enum Action { - case changeDepartmentButtonTapped - case changeSubscriptionButtonTapped - - case changeDepartment(PresentationAction) - case changeSubscription(PresentationAction) - - case delegate(Delegate) - - enum Delegate { - case editDepartment - } - } - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .changeDepartmentButtonTapped: - state.changeDepartment = DepartmentSelectorFeature.State( - currentDepartment: state.currentDepartment, - addedDepartment: IdentifiedArray(uniqueElements: NoticeProvider.departments) - ) - return .none - - case .changeSubscriptionButtonTapped: - state.changeSubscription = SubscriptionAppFeature.State() - return .none - - case let .changeDepartment(.presented(.delegate(delegate))): - switch delegate { - case .editDepartment: - state.changeDepartment = nil - return .send(.delegate(.editDepartment)) - } - - // TODO: Delegate - case .changeDepartment(.presented(.selectDepartment)): - guard let selectedDepartment = state.changeDepartment?.currentDepartment else { - return .none - } - state.currentDepartment = selectedDepartment - return .none - - case .changeSubscription(.presented(.subscriptionView(.subscriptionResponse))): - state.changeSubscription = nil - return .none - - case .changeDepartment: - return .none - - case .changeSubscription: - return .none - - case .delegate: - return .none - } - } - .ifLet(\.$changeDepartment, action: /Action.changeDepartment) { - DepartmentSelectorFeature() - } - .ifLet(\.$changeSubscription, action: /Action.changeSubscription) { - SubscriptionAppFeature() - } - } -} - struct NoticeList: View { let store: StoreOf var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - List { - Button((viewStore.currentDepartment ?? NoticeProvider.departments[0]).korName) { - viewStore.send(.changeDepartmentButtonTapped) - } - - ForEach(viewStore.state.notices) { notice in + let noticeType = viewStore.provider + let notices = viewStore.noticeDictionary[noticeType]?.notices + + Section { + List(notices ?? [], id: \.id) { notice in NavigationLink( state: NoticeAppFeature.Path.State.detail( NoticeDetailFeature.State(notice: notice) ) ) { - VStack(alignment: .leading) { - Text(notice.subject) - - Text(notice.postedDate) - } + NoticeRow(notice: notice) + .listRowInsets(EdgeInsets()) + .onAppear { + let type = viewStore.provider + let noticeInfo = viewStore.noticeDictionary[type] + + /// 마지막 공지가 보이면 update + if noticeInfo?.notices.last == notice { + viewStore.send(.fetchNotices) + } + } + .swipeActions(edge: .leading) { + Button { + viewStore.send(.bookmarkTapped(notice)) + } label: { + Image(systemName: "bookmark.slash") + // Image(systemName: isBookmark ? "bookmark.slash" : "bookmark") + } + .tint(Color.accentColor) + } } } - } - .navigationTitle("Notice List") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - NavigationLink( - state: NoticeAppFeature.Path.State.search( - SearchFeature.State() - ) + .listStyle(.plain) + } header: { + if viewStore.provider.category == .학과 { + DepartmentSelectorLink( + department: viewStore.provider, + isLoading: viewStore.$isLoading ) { - Image(systemName: "magnifyingglass") - } - } - - ToolbarItem(placement: .topBarTrailing) { - Button { - viewStore.send(.changeSubscriptionButtonTapped) - } label: { - Image(systemName: "bell") + viewStore.send(.changeDepartmentButtonTapped) } - } - } - .sheet( - store: self.store.scope( - state: \.$changeDepartment, - action: { .changeDepartment($0) } - ) - ) { store in - NavigationStack { - DepartmentSelector(store: store) - .navigationTitle("Department Selector") - } - } - .sheet( - store: self.store.scope( - state: \.$changeSubscription, - action: { .changeSubscription($0) } - ) - ) { store in - NavigationStack { - SubscriptionApp(store: store) + } else { + EmptyView() } } } } } - -#Preview { - NavigationStack { - NoticeList( - store: Store( - initialState: NoticeListFeature.State(notices: [.random]), - reducer: { NoticeListFeature() } - ) - ) - } -} diff --git a/KuringApp/KuringApp/NoticeList/NoticeRow.swift b/KuringApp/KuringApp/NoticeList/NoticeRow.swift new file mode 100644 index 00000000..4a2d7669 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeRow.swift @@ -0,0 +1,164 @@ +// +// NoticeRow.swift +// KuringApp +// +// Created by 🏝️ GeonWoo Lee on 9/29/23. +// + +import SwiftUI +import Model + +struct NoticeRow: View { + var rowType: NoticeRowType + let notice: Notice + + init(notice: Notice) { + self.notice = notice + + var bookmarkedNotices = [Notice]() + let isBookmark: Bool = bookmarkedNotices.contains(notice) + if notice.important { + if isBookmark { self.rowType = .importantAndBookmark } + else { self.rowType = .important } + } else { + if isBookmark { self.rowType = .bookmark } + else { self.rowType = .none } + } + } + + enum NoticeRowType { + /// 중요이면서 북마크 + case importantAndBookmark + /// 중요 + case important + /// 북마크 + case bookmark + /// 기본 + case none + } + + var body: some View { + ZStack { + switch rowType { + case .important, .importantAndBookmark: + Color(red: 0.24, green: 0.74, blue: 0.5, opacity: 0.1) + default: + Color.clear + } + + switch rowType { + case .importantAndBookmark: + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .top) { + VStack { + importantTagView + .padding(.top, 12) + } + Spacer() + bookmarkView + } + titleView + dateView + } + .padding(.horizontal, 20) + .padding(.bottom, 12) + case .important: + VStack(alignment: .leading, spacing: 4) { + importantTagView + titleView + dateView + } + .padding(.horizontal, 20) + .padding(.vertical, 12) + case .bookmark: + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .top, spacing: 0) { + VStack { + titleView + .padding(.top, 12) + } + Spacer() + bookmarkView + } + dateView + } + .padding(.horizontal, 20) + .padding(.bottom, 12) + case .none: + HStack { + VStack(alignment: .leading, spacing: 4) { + titleView + dateView + } + Spacer() + } + .padding(.horizontal, 20) + .padding(.vertical, 12) + } + } + } + + @ViewBuilder + private var importantTagView: some View { + Text("중요") + .font(.system(size: 12, weight: .semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundStyle(Color(red: 0.24, green: 0.74, blue: 0.5)) + .background(Color.white) + .cornerRadius(16) + .overlay( + RoundedRectangle(cornerRadius: 16) + .inset(by: 0.25) + .stroke(Color(red: 0.24, green: 0.74, blue: 0.5), lineWidth: 0.5) + ) + } + + @ViewBuilder + private var titleView: some View { + Text(notice.subject) + .font(.system(size: 15, weight: .medium)) + .foregroundColor(Color(red: 0.21, green: 0.24, blue: 0.29)) + } + + @ViewBuilder + private var dateView: some View { + // TODO: - 정보 재구성 + Text(notice.postedDate) + .font(.system(size: 14)) + .foregroundStyle(Color(red: 0.21, green: 0.24, blue: 0.29).opacity(0.6)) + } + + @ViewBuilder + private var bookmarkView: some View { + // TODO: 디자인 시스템 분리 - 북마크 + ZStack { + RoundedRectangle(cornerRadius: 2) + .compositingGroup() + .foregroundStyle(Color(red: 0.24, green: 0.74, blue: 0.5)) + .frame(width: 16, height: 21) + + RoundedRectangle(cornerRadius: 2) + .rotation(.degrees(45)) + .frame(width: 16, height: 16) + .offset(x: 0, y: 14.5) + .foregroundStyle(Color.red) + .blendMode(.destinationOut) + } + .compositingGroup() + } +} + +#Preview { + List { + NoticeRow(notice: .random) + .listRowInsets(EdgeInsets()) + NoticeRow(notice: .random) + .listRowInsets(EdgeInsets()) + NoticeRow(notice: .random) + .listRowInsets(EdgeInsets()) + NoticeRow(notice: .random) + .listRowInsets(EdgeInsets()) + } + .listStyle(.plain) +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeTypeColumn.swift b/KuringApp/KuringApp/NoticeList/NoticeTypeColumn.swift new file mode 100644 index 00000000..3ff988b1 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeTypeColumn.swift @@ -0,0 +1,40 @@ +// +// NoticeTypeColumn.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI +import ComposableArchitecture + +struct NoticeTypeColumn: View { + let noticeType: NoticeType + let selectedID: NoticeType.ID + + var body: some View { + let itemSize: CGSize = CGSize(width: 64, height: 48) + let lineHeight: CGFloat = 3 + + Text(noticeType.rawValue) + .font(.system(size: 16, weight: noticeType.id == selectedID ? .semibold : .regular)) + .padding(.vertical, 8) + .frame(width: itemSize.width, height: itemSize.height) + .overlay { + VStack { + Spacer() + + RoundedRectangle(cornerRadius: lineHeight / 2) + .frame(width: itemSize.width, height: lineHeight) + .opacity(noticeType.id == selectedID ? 1 : 0) + } + } + .foregroundStyle( + noticeType.id == selectedID + ? Color.accentColor + : Color.black.opacity(0.3) + ) + .id(noticeType.id) + } +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeTypePicker.swift b/KuringApp/KuringApp/NoticeList/NoticeTypePicker.swift new file mode 100644 index 00000000..385cef63 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeTypePicker.swift @@ -0,0 +1,41 @@ +// +// NoticeTypePicker.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI +import ComposableArchitecture + +struct NoticeCategoryPicker: View { + @Binding var selection: NoticeProvider + + var body: some View { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 0) { + ForEach(NoticeType.allCases, id: \.self) { category in + Button { + selection = category.provider + } label: { + NoticeTypeColumn( + noticeType: category, + selectedID: selection.category.id + ) + } + .buttonStyle(.plain) + } + } + .padding(.leading, 10) + .onChange(of: selection) { value in + withAnimation { + proxy.scrollTo(value.category.id, anchor: .center) + } + } + } + } + .frame(height: 48) + } +} diff --git a/KuringApp/KuringApp/WillBeRemoved/Department.swift b/KuringApp/KuringApp/WillBeRemoved/Department.swift new file mode 100644 index 00000000..37d2f414 --- /dev/null +++ b/KuringApp/KuringApp/WillBeRemoved/Department.swift @@ -0,0 +1,55 @@ +// +// Department.swift +// KuringApp +// +// Created by Jaesung Lee on 2023/09/24. +// + +import Foundation + + +import Foundation + +public struct Department: Codable, Identifiable, Hashable, Equatable { + public var id: String { self.name } + + public var name: String // 학과이름 + public var hostPrefix: String + public var korName: String + + public var isSubscribed: Bool = false + + enum CodingKeys: CodingKey { + case name + case hostPrefix + case korName + } + + public init( + name: String, + hostPrefix: String, + korName: String, + isSubscribed: Bool = false + ) { + self.name = name + self.hostPrefix = hostPrefix + self.korName = korName + self.isSubscribed = isSubscribed + } + + public static func == (_ lhs: Department,_ rhs: Department) -> Bool { + return lhs.name == rhs.name + && lhs.hostPrefix == rhs.hostPrefix + && lhs.korName == rhs.korName + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +extension Department { + static let 전기전자공학부 = Department(name: "전기전자공학부", hostPrefix: "bch", korName: "전전", isSubscribed: false) + static let 컴퓨터공학부 = Department(name: "컴퓨터공학부", hostPrefix: "kor", korName: "컴공", isSubscribed: false) + static let 산업디자인학과 = Department(name: "산업디자인학과", hostPrefix: "eng", korName: "산디", isSubscribed: false) +} diff --git a/KuringApp/KuringApp/WillBeRemoved/NoticeProvider.swift b/KuringApp/KuringApp/WillBeRemoved/NoticeProvider.swift deleted file mode 100644 index 36ce7585..00000000 --- a/KuringApp/KuringApp/WillBeRemoved/NoticeProvider.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// NoticeProvider.swift -// KuringApp -// -// Created by 이재성 on 10/12/23. -// - -import Foundation - -struct NoticeProvider: Identifiable, Equatable { - var id: String { self.hostPrefix } - - let name: String - let hostPrefix: String - let korName: String -} - -extension NoticeProvider { - static let univNoticeTypes: [NoticeProvider] = [ - NoticeProvider( - name: "bachelor", - hostPrefix: "bch", - korName: "학사" - ), - NoticeProvider( - name: "employment", - hostPrefix: "emp", - korName: "취창업" - ), - NoticeProvider( - name: "library", - hostPrefix: "lib", - korName: "도서관" - ), - NoticeProvider( - name: "student", - hostPrefix: "stu", - korName: "학생" - ), - NoticeProvider( - name: "national", - hostPrefix: "nat", - korName: "국제" - ), - NoticeProvider( - name: "scholarship", - hostPrefix: "sch", - korName: "장학" - ), - NoticeProvider( - name: "industry_university", - hostPrefix: "ind", - korName: "산학" - ), - NoticeProvider( - name: "normal", - hostPrefix: "nor", - korName: "일반" - ), - ] -} - -/// Mock 데이터 -extension NoticeProvider { - static let departments: [NoticeProvider] = [ - NoticeProvider( - name: "education", - hostPrefix: "edu", - korName: "교직과" - ), - NoticeProvider( - name: "physical_education", - hostPrefix: "kupe", - korName: "체육교육과" - ), - NoticeProvider( - name: "computer_science", - hostPrefix: "cse", - korName: "컴퓨터공학부" - ), - ] -} diff --git a/KuringModulePackage/Package.swift b/KuringModulePackage/Package.swift index e68946ca..7f5428c6 100644 --- a/KuringModulePackage/Package.swift +++ b/KuringModulePackage/Package.swift @@ -65,7 +65,8 @@ let package = Package( "Model", "KuringDependencies" ], - path: "Sources/Feature/NoticeList" + path: "Sources/Feature/NoticeList", + resources: [.process("Resources")] ), .testTarget( name: "NoticeListFeatureTests", diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Bundle.swift b/KuringModulePackage/Sources/Feature/NoticeList/Bundle.swift new file mode 100644 index 00000000..a9a0f115 --- /dev/null +++ b/KuringModulePackage/Sources/Feature/NoticeList/Bundle.swift @@ -0,0 +1,12 @@ +// +// Bundle.swift +// +// +// Created by 🏝️ GeonWoo Lee on 10/1/23. +// + +import Foundation + +public extension Bundle { + static var noticeList: Bundle { .module } +} diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/Contents.json b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/Contents.json b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/Contents.json new file mode 100644 index 00000000..14f777df --- /dev/null +++ b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "appIconLabel.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "appIconLabel@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "appIconLabel@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel.png b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel.png new file mode 100644 index 00000000..b138c8b6 Binary files /dev/null and b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel.png differ diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@2x.png b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@2x.png new file mode 100644 index 00000000..8e73f1ee Binary files /dev/null and b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@2x.png differ diff --git a/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@3x.png b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@3x.png new file mode 100644 index 00000000..ec53e6b8 Binary files /dev/null and b/KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@3x.png differ diff --git a/KuringModulePackage/Sources/Shared/Model/NoticeType.swift b/KuringModulePackage/Sources/Shared/Model/NoticeType.swift new file mode 100644 index 00000000..24a0ff56 --- /dev/null +++ b/KuringModulePackage/Sources/Shared/Model/NoticeType.swift @@ -0,0 +1,162 @@ +// +// NoticeType.swift +// +// +// Created by 🏝️ GeonWoo Lee on 9/30/23. +// + +import Foundation + +public enum NoticeType: String, Hashable, CaseIterable, Identifiable { + case 학과, 학사, 장학, 도서관, 취창업, 국제, 학생, 산학, 일반 + + public var id: Self { self } + + public var provider: NoticeProvider { + switch self { + case .학과: + return NoticeProvider.departments.first ?? NoticeProvider.emptyDepartment + case .학사: + return .학사 + case .장학: + return .장학 + case .도서관: + return .도서관 + case .취창업: + return .취창업 + case .국제: + return .국제 + case .학생: + return .학생 + case .산학: + return .산학 + case .일반: + return .일반 + } + } + +// public static func from(_ string: String) -> NoticeType { +// switch string { +// case "department": return .학과 +// case "bachelor": return .학사 +// case "scholarship": return .장학 +// case "employment": return .취창업 +// case "national": return .국제 +// case "student": return .학생 +// case "industry_university": return .산학 +// case "normal": return .일반 +// case "library": return .도서관 +// default: return .일반 +// } +// } +} + +public struct NoticeProvider: Identifiable, Equatable, Hashable { + public var id: String { self.hostPrefix } + + public let name: String + public let hostPrefix: String + public let korName: String + public var category: NoticeType + + public init(name: String, hostPrefix: String, korName: String, category: NoticeType) { + self.name = name + self.hostPrefix = hostPrefix + self.korName = korName + self.category = category + } +} + +extension NoticeProvider { + public static let emptyDepartment = NoticeProvider( + name: "", + hostPrefix: "", + korName: "", + category: .학과 + ) + + public static let 학사 = NoticeProvider( + name: "bachelor", + hostPrefix: "bch", + korName: "학사", + category: .학사 + ) + + public static let 취창업 = NoticeProvider( + name: "employment", + hostPrefix: "emp", + korName: "취창업", + category: .취창업 + ) + + public static let 도서관 = NoticeProvider( + name: "library", + hostPrefix: "lib", + korName: "도서관", + category: .도서관 + ) + + public static let 학생 = NoticeProvider( + name: "student", + hostPrefix: "stu", + korName: "학생", + category: .학생 + ) + + public static let 국제 = NoticeProvider( + name: "national", + hostPrefix: "nat", + korName: "국제", + category: .국제 + ) + + public static let 장학 = NoticeProvider( + name: "scholarship", + hostPrefix: "sch", + korName: "장학", + category: .장학 + ) + + public static let 산학 = NoticeProvider( + name: "industry_university", + hostPrefix: "ind", + korName: "산학", + category: .산학 + ) + + public static let 일반 = NoticeProvider( + name: "normal", + hostPrefix: "nor", + korName: "일반", + category: .일반 + ) + + public static let univNoticeTypes: [NoticeProvider] = [ + .학사, .취창업, .도서관, .학생, .국제, .장학, .산학, .일반 + ] +} + +/// Mock 데이터 +extension NoticeProvider { + public static let departments: [NoticeProvider] = [ + NoticeProvider( + name: "education", + hostPrefix: "edu", + korName: "교직과", + category: .학과 + ), + NoticeProvider( + name: "physical_education", + hostPrefix: "kupe", + korName: "체육교육과", + category: .학과 + ), + NoticeProvider( + name: "computer_science", + hostPrefix: "cse", + korName: "컴퓨터공학부", + category: .학과 + ), + ] +} +