From 80784755e6767b796211eab82679d08366d8102a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Sat, 8 Nov 2025 16:55:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/feature/auth/ui/HomeView.swift | 35 +++++-- .../notification/data/NotificationApi.swift | 51 +++++++++++ .../data/NotificationRepositoryImpl.swift | 37 ++++++++ .../NotificationRepositoryProtocol.swift | 27 ++++++ .../ui/NotificationListView.swift | 91 +++++++++++++++++++ .../viewmodel/NotificationViewModel.swift | 84 +++++++++++++++++ .../feature/orders/ui/OrderDetailView.swift | 1 + .../app/feature/user/ui/ProfileView.swift | 3 +- .../app/navigation/MainTabView.swift | 5 +- .../notiImage.imageset/Contents.json | 21 +++++ .../notiImage.imageset/notiImage.svg | 5 + 11 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 StockMate/StockMate/app/feature/notification/data/NotificationApi.swift create mode 100644 StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift create mode 100644 StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift create mode 100644 StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift create mode 100644 StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift create mode 100644 StockMate/StockMate/resources/Assets.xcassets/notiImage.imageset/Contents.json create mode 100644 StockMate/StockMate/resources/Assets.xcassets/notiImage.imageset/notiImage.svg diff --git a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift index fa9e0b8..e82a5f3 100644 --- a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift @@ -14,6 +14,7 @@ struct HomeView: View { @StateObject private var inventoryViewModel = InventoryViewModel() // @EnvironmentObject var dashboardViewModel: DashboardViewModel //preview ์šฉ @StateObject private var dashboardViewModel = DashboardViewModel() + @StateObject private var notificationViewModel = NotificationViewModel() // ๐Ÿ”ด ์ถ”๊ฐ€ @State private var selectedMonth: String? = nil // โœ… ์ถ”๊ฐ€ @@ -40,10 +41,32 @@ struct HomeView: View { Spacer() - Image("notification") - .font(.system(size: 20)) - .foregroundColor(.gray) +// Image("notification") +// .font(.system(size: 20)) +// .foregroundColor(.gray) + NavigationLink(destination: NotificationListView() + .environmentObject(notificationViewModel)) { // ๐Ÿ”ด ์ „๋‹ฌ + ZStack(alignment: .topTrailing) { + Image("notification") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + .foregroundColor(.gray) + + if notificationViewModel.unreadCount > 0 { + Text("\(notificationViewModel.unreadCount)") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(5) + .background(Color.red) + .clipShape(Circle()) + .offset(x: 5, y: -5) + } + } + } .padding(.trailing, 5) + + } .padding(.horizontal) @@ -146,6 +169,7 @@ struct HomeView: View { await inventoryViewModel.loadLackCountByCategory() await dashboardViewModel.fetchMonthlySpending() // โœ… ์ถ”๊ฐ€ await dashboardViewModel.fetchCategorySpending() // โœ… ์ถ”๊ฐ€ + await notificationViewModel.fetchUnreadCount() // ๐Ÿ”ด ์ถ”๊ฐ€ } .onAppear { Task { await userViewModel.loadUserInfo() } @@ -260,11 +284,6 @@ struct StatusItem: View { } - -//#Preview { -// HomeView() -//} - #Preview { let dashboardVM = DashboardViewModel() dashboardVM.categorySpendings = [ diff --git a/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift b/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift new file mode 100644 index 0000000..b9c139a --- /dev/null +++ b/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift @@ -0,0 +1,51 @@ +// +// NotificationApi.swift +// StockMate +// +// Created by Admin on 11/7/25. +// + +import Foundation +import Alamofire + +struct NotificationItem: Decodable, Identifiable { + let id: Int + let orderId: Int + let orderNumber: String + let message: String + let createdAt: String + let read: Bool +} + +enum NotificationApi { + + // 1๏ธโƒฃ ์•Œ๋ฆผ ์ „์ฒด ์กฐํšŒ + static func getAllNotifications() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/store/notifications/" + return ApiClient.shared.request(url, method: .get) + } + + // 2๏ธโƒฃ ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ๊ฐœ์ˆ˜ ์กฐํšŒ + static func getUnreadCount() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/store/notifications/unread/count" + return ApiClient.shared.request(url, method: .get) + } + + // 3๏ธโƒฃ ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ์กฐํšŒ + static func getUnreadNotifications() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/store/notifications/unread" + return ApiClient.shared.request(url, method: .get) + } + + // 4๏ธโƒฃ ์ „์ฒด ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ + static func markAllAsRead() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/store/notifications/read-all" + return ApiClient.shared.request(url, method: .patch) + } + + // 5๏ธโƒฃ ๊ฐœ๋ณ„ ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ + static func markAsRead(notificationId: Int) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/store/notifications/read?notificationId=\(notificationId)" + return ApiClient.shared.request(url, method: .patch) + } +} diff --git a/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift b/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift new file mode 100644 index 0000000..b5ec459 --- /dev/null +++ b/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift @@ -0,0 +1,37 @@ +// +// NotificationRepositoryImpl.swift +// StockMate +// +// Created by Admin on 11/7/25. +// + +import SwiftUI +import Alamofire + +final class NotificationRepositoryImpl: NotificationRepositoryProtocol { + + func getAllNotifications() async -> AppResult> { + let request = NotificationApi.getAllNotifications() + return await safeApi(request, decodeTo: ApiResponse<[NotificationItem]>.self) + } + + func getUnreadCount() async -> AppResult> { + let request = NotificationApi.getUnreadCount() + return await safeApi(request, decodeTo: ApiResponse.self) + } + + func getUnreadNotifications() async -> AppResult> { + let request = NotificationApi.getUnreadNotifications() + return await safeApi(request, decodeTo: ApiResponse<[NotificationItem]>.self) + } + + func markAllAsRead() async -> AppResult> { + let request = NotificationApi.markAllAsRead() + return await safeApi(request, decodeTo: ApiResponse.self) + } + + func markAsRead(notificationId: Int) async -> AppResult> { + let request = NotificationApi.markAsRead(notificationId: notificationId) + return await safeApi(request, decodeTo: ApiResponse.self) + } +} diff --git a/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift b/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift new file mode 100644 index 0000000..207f460 --- /dev/null +++ b/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift @@ -0,0 +1,27 @@ +// +// NotificationRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 11/7/25. +// + +import SwiftUI +import Alamofire + + +protocol NotificationRepositoryProtocol { + // 1๏ธโƒฃ ์ „์ฒด ์•Œ๋ฆผ ์กฐํšŒ + func getAllNotifications() async -> AppResult> + + // 2๏ธโƒฃ ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ๊ฐœ์ˆ˜ ์กฐํšŒ + func getUnreadCount() async -> AppResult> + + // 3๏ธโƒฃ ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ์กฐํšŒ + func getUnreadNotifications() async -> AppResult> + + // 4๏ธโƒฃ ์ „์ฒด ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ + func markAllAsRead() async -> AppResult> + + // 5๏ธโƒฃ ๊ฐœ๋ณ„ ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ + func markAsRead(notificationId: Int) async -> AppResult> +} diff --git a/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift new file mode 100644 index 0000000..aed62a7 --- /dev/null +++ b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift @@ -0,0 +1,91 @@ +// +// NotificationListView.swift +// StockMate +// +// Created by Admin on 11/7/25. +// +import SwiftUI + +struct NotificationListView: View { + @StateObject private var viewModel = NotificationViewModel() + @State private var selectedOrderId: Int? = nil + + var body: some View { + VStack { + ScrollView { + LazyVStack(spacing: 12) { + ForEach(viewModel.notifications) { notification in + NotificationCardView(item: notification) { + Task { + await viewModel.markAsRead(notification.id) + selectedOrderId = notification.orderId + } + } + } + } + .padding() + } + } + .background(Color.Light) + .navigationTitle("์•Œ๋ฆผ") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("์ „์ฒด ์ฝ์Œ") { + Task { await viewModel.markAllAsRead() } + } + .foregroundColor(.red) + .font(.subheadline) + } + } + .navigationDestination(item: $selectedOrderId) { orderId in + OrderDetailView(orderId: orderId, orderViewModel: OrderViewModel()) + } + .task { + await viewModel.fetchNotifications() + } + } +} + + +struct NotificationCardView: View { + let item: NotificationItem + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(alignment: .center, spacing: 14) { + Image("notiImage") + .resizable() + .scaledToFit() + .frame(width: 38, height: 38) + + VStack(alignment: .leading, spacing: 6) { + Text(item.message) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.primary) + Text(item.orderNumber) + .font(.system(size: 13, weight: .regular)) + .foregroundColor(.gray) + Text(formattedDate(item.createdAt)) + .font(.caption) + .foregroundColor(.gray) + } + Spacer() + + // ๐Ÿ”ด ์•ˆ ์ฝ์€ ์•Œ๋ฆผ ํ‘œ์‹œ ์  + if !item.read { + Circle() + .fill(Color.red) + .frame(width: 7, height: 7) + .padding(.trailing) + } + + } + .padding() + .background(Color.white) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 1) + } + } +} diff --git a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift new file mode 100644 index 0000000..a09e741 --- /dev/null +++ b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift @@ -0,0 +1,84 @@ +// +// NotificationViewModel.swift +// StockMate +// +// Created by Admin on 11/7/25. +// + +import Foundation + +@MainActor +final class NotificationViewModel: ObservableObject { + @Published var notifications: [NotificationItem] = [] + @Published var unreadCount: Int = 0 // ๐Ÿ”ด ์ถ”๊ฐ€ + @Published var isLoading = false + + private let repository: NotificationRepositoryProtocol = NotificationRepositoryImpl() + + // ์ „์ฒด ์•Œ๋ฆผ ์กฐํšŒ + func fetchNotifications() async { + isLoading = true + defer { isLoading = false } + + let result = await repository.getAllNotifications() + switch result { + case .success(let response): + notifications = response.data!.sorted { $0.createdAt > $1.createdAt } + case .failure(let error): + print("โŒ ์•Œ๋ฆผ ์กฐํšŒ ์‹คํŒจ:", error.localizedDescription) + } + } + + // ๐Ÿ”ด ์ฝ์ง€ ์•Š์€ ๊ฐœ์ˆ˜ ์กฐํšŒ + func fetchUnreadCount() async { + let result = await repository.getUnreadCount() + switch result { + case .success(let response): + unreadCount = response.data ?? 0 + case .failure(let error): + print("โŒ ์ฝ์ง€ ์•Š์€ ๊ฐœ์ˆ˜ ์กฐํšŒ ์‹คํŒจ:", error.localizedDescription) + } + } + + // ๊ฐœ๋ณ„ ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ + func markAsRead(_ id: Int) async { + let result = await repository.markAsRead(notificationId: id) + switch result { + case .success: + if let index = notifications.firstIndex(where: { $0.id == id }) { + notifications[index] = NotificationItem( + id: notifications[index].id, + orderId: notifications[index].orderId, + orderNumber: notifications[index].orderNumber, + message: notifications[index].message, + createdAt: notifications[index].createdAt, + read: true + ) + } + unreadCount = max(0, unreadCount - 1) // ๐Ÿ”ด ์นด์šดํŠธ ์ฆ‰์‹œ ๋ฐ˜์˜ + case .failure(let error): + print("โŒ ์•Œ๋ฆผ ์ฝ์Œ ์ฒ˜๋ฆฌ ์‹คํŒจ:", error.localizedDescription) + } + } + + // ์ „์ฒด ์ฝ์Œ ์ฒ˜๋ฆฌ + func markAllAsRead() async { + let result = await repository.markAllAsRead() + switch result { + case .success: + for i in 0.. + + + + From 2092887c0d334af40c47a207eb6c73745aa28eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Sat, 8 Nov 2025 17:26:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[REFAC]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- StockMate/StockMate/app/feature/auth/ui/HomeView.swift | 8 ++------ .../feature/notification/ui/NotificationListView.swift | 10 +++++----- .../notification/viewmodel/NotificationViewModel.swift | 3 ++- .../app/feature/orders/ui/OrderDetailView.swift | 1 - 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift index e82a5f3..825748f 100644 --- a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift @@ -40,12 +40,8 @@ struct HomeView: View { } Spacer() - -// Image("notification") -// .font(.system(size: 20)) -// .foregroundColor(.gray) - NavigationLink(destination: NotificationListView() - .environmentObject(notificationViewModel)) { // ๐Ÿ”ด ์ „๋‹ฌ + + NavigationLink(destination: NotificationListView()) { // ๐Ÿ”ด ์ „๋‹ฌ ZStack(alignment: .topTrailing) { Image("notification") .resizable() diff --git a/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift index aed62a7..c04134c 100644 --- a/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift +++ b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift @@ -7,17 +7,17 @@ import SwiftUI struct NotificationListView: View { - @StateObject private var viewModel = NotificationViewModel() + @StateObject private var notificationViewModel = NotificationViewModel() @State private var selectedOrderId: Int? = nil var body: some View { VStack { ScrollView { LazyVStack(spacing: 12) { - ForEach(viewModel.notifications) { notification in + ForEach(notificationViewModel.notifications) { notification in NotificationCardView(item: notification) { Task { - await viewModel.markAsRead(notification.id) + await notificationViewModel.markAsRead(notification.id) selectedOrderId = notification.orderId } } @@ -32,7 +32,7 @@ struct NotificationListView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("์ „์ฒด ์ฝ์Œ") { - Task { await viewModel.markAllAsRead() } + Task { await notificationViewModel.markAllAsRead() } } .foregroundColor(.red) .font(.subheadline) @@ -42,7 +42,7 @@ struct NotificationListView: View { OrderDetailView(orderId: orderId, orderViewModel: OrderViewModel()) } .task { - await viewModel.fetchNotifications() + await notificationViewModel.fetchNotifications() } } } diff --git a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift index a09e741..e4d6b11 100644 --- a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift +++ b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift @@ -23,7 +23,8 @@ final class NotificationViewModel: ObservableObject { let result = await repository.getAllNotifications() switch result { case .success(let response): - notifications = response.data!.sorted { $0.createdAt > $1.createdAt } + notifications = (response.data ?? []).sorted { $0.createdAt > $1.createdAt } +// notifications = response.data!.sorted { $0.createdAt > $1.createdAt } case .failure(let error): print("โŒ ์•Œ๋ฆผ ์กฐํšŒ ์‹คํŒจ:", error.localizedDescription) } diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift index 7c3455d..8ff3d78 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift @@ -11,7 +11,6 @@ struct OrderDetailView: View { let orderId: Int @ObservedObject var orderViewModel: OrderViewModel @StateObject private var viewModel = OrderDetailViewModel() - @State private var didFetch = false // โœ… ํ•œ ๋ฒˆ๋งŒ fetch var body: some View { ScrollView { From 8274d9de6d91c037333891b3d447adce425d7545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Sat, 8 Nov 2025 17:32:25 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[REFAC]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/notification/viewmodel/NotificationViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift index e4d6b11..a2fb0de 100644 --- a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift +++ b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift @@ -24,7 +24,6 @@ final class NotificationViewModel: ObservableObject { switch result { case .success(let response): notifications = (response.data ?? []).sorted { $0.createdAt > $1.createdAt } -// notifications = response.data!.sorted { $0.createdAt > $1.createdAt } case .failure(let error): print("โŒ ์•Œ๋ฆผ ์กฐํšŒ ์‹คํŒจ:", error.localizedDescription) }