From 9d67d64b90ca21ffe08b2b1b9b13e385434ab34a Mon Sep 17 00:00:00 2001 From: lgvv Date: Sat, 30 Sep 2023 19:07:58 +0900 Subject: [PATCH] =?UTF-8?q?[=EC=B6=94=EA=B0=80]=20NoticeList=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KuringApp/KuringApp.xcodeproj/project.pbxproj | 24 ++ .../xcshareddata/swiftpm/Package.resolved | 6 +- KuringApp/KuringApp/ContentView.swift | 2 +- .../Department/DepartmentEditor.swift | 125 ++++++--- .../Department/DepartmentSelector.swift | 76 +++++- KuringApp/KuringApp/Info.plist | 11 + KuringApp/KuringApp/NoticeApp.swift | 46 +++- .../NoticeList/DepartmentSelectorLink.swift | 36 +++ .../NoticeList/NoticeList.NoDepartment.swift | 64 +++++ .../KuringApp/NoticeList/NoticeList.swift | 251 ++++++++++++++---- .../KuringApp/NoticeList/NoticeRow.swift | 164 ++++++++++++ .../NoticeList/NoticeTypeColumn.swift | 40 +++ .../KuringApp/NoticeList/NoticeTypeGrid.swift | 38 +++ .../KuringApp/WillBeRemoved/Department.swift | 47 +++- KuringModulePackage/Package.swift | 3 +- .../Sources/Feature/NoticeList/Bundle.swift | 12 + .../Resources/Symbols.xcassets/Contents.json | 6 + .../appIconLabel.imageset/Contents.json | 23 ++ .../appIconLabel.imageset/appIconLabel.png | Bin 0 -> 917 bytes .../appIconLabel.imageset/appIconLabel@2x.png | Bin 0 -> 1425 bytes .../appIconLabel.imageset/appIconLabel@3x.png | Bin 0 -> 2060 bytes .../Sources/Shared/Model/NoticeType.swift | 77 ++++++ 22 files changed, 918 insertions(+), 133 deletions(-) create mode 100644 KuringApp/KuringApp/Info.plist create mode 100644 KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift create mode 100644 KuringApp/KuringApp/NoticeList/NoticeList.NoDepartment.swift create mode 100644 KuringApp/KuringApp/NoticeList/NoticeRow.swift create mode 100644 KuringApp/KuringApp/NoticeList/NoticeTypeColumn.swift create mode 100644 KuringApp/KuringApp/NoticeList/NoticeTypeGrid.swift create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Bundle.swift create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/Contents.json create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/Contents.json create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel.png create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@2x.png create mode 100644 KuringModulePackage/Sources/Feature/NoticeList/Resources/Symbols.xcassets/appIconLabel.imageset/appIconLabel@3x.png create mode 100644 KuringModulePackage/Sources/Shared/Model/NoticeType.swift diff --git a/KuringApp/KuringApp.xcodeproj/project.pbxproj b/KuringApp/KuringApp.xcodeproj/project.pbxproj index cdf263ee..118725c1 100644 --- a/KuringApp/KuringApp.xcodeproj/project.pbxproj +++ b/KuringApp/KuringApp.xcodeproj/project.pbxproj @@ -24,6 +24,11 @@ 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 */; }; + B1CBFA662AC7113C00C1E0ED /* NoticeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */; }; + CAD5A4272B10723500DED0D5 /* NoticeTypeGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A4262B10723500DED0D5 /* NoticeTypeGrid.swift */; }; + CAD5A4292B10724100DED0D5 /* NoticeTypeColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A4282B10724100DED0D5 /* NoticeTypeColumn.swift */; }; + CAD5A42B2B10750800DED0D5 /* DepartmentSelectorLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A42A2B10750800DED0D5 /* DepartmentSelectorLink.swift */; }; + CAD5A42D2B1077C200DED0D5 /* NoticeList.NoDepartment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD5A42C2B1077C200DED0D5 /* NoticeList.NoDepartment.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -43,6 +48,12 @@ A9DAFA572AB1F04C0064F748 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 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 = ""; }; + B1129A702AF7D11400FB2ED2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRow.swift; sourceTree = ""; }; + CAD5A4262B10723500DED0D5 /* NoticeTypeGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeTypeGrid.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 /* NoticeList.NoDepartment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeList.NoDepartment.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,6 +116,11 @@ children = ( A9B4F0172ABCA9AF00354C00 /* NoticeDetailView.swift */, A9B4F0192ABCAF9800354C00 /* NoticeList.swift */, + CAD5A42C2B1077C200DED0D5 /* NoticeList.NoDepartment.swift */, + B1CBFA652AC7113C00C1E0ED /* NoticeRow.swift */, + CAD5A4262B10723500DED0D5 /* NoticeTypeGrid.swift */, + CAD5A4282B10724100DED0D5 /* NoticeTypeColumn.swift */, + CAD5A42A2B10750800DED0D5 /* DepartmentSelectorLink.swift */, ); path = NoticeList; sourceTree = ""; @@ -130,6 +146,7 @@ A9DAFA522AB1F04B0064F748 /* KuringApp */ = { isa = PBXGroup; children = ( + B1129A702AF7D11400FB2ED2 /* Info.plist */, A9DAFA532AB1F04B0064F748 /* KuringApp.swift */, A9B4F0132ABCA86500354C00 /* NoticeApp.swift */, A9B4F0152ABCA93400354C00 /* NoticeApp.Path.swift */, @@ -235,11 +252,16 @@ A9B4F01A2ABCAF9800354C00 /* NoticeList.swift in Sources */, A9DAFA562AB1F04B0064F748 /* ContentView.swift in Sources */, A9B4F01D2ABCB4CE00354C00 /* SearchView.swift in Sources */, + CAD5A42D2B1077C200DED0D5 /* NoticeList.NoDepartment.swift in Sources */, + CAD5A4272B10723500DED0D5 /* NoticeTypeGrid.swift in Sources */, A965B7A32AC013060026ECDC /* DepartmentEditor.swift in Sources */, A965B7A52AC013BF0026ECDC /* DepartmentSelector.swift in Sources */, A9B4F0162ABCA93400354C00 /* NoticeApp.Path.swift in Sources */, A965B7A72AC0696B0026ECDC /* Department.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 */, A9DAFA542AB1F04B0064F748 /* KuringApp.swift in Sources */, ); @@ -378,6 +400,7 @@ DEVELOPMENT_TEAM = 77CD4KLN3Y; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = KuringApp/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -407,6 +430,7 @@ DEVELOPMENT_TEAM = 77CD4KLN3Y; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = KuringApp/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/KuringApp/KuringApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/KuringApp/KuringApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 615a8d47..a0974a23 100644 --- a/KuringApp/KuringApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/KuringApp/KuringApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,7 +42,7 @@ "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { "branch" : "main", - "revision" : "667d92fad15e60b32a4c3e1289c9a15814798665" + "revision" : "6226901dec30b679841424d5069c11fdc2e3583a" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", - "version" : "1.0.0" + "revision" : "3efbfba0e4e56c7187cc19137ee16b7c95346b79", + "version" : "1.1.0" } }, { diff --git a/KuringApp/KuringApp/ContentView.swift b/KuringApp/KuringApp/ContentView.swift index 3e5b0703..a59c2024 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/Department/DepartmentEditor.swift b/KuringApp/KuringApp/Department/DepartmentEditor.swift index c3db8d2d..b39a6023 100644 --- a/KuringApp/KuringApp/Department/DepartmentEditor.swift +++ b/KuringApp/KuringApp/Department/DepartmentEditor.swift @@ -24,7 +24,7 @@ struct DepartmentEditorFeature: Reducer { @PresentationState var alert: AlertState? } - enum Action: BindableAction { + enum Action: BindableAction, Equatable { case binding(BindingAction) /// 학과 추가 버튼 눌렀을 때 @@ -35,6 +35,8 @@ struct DepartmentEditorFeature: Reducer { case deleteMyDepartmentButtonTapped(id: Department.ID) /// 내 학과 전체삭제 버튼 눌렀을 때 case deleteAllMyDepartmentButtonTapped + /// 텍스트 필드의 xmark를 눌렀을 때 + case clearTextFieldButtonTapped /// 알림 enum Alert: Equatable { @@ -96,6 +98,9 @@ struct DepartmentEditorFeature: Reducer { } } return .none + case .clearTextFieldButtonTapped: + state.searchText = "" + return .none // MARK: Alert case let .alert(.presented(alertAction)): @@ -124,51 +129,76 @@ struct DepartmentEditor: View { var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in VStack { - List { - Text("학과를 추가하거나 삭제할 수 있어요") - - /** - - `viewStore.$searchText` - - `bind(viewStore.$focus, to: $focus)` - */ - Section { - TextField("추가할 학과를 검색해 주세요", text: viewStore.$searchText) - .focused($focus, equals: .search) - .bind(viewStore.$focus, to: self.$focus) + HStack { + Text("학과를 추가하거나 \n삭제할 수 있어요") + .font(.system(size: 24, weight: .bold)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.15)) + Spacer() + } + .padding(.top, 28) + .padding(.bottom, 24) + + HStack(alignment: .center, spacing: 12) { + if viewStore.searchText.isEmpty { + Image(systemName: "magnifyingglass") + .frame(width: 16, height: 16) + .foregroundStyle(Color(red: 0.21, green: 0.24, blue: 0.29).opacity(0.6)) } - /** - - `viewStore.myDepartments` - - `.deleteMyDepartmentButtonTapped` - */ - Section { + TextField("추가할 학과를 검색해 주세요", text: viewStore.$searchText) + .focused($focus, equals: .search) + .bind(viewStore.$focus, to: self.$focus) + + if !viewStore.searchText.isEmpty { + Image(systemName: "xmark") + .frame(width: 16, height: 16) + .foregroundStyle(Color(red: 0.21, green: 0.24, blue: 0.29).opacity(0.6)) + .onTapGesture { + viewStore.send(.clearTextFieldButtonTapped) + focus = nil + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 7) + .background(Color(red: 0.95, green: 0.95, blue: 0.96)) + .cornerRadius(20) + .padding(.bottom, 16) + + HStack(alignment: .center, spacing: 10) { + Text(viewStore.searchText.isEmpty ? "내 학과" : "검색 결과") + .font(.system(size: 14)) + .foregroundStyle(Color(red: 0.21, green: 0.24, blue: 0.29).opacity(0.6)) + Spacer() + } + .padding(.horizontal, 4) + .padding(.vertical, 10) + + if viewStore.searchText.isEmpty { + // 내학과 + ScrollView { ForEach(viewStore.myDepartments) { myDepartment in - HStack { + HStack(alignment: .center) { Text(myDepartment.korName) - Spacer() - - Button("삭제") { + Button { viewStore.send(.deleteMyDepartmentButtonTapped(id: myDepartment.id)) + } label: { + Text("삭제") + .foregroundStyle(Color(red: 0.21, green: 0.24, blue: 0.29).opacity(0.6)) } } + .padding(.horizontal, 4) + .padding(.vertical, 10) } - } header: { - Text("내 학과") } - - /** - - `viewStore.results` - - `addDepartmentButtonTapped` - - `cancelAdditionButtonTapped` - */ - Section { + } else { + // 검색결과 + ScrollView { ForEach(viewStore.results) { result in - HStack { + HStack(alignment: .center) { Text(result.korName) - Spacer() - Button { if viewStore.myDepartments.contains(result) { viewStore.send(.cancelAdditionButtonTapped(id: result.id)) @@ -176,27 +206,45 @@ struct DepartmentEditor: View { viewStore.send(.addDepartmentButtonTapped(id: result.id)) } } label: { + let isSelected = viewStore.myDepartments.contains(result) Image( - systemName: viewStore.myDepartments.contains(result) + systemName: isSelected ? "checkmark.circle.fill" : "plus.circle" ) + .foregroundStyle( + isSelected + ? Color(red: 0.24, green: 0.74, blue: 0.5) + : Color.black.opacity(0.1) + ) } } + .padding(.horizontal, 4) + .padding(.vertical, 10) } - } header: { - Text("검색 결과") } } + + Spacer() } + .padding(.horizontal, 20) .toolbar { ToolbarItem(placement: .topBarTrailing) { - Button("전체 삭제") { + Button { viewStore.send(.deleteAllMyDepartmentButtonTapped) + focus = .search + } label: { + Text("전체 삭제") + .foregroundStyle( + viewStore.myDepartments.isEmpty + ? Color(red: 0.21, green: 0.21, blue: 0.21).opacity(0.5) + : Color(red: 0.24, green: 0.74, blue: 0.5) + ) } .disabled(viewStore.myDepartments.isEmpty) } } + .bind(viewStore.$focus, to: self.$focus) .alert( store: self.store.scope( state: \.$alert, @@ -204,10 +252,10 @@ struct DepartmentEditor: View { ) ) } + } } - #Preview { NavigationStack { DepartmentEditor( @@ -220,5 +268,6 @@ struct DepartmentEditor: View { ) ) .navigationTitle("Department Editor") + .toolbarTitleDisplayMode(.inline) } } diff --git a/KuringApp/KuringApp/Department/DepartmentSelector.swift b/KuringApp/KuringApp/Department/DepartmentSelector.swift index 6634ef10..01318f31 100644 --- a/KuringApp/KuringApp/Department/DepartmentSelector.swift +++ b/KuringApp/KuringApp/Department/DepartmentSelector.swift @@ -15,7 +15,7 @@ struct DepartmentSelectorFeature: Reducer { var addedDepartment: IdentifiedArrayOf } - enum Action { + enum Action: Equatable { // TODO: String -> Department case selectDepartment(id: Department.ID) case editDepartmentsButtonTapped @@ -36,7 +36,7 @@ struct DepartmentSelectorFeature: Reducer { } state.currentDepartment = department return .none - + case .editDepartmentsButtonTapped: return .send(.delegate(.editDepartment)) @@ -52,28 +52,77 @@ struct DepartmentSelector: View { var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - List { - Section { + VStack { + HStack(alignment: .center, spacing: 10) { + Text("대표 학과 선택") + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(Color.black) + } + .padding(.horizontal, 20) + .padding(.top, 24) + .padding(.bottom, 12) + .frame(width: 375, alignment: .leading) + + ScrollView { ForEach(viewStore.addedDepartment) { department in - Button { - viewStore.send(.selectDepartment(id: department.id)) - } label: { - Label( - department.korName, - systemImage: department == viewStore.currentDepartment + HStack { + Text(department.id) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.black) + + Spacer() + + Image( + systemName: department == viewStore.currentDepartment ? "checkmark.circle.fill" : "circle" ) + .foregroundStyle( + department == viewStore.currentDepartment + ? Color(red: 0.24, green: 0.74, blue: 0.5) + : Color.black.opacity(0.1) + ) + .frame(width: 20, height: 20) + } + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background(.white) + .onTapGesture { + viewStore.send(.selectDepartment(id: department.id)) } } } - Button("내 학과 편집하기") { - viewStore.send(.editDepartmentsButtonTapped) - } + topBlurButton( + "내 학과 편집하기", + fontColor:Color(red: 0.24, green: 0.74, blue: 0.5), + backgroundColor: Color(red: 0.24, green: 0.74, blue: 0.5).opacity(0.15) + ) + .padding(.horizontal, 20) } } } + + // TODO: 디자인 시스템 분리 - 상단에 블러가 존재하는 버튼 + @ViewBuilder + private func topBlurButton(_ title: String, fontColor: Color, backgroundColor: Color) -> some View { + HStack(alignment: .center, spacing: 10) { + Spacer() + Text(title) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(fontColor) + Spacer() + } + .padding(.horizontal, 50) + .padding(.vertical, 16) + .frame(height: 50, alignment: .center) + .background(backgroundColor) + .cornerRadius(100) + .background { + LinearGradient(gradient: Gradient(colors: [.white.opacity(0.1), .white]), startPoint: .top, endPoint: .bottom) + .offset(x: 0, y: -32) + } + } } #Preview { @@ -84,6 +133,5 @@ struct DepartmentSelector: View { reducer: { DepartmentSelectorFeature() } ) ) - .navigationTitle("Department Selector") } } diff --git a/KuringApp/KuringApp/Info.plist b/KuringApp/KuringApp/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/KuringApp/KuringApp/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/KuringApp/KuringApp/NoticeApp.swift b/KuringApp/KuringApp/NoticeApp.swift index 0895a5c8..6dad5d03 100644 --- a/KuringApp/KuringApp/NoticeApp.swift +++ b/KuringApp/KuringApp/NoticeApp.swift @@ -7,6 +7,7 @@ import Model import SwiftUI +import NoticeListFeature import ComposableArchitecture struct NoticeAppFeature: Reducer { @@ -67,6 +68,21 @@ struct NoticeAppView: View { 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) + } + } + } + } destination: { state in switch state { case .detail: @@ -94,20 +110,26 @@ struct NoticeAppView: View { ) { store in DepartmentEditor(store: store) .navigationTitle("Department Editor") +// .navigationBarBackButtonHidden(true) +// .toolbar { +// ToolbarItemGroup(placement: .navigationBarLeading) { +// Image(systemName: "chevron.left") +// } +// } } + } } - } } - -#Preview { - NoticeAppView( - store: Store( - initialState: NoticeAppFeature.State( - noticeList: NoticeListFeature.State(notices: [.random]) - ), - reducer: { NoticeAppFeature() } - ) - ) -} +// +//#Preview { +// NoticeAppView( +// store: Store( +// initialState: NoticeAppFeature.State( +// noticeList: NoticeListFeature.State(notices: [.국제: .init(arrayLiteral: .random)]) +// ), +// reducer: { NoticeAppFeature() } +// ) +// ) +//} diff --git a/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift b/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift new file mode 100644 index 00000000..dce72ba3 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/DepartmentSelectorLink.swift @@ -0,0 +1,36 @@ +// +// DepartmentSelectorLink.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI + +struct DepartmentSelectorLink: View { + let department: Department + let action: () -> Void + + var body: some View { + HStack { + Text(department.korName) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(Color.black.opacity(0.8)) + + Spacer() + + Image(systemName: "chevron.right") + } + .contentShape(Rectangle()) + .padding(.horizontal, 20) + .padding(.vertical, 16) + .onTapGesture(perform: action) + } +} + +#Preview { + DepartmentSelectorLink(department: .산업디자인학과) { + // 액션 정의. 예) `viewStore.send(.changeDepartmentButtonTapped)` + } +} diff --git a/KuringApp/KuringApp/NoticeList/NoticeList.NoDepartment.swift b/KuringApp/KuringApp/NoticeList/NoticeList.NoDepartment.swift new file mode 100644 index 00000000..09dce24f --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeList.NoDepartment.swift @@ -0,0 +1,64 @@ +// +// NoticeList.NoDepartment.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import SwiftUI + +extension NoticeList { + @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: [.전기전자공학부, .컴퓨터공학부], + results: [.전기전자공학부, .컴퓨터공학부, .산업디자인학과] + ) + ) + ) { + 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/NoticeList.swift b/KuringApp/KuringApp/NoticeList/NoticeList.swift index 9bb12d0f..2686b434 100644 --- a/KuringApp/KuringApp/NoticeList/NoticeList.swift +++ b/KuringApp/KuringApp/NoticeList/NoticeList.swift @@ -7,40 +7,79 @@ import Model import SwiftUI +import Network import ComposableArchitecture struct NoticeListFeature: Reducer { struct State: Equatable { - var notices: IdentifiedArrayOf = [] - - var currentDepartment: Department? + var currentNoticeType: NoticeType = .학과 + var currentDepartment: Department? = .산업디자인학과 // 기본값은 addedDepartment의 첫번째 값 @PresentationState var changeDepartment: DepartmentSelectorFeature.State? // TODO: (고민포인트) AppFeature 단으로 (부모 리듀서) 로 옮길 필요는 없을까? - 도메인에 대한 고민 @PresentationState var changeSubscription: SubscriptionAppFeature.State? + + /// 공지 + var noticeDictionary: [NoticeType: NoticeInfo] = [:] + + struct NoticeInfo: Equatable { + /// 공지 + var notices: [Notice] = [] + /// 공지 페이지 + var page: Int = 0 + /// 공지를 더 가져올 수 있는지 여부 + var hasNextList: Bool = true + /// 한번 요청 시 가져올 수 있는 공지 사항 개수 최댓값 + let loadLimit = 20 + } } enum Action { + /// 학과 변경하기 버튼을 탭한 경우 case changeDepartmentButtonTapped + + /// 구독 변경 버튼을 탭한 경우 case changeSubscriptionButtonTapped + /// 공지 카테고리 세그먼트를 탭한 경우 + /// - Parameter noticeType: 선택한 공지 카테고리. + case noticeTypeSegmentTapped(NoticeType) + + /// ``DepartmentSelectorFeature`` 의 Presentation 액션 case changeDepartment(PresentationAction) + + /// ``SubscriptionAppFeature`` 의 Presentation 액션 case changeSubscription(PresentationAction) + /// ``NoticeAppFeature`` 으로 액션을 전달하기 위한 델리게이트 case delegate(Delegate) + /// 공지를 가져오기 위한 네트워크 요청을 하는 경우 + case fetchNotices + + /// 네트워크 요청에 대한 응답을 받은 경우 + case responseNotices(TaskResult<(NoticeType, [Notice])>) + + /// 북마크 버튼을 탭한 경우 + /// - Parameter notice: 북마크 액션 대상인 공지 + case bookmarkTapped(Notice) + + /// ``NoticeAppFeature`` 에 전달 될 액션 종류 enum Delegate { + /// 학과 편집 버튼을 선택한 경우 case editDepartment } } + @Dependency(\.kuringLink) var kuringLink + var body: some ReducerOf { Reduce { state, action in switch action { case .changeDepartmentButtonTapped: state.changeDepartment = DepartmentSelectorFeature.State( currentDepartment: state.currentDepartment, - addedDepartment: [.전기전자공학부, .컴퓨터공학부, .산업디자인학과] + addedDepartment: [.전기전자공학부, .컴퓨터공학부, .산업디자인학과] // TODO: Dependency ) return .none @@ -48,6 +87,17 @@ struct NoticeListFeature: Reducer { state.changeSubscription = SubscriptionAppFeature.State() return .none + case let .noticeTypeSegmentTapped(noticeType): + if state.currentNoticeType != noticeType { + state.currentNoticeType = noticeType + + return .run { send in + await send(.fetchNotices) + } + } else { + return .none + } + case let .changeDepartment(.presented(.delegate(delegate))): switch delegate { case .editDepartment: @@ -56,7 +106,11 @@ struct NoticeListFeature: Reducer { } // TODO: Delegate + case .delegate: + return .none + case .changeDepartment(.presented(.selectDepartment)): + /// ``DepartmentSelectorFeature`` 액션 guard let selectedDepartment = state.changeDepartment?.currentDepartment else { return .none } @@ -64,16 +118,68 @@ struct NoticeListFeature: Reducer { return .none case .changeSubscription(.presented(.subscriptionView(.confirmButtonTapped))): + /// ``SubscriptionAppFeature`` 액션 state.changeSubscription = nil return .none case .changeDepartment: return .none - + case .changeSubscription: return .none + + case .fetchNotices: + let currentNoticeType = state.currentNoticeType + let currentDepartment = state.currentDepartment + let noticeDictionary = state.noticeDictionary + + return .run { + [ + currentNoticeType = currentNoticeType, + currentDepartment = currentDepartment, + noticeDictionary = noticeDictionary + ] send in + + var noticeInfo = noticeDictionary[currentNoticeType] + if noticeInfo == nil { noticeInfo = State.NoticeInfo() } + + var departmentHostPrefix: String? = nil + if currentNoticeType == .학과 && currentDepartment != nil { + departmentHostPrefix = currentDepartment?.hostPrefix + } + do { + let notices = try await kuringLink.fetchNotices( + noticeInfo?.loadLimit ?? 20, + currentNoticeType.shortStringValue, + departmentHostPrefix, + noticeInfo?.page ?? 0 + ) + await send(.responseNotices(.success((currentNoticeType, notices)))) + } catch { + await send(.responseNotices(.failure(error))) + } + } + + case let .responseNotices(.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 + + return .none + + case let .responseNotices(.failure(error)): + + return .none + + case let .bookmarkTapped(notice): - case .delegate: return .none } } @@ -84,74 +190,103 @@ struct NoticeListFeature: Reducer { SubscriptionAppFeature() } } + + func save() { } } struct NoticeList: View { let store: StoreOf + /// - NOTE: NoticeList 만 제외하고 나머지는 NotiecApp 단으로 옮겨야 하는가? + var body: some View { WithViewStore(self.store, observe: { $0 }) { viewStore in - List { - Button((viewStore.currentDepartment ?? .전기전자공학부).korName) { - viewStore.send(.changeDepartmentButtonTapped) - } + VStack(spacing: 0) { + // 공지 카테고리 리스트 + NoticeTypeGrid(store: self.store) + .frame(height: 48) + .onAppear { + print("on Appear") + viewStore.send(.fetchNotices) + } - ForEach(viewStore.state.notices) { notice in - NavigationLink( - state: NoticeAppFeature.Path.State.detail( - NoticeDetailFeature.State(notice: notice) - ) - ) { - VStack(alignment: .leading) { - Text(notice.subject) - - Text(notice.postedDate) + switch viewStore.currentNoticeType { + case .학과: + // 학과공지 + if let currentDepartment = viewStore.currentDepartment { + Section { + let noticeInfo = viewStore.noticeDictionary[.국제] // viewStore.noticeDictionary[currentDepartment] + noticeList(noticeInfo?.notices ?? []) + } header: { + DepartmentSelectorLink(department: currentDepartment) { + viewStore.send(.changeDepartmentButtonTapped) + } } + } else { + NoDepartmentView() } - } - } - .navigationTitle("Notice List") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - NavigationLink( - state: NoticeAppFeature.Path.State.search( - SearchFeature.State() - ) - ) { - Image(systemName: "magnifyingglass") - } + + default: + // 대학공지 + let noticeType = viewStore.currentNoticeType + let notices = viewStore.noticeDictionary[noticeType]?.notices + noticeList(notices ?? []) + + Spacer() } - ToolbarItem(placement: .topBarTrailing) { - Button { - viewStore.send(.changeSubscriptionButtonTapped) - } label: { - Image(systemName: "bell") - } - } } - .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: \.$changeDepartment, + action: { .changeDepartment($0) } + ) + ) { store in + NavigationStack { + DepartmentSelector(store: store) } - .sheet( - store: self.store.scope( - state: \.$changeSubscription, - action: { .changeSubscription($0) } - ) - ) { store in - NavigationStack { - SubscriptionApp(store: store) + .presentationDetents([.medium]) + } + } + + + + /// 공지 리스트 + @ViewBuilder + private func noticeList(_ notices: [Notice]) -> some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + List(notices, id: \.id) { notice in + NavigationLink( + state: NoticeAppFeature.Path.State.detail( + NoticeDetailFeature.State(notice: notice) + ) + ) { + NoticeRow(notice: notice) + .listRowInsets(EdgeInsets()) + .onAppear { + let type = viewStore.currentNoticeType + 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) + } } } + .listStyle(.plain) } + } } @@ -159,7 +294,7 @@ struct NoticeList: View { NavigationStack { NoticeList( store: Store( - initialState: NoticeListFeature.State(notices: [.random]), + initialState: NoticeListFeature.State(), 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..8b30cb10 --- /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.koreanValue) + .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/NoticeTypeGrid.swift b/KuringApp/KuringApp/NoticeList/NoticeTypeGrid.swift new file mode 100644 index 00000000..2fa0b195 --- /dev/null +++ b/KuringApp/KuringApp/NoticeList/NoticeTypeGrid.swift @@ -0,0 +1,38 @@ +// +// NoticeTypeGrid.swift +// KuringApp +// +// Created by 이재성 on 11/24/23. +// + +import Model +import SwiftUI +import ComposableArchitecture + +struct NoticeTypeGrid: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0.currentNoticeType }) { viewStore in + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 0) { + ForEach(NoticeType.allCases, id: \.self) { noticeType in + NoticeTypeColumn( + noticeType: noticeType, + selectedID: viewStore.id + ) + .onTapGesture { + viewStore.send(.noticeTypeSegmentTapped(noticeType)) + withAnimation { + proxy.scrollTo(noticeType, anchor: .center) + } + } + } + } + .padding(.leading, 10) + } + } + } + } +} diff --git a/KuringApp/KuringApp/WillBeRemoved/Department.swift b/KuringApp/KuringApp/WillBeRemoved/Department.swift index 3f939f74..37d2f414 100644 --- a/KuringApp/KuringApp/WillBeRemoved/Department.swift +++ b/KuringApp/KuringApp/WillBeRemoved/Department.swift @@ -7,14 +7,49 @@ import Foundation -struct Department: Identifiable, Equatable { - let id: String + +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 + } - var korName: String { self.id } + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } } extension Department { - static let 전기전자공학부 = Department(id: "전기전자공학부") - static let 컴퓨터공학부 = Department(id: "컴퓨터공학부") - static let 산업디자인학과 = Department(id: "디자인학과") + 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/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 0000000000000000000000000000000000000000..b138c8b6cbef88661fee8fd271e009f381d856e3 GIT binary patch literal 917 zcmV;G18V$GK~#7Fy;jX@ z6G0SzGZSNJ5m$QdcRuk{Ldu?MBLjyM;)KUNJiH;GoNRdR}=8h|`4D^A0_M|(f988~t^$5} zoim^{H_QVHN%xb`A%@2$Fw2FCqqzJmpKPa_zNf2E}C@fw|?f;j~OZZ zb*=wN<+5TFlZKll{z;^DU4gwl1rCDTB?QohXaN}<4M#6g65O>9f%$~(xJ8|Et69b=QA0_rF6MQ7 z=n#Q=PDGM3F;}MGAJFkh2;dfR51Odq8YD41Hoo2>WhA>aKfea;NDPf;4CsV}fDMd) zNzQ9F40wUpRb*0Y&3632Sv&08F+#`;bb!WWLmT($2xd~kX0g-GMcHu?)WCAq&Y>~ zw!osoNxfKD*lZLY`&I5WFP}gTt7Z`MX$mu2c2I>AwvxU(8n8L`tK4f|67C85B|DPI zm_iVFLMnKnE_IDW106h-39Jqqy-=`awbq<68n_;gJsg6Fjp|gxVc@ueV~`2*)Kh@+ z((`<=t0Z_VmaWDgLnCHboPEZBjur62Ah6*)YPN|afyp!{gqTH@92^1o*JU|zb`=TC rqQ_0}Za(~15$h;REidK^%{G4l5U3;AkmEjj00000NkvXXu0mjf*;|`X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e73f1eec03c597fad9a7813e802f7cba652520f GIT binary patch literal 1425 zcmV;C1#bF@P)l@o??A=*=*MAbP)t}_dW@dIZ^V|1-7Gccr_D_y25V9oT z1`t@P4-pQC_$OiW;k!;bcn`vuz}|B#pJ0PGZg-rBP_7{?|2pHi=UrofIsp>)_|l4b zi(3;gb4En!Nniq|n1FqVPrw;mL%>_*8K){B<&XfYkemPoB|TrLi&RTOQw@%w53n>$ zr&&ur5zQpI{;_6UGpk$N$6hkAJoGJ;}ZX-Yb*Z z`Kgu!Npui*uKD+A)X-r_5N zbMaK_y4?q}t2-B>4pN{6L&D_a;}Y};mI6n$7i1I2f+1nz_kRyLJ6wZ=V6U+L%gpMg zx(=xW3aYYJ9F?|uGhp4*MqvCJRGJN)*uy8AAjgls-P1t%x*)W`HJCg$o_+^$0>r8h6-CgdK0H zEc2i4Bp|&UH8glh@G~XT^x7&uLdq%(4QN51Fy|K*2ONj_Y}ne>fzaT@YB4a6%53~* zpnq5@%8jaAodd0?kxS1V2H=vxs7Y0x>#7n=7BE?%{!1sJUzZ#MXOgZwlycP}iAiND zt;FB(?k^-pt;S({Ub^n-8wo2#5u3m^W{X0y?2dz_^hdZh)uQ!Y^;^0b`9kg5o zbZ~F0xDuRO`nKS5Gsl6MQXRWxXi8PgAfl>0^6J1F4(A<`Hap)VcQn0L)cn!62|A;O zqO*G?k;wgyJPD1gG|-1|ZSLlxcK%%M7LSu18X$@!X7p_WqWT&&h&=y(W8ME!?U*E~ z!&S9WY@9%aKP`}yi44t0UJ|}9*2`>r!G4Zh?{KrrKV=}7o9!-BqC1Z)G>xVfsmL7! z`L`t+YQw?mkj-zO3ouX*TO&~MeWT;4bt;0T@3yv-O5BFzXHXT^gHJk1fkI*uPzZIV z=eGWYi4_s6)koNLfdpOv!~--=9{`dU04F3g5=msb ztw`JtA$Bp_R*<1fBuFYkWn$0Vp5r)C2$>JtGl|ogUo;-ub7Lp-_dQ?tcnB#i@#!za zd`nrw#7V7@2fj8;%2r!nFk+L(oerq9qEV)U2ga@D9t7p+J;zw84a5S9}ZDKxqg@b>;-0|4IA7!8qgtPvs z5dKI_DC0N4e7=Ua7ngTD<{aum<->On#Dmj$J9X9`aA{jM8*QLgt6_%K3xKH*{``tI zP^-1zL3;f?d`(mTx$_!DFkNvAKbPtkSfAsJZ#5zFR*10jlr6g_zK! zs3>}2SBso&l@L8kTw}^_JV>2&5TuVN!X>_Eqe*<6^|GweZG9pYA_Qwy zEIAJP&3f{+D?vi!bus<6IV> zN9#4HCCAWx_j^am+Yjtnt%*Zp2|2S~#~r8IQ=Fc*5i=bA%C46~;Q5_L=!My-$+{V7 ztWjiY4)Rgjv#_J;RToLO0~7@qMb_RC|X>dlYA(Qhc`bH{Xl2tgn~Q3L*0$ zbKh22E0qGG>8c2VPm%F4S|n^fl!mWiAmmytLWa$U_J6IZ8!J@Py@;Du=Uzx(x9^2$ z_=yrahu0jRrf^|++QqRAHbJt>pi(=gOyd)1y>aOWi#OZV`X8-X_=9HIt<)o_1;cPW zqd!Aq86il8Xwb;Mv#RP0L$14_C{zF1&^8Q75v94T>Od=6ShClr`*s`~sK(Yd(d_u3zwjf4g!`J?E7i1a_v8|blLIiXIQV8}3q-xH5$l($m+id@(Engy_K(oWs zK{~~=XoAEE!G%T%hK3NZX0EkfB+|XkOVuMw#&c7X#|`bg$J=o}Dp#xY6NEUT8g6D* zBrbdL+sB9uQAj_@jOvNI5ktYvd<7QRM+Kt*6o9ckg;vJCP^n`)@z)r@em6WsT@(nV)Fy*$2dv=+iPNjeSvOzg6BV`RDX|>#X qU8$AER^q6r?5r9S#bPx5xBmf&^r{#bXi?k%0000 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 var koreanValue: String { + switch self { + case .학과: return "학과" + case .학사: return "학사" + case .장학: return "장학" + case .취창업: return "취창업" + case .국제: return "국제" + case .학생: return "학생" + case .산학: return "산학" + case .일반: return "일반" + case .도서관: return "도서관" + } + } + + public var stringValue: String { + switch self { + case .학과: return "department" + case .학사: return "bachelor" + case .장학: return "scholarship" + case .취창업: return "employment" + case .국제: return "national" + case .학생: return "student" + case .산학: return "industry_university" + case .일반: return "normal" + case .도서관: return "library" + } + } + + public var shortStringValue: String { + switch self { + case .학과: return "dep" + case .학사: return "bch" + case .장학: return "sch" + case .취창업: return "emp" + case .국제: return "nat" + case .학생: return "stu" + case .산학: return "ind" + case .일반: return "nor" + case .도서관: return "lib" + } + } +} +