Skip to content

[FEAT] 알림 기능 구현#16

Merged
Yoo-Hyuna merged 4 commits into
devfrom
feat/#15
Nov 8, 2025
Merged

[FEAT] 알림 기능 구현#16
Yoo-Hyuna merged 4 commits into
devfrom
feat/#15

Conversation

@Yoo-Hyuna

@Yoo-Hyuna Yoo-Hyuna commented Nov 8, 2025

Copy link
Copy Markdown
Collaborator

📣 Related Issue

📝 Summary

알림 기능 구현

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 알림 기능 추가: 홈 화면에 읽지 않은 알림 개수를 표시하는 배지 추가
    • 알림 목록 뷰 추가: 모든 알림을 조회하고 개별 또는 일괄 읽음 처리 가능
    • 프로필 화면에서 알림 목록으로 직접 이동 가능

@Yoo-Hyuna Yoo-Hyuna requested a review from rhkr8521 November 8, 2025 08:01
@Yoo-Hyuna Yoo-Hyuna self-assigned this Nov 8, 2025
@Yoo-Hyuna Yoo-Hyuna added the FEAT label Nov 8, 2025
@coderabbitai

coderabbitai Bot commented Nov 8, 2025

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

알림 기능을 구현하는 PR로, API 엔드포인트 연결, 저장소 계층, ViewModel, 그리고 UI 뷰를 추가합니다. 홈뷰에 알림 배지, 알림 목록 페이지, 개별/전체 읽음 기능이 포함됩니다.

Changes

Cohort / File(s) Change Summary
API 및 저장소 계층
NotificationApi.swift, NotificationRepositoryProtocol.swift, NotificationRepositoryImpl.swift
NotificationItem 데이터 모델 추가; 알림 조회, 읽지 않음 개수 조회, 개별/전체 읽음 표시 API 메서드 정의 및 구현
알림 ViewModel
NotificationViewModel.swift
@MainActor ViewModel 추가로 알림 상태(notifications, unreadCount, isLoading) 관리; fetchNotifications, fetchUnreadCount, markAsRead, markAllAsRead 메서드 구현
알림 UI 뷰
NotificationListView.swift
NotificationListView와 NotificationCardView 추가; 알림 목록 표시, 각 항목의 읽음 상태 표시, 전체 읽음 버튼 제공
기존 뷰 통합
HomeView.swift, ProfileView.swift
HomeView에 NotificationViewModel 추가 및 알림 배지(읽지 않음 개수) 표시하는 NavigationLink 추가; ProfileView의 알림 설정 행을 NotificationListView로 네비게이션하는 SettingNavigationRow로 변경
네비게이션 구조 정리
MainTabView.swift
탭 0, 3의 NavigationStack 래퍼 제거; HomeView와 ProfileView를 직접 렌더링
리소스
notiImage.imageset/Contents.json
알림 아이콘 이미지 에셋 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeView
    participant NotificationViewModel
    participant NotificationApi
    participant Backend
    participant NotificationListView

    User->>HomeView: 화면 진입
    activate HomeView
    HomeView->>NotificationViewModel: fetchUnreadCount() 호출
    activate NotificationViewModel
    NotificationViewModel->>NotificationApi: getUnreadCount() 요청
    activate NotificationApi
    NotificationApi->>Backend: GET /api/v1/order/store/notifications/unread/count
    activate Backend
    Backend-->>NotificationApi: unreadCount 반환
    deactivate Backend
    deactivate NotificationApi
    NotificationViewModel->>NotificationViewModel: `@Published` unreadCount 갱신
    Note over HomeView: 배지에 읽지 않음 개수 표시
    deactivate NotificationViewModel
    deactivate HomeView

    User->>HomeView: 알림 아이콘 탭
    activate HomeView
    HomeView->>NotificationListView: NavigationLink로 이동
    deactivate HomeView
    activate NotificationListView
    NotificationListView->>NotificationViewModel: fetchNotifications() 호출
    activate NotificationViewModel
    NotificationViewModel->>NotificationApi: getAllNotifications() 요청
    activate NotificationApi
    NotificationApi->>Backend: GET /api/v1/order/store/notifications/
    activate Backend
    Backend-->>NotificationApi: [NotificationItem] 반환
    deactivate Backend
    deactivate NotificationApi
    NotificationViewModel->>NotificationViewModel: 알림 목록 정렬 및 `@Published` notifications 갱신
    Note over NotificationListView: 알림 목록 렌더링
    deactivate NotificationViewModel
    deactivate NotificationListView

    User->>NotificationListView: 특정 알림 탭
    activate NotificationListView
    NotificationListView->>NotificationViewModel: markAsRead(notificationId) 호출
    activate NotificationViewModel
    NotificationViewModel->>NotificationApi: PATCH /api/v1/order/store/notifications/read?notificationId=...
    activate NotificationApi
    NotificationApi->>Backend: 알림 읽음 처리
    activate Backend
    Backend-->>NotificationApi: 성공 응답
    deactivate Backend
    deactivate NotificationApi
    NotificationViewModel->>NotificationViewModel: 로컬 상태 갱신 (read=true, unreadCount--)
    Note over NotificationListView: UI 업데이트 (읽음 표시기 제거)
    deactivate NotificationViewModel
    deactivate NotificationListView
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 깊게 검토해야 할 영역:
    • NotificationViewModel의 비동기 메서드들에서 @Published 속성 업데이트 로직 및 에러 처리 방식 확인 필요
    • NotificationRepositoryImpl에서 API 응답 처리 및 결과 매핑 로직 검증
    • HomeView와 NotificationListView 간 ViewModel 전달 (@environmentObject) 및 상태 동기화 검증
    • 알림 읽음 상태 업데이트 시 로컬 배열 변경 및 unreadCount 감소 로직 일관성 확인
    • MainTabView의 네비게이션 구조 변경이 다른 탭 동작에 영향을 주지 않는지 확인

Possibly related PRs

  • StockMate-Hyundai/iOS#4: HomeView.swift 수정 관련 - 해당 PR은 새로운 HomeView UI 구현이고, 이 PR은 NotificationViewModel 통합 및 알림 배지 추가로 HomeView를 확장합니다.

Suggested reviewers

  • rhkr8521

Poem

🐰 알림이 쏙쏙, 배지도 반짝,
읽음 표시 척척, 기능도 쏙쏙!
API는 통통, ViewModel은 똑똑,
깔끔한 구조에 사용자 가슴 쌨다! 🔔✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '[FEAT] 알림 기능 구현'은 변경사항의 주요 내용인 알림 기능 구현을 명확하게 나타낸다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항이 linked issue #15의 요구사항을 충족한다: 알림 API 연결, 알림 페이지 생성 및 아이콘 추가, 알림 읽음 기능 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항이 알림 기능 구현의 범위 내에 있으며, 관련 없는 변경사항은 없다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Yoo-Hyuna

Copy link
Copy Markdown
Collaborator Author

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#15

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 8, 2025

Copy link
Copy Markdown

@Yoo-Hyuna: 알림 기능 구현에 대한 리뷰를 시작하겠습니다. 잠시만 기다려주세요! 🔍

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1)

11-11: 불필요한 nil 초기화를 제거하세요.

Optional 변수를 nil로 초기화하는 것은 중복입니다. SwiftLint도 이를 경고하고 있습니다.

다음 diff를 적용하세요:

-    @State private var selectedOrderId: Int? = nil
+    @State private var selectedOrderId: Int?
StockMate/StockMate/app/feature/notification/data/NotificationApi.swift (3)

11-18: createdAt를 Date 타입으로 고려해보세요.

createdAt이 String으로 정의되어 있어 날짜 정렬이나 포매팅 시 추가 변환이 필요합니다. 서버에서 ISO 8601 형식으로 제공한다면 문자열 정렬이 가능하지만, Date 타입을 사용하면 더 안전하고 명확한 날짜 처리가 가능합니다.

Date 타입을 사용하려면 다음과 같이 커스텀 디코딩을 추가할 수 있습니다:

struct NotificationItem: Decodable, Identifiable {
    let id: Int
    let orderId: Int
    let orderNumber: String
    let message: String
    let createdAt: Date
    let read: Bool
    
    enum CodingKeys: String, CodingKey {
        case id, orderId, orderNumber, message, createdAt, read
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        orderId = try container.decode(Int.self, forKey: .orderId)
        orderNumber = try container.decode(String.self, forKey: .orderNumber)
        message = try container.decode(String.self, forKey: .message)
        read = try container.decode(Bool.self, forKey: .read)
        
        let dateString = try container.decode(String.self, forKey: .createdAt)
        let formatter = ISO8601DateFormatter()
        createdAt = formatter.date(from: dateString) ?? Date()
    }
}

23-26: URL 구성 방식을 개선할 수 있습니다.

문자열 연결을 통한 URL 구성은 동작하지만, URLComponents를 사용하면 더 안전하고 유지보수가 쉬운 코드가 됩니다. 특히 쿼리 파라미터가 있는 경우 자동으로 URL 인코딩을 처리해줍니다.

static func getAllNotifications() -> DataRequest {
    var components = URLComponents(string: ApiClient.baseURL + "api/v1/order/store/notifications/")!
    return ApiClient.shared.request(components.url!, method: .get)
}

47-50: 쿼리 파라미터 처리를 개선하세요.

쿼리 파라미터를 URL 문자열 보간으로 처리하고 있습니다. Int 타입이므로 안전하지만, URLComponents를 사용하면 더 명확하고 확장 가능한 코드가 됩니다.

static func markAsRead(notificationId: Int) -> DataRequest {
    var components = URLComponents(string: ApiClient.baseURL + "api/v1/order/store/notifications/read")!
    components.queryItems = [URLQueryItem(name: "notificationId", value: String(notificationId))]
    return ApiClient.shared.request(components.url!, method: .patch)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bebaee0 and c6a39ca.

⛔ Files ignored due to path filters (1)
  • StockMate/StockMate/resources/Assets.xcassets/notiImage.imageset/notiImage.svg is excluded by !**/*.svg
📒 Files selected for processing (10)
  • StockMate/StockMate/app/feature/auth/ui/HomeView.swift (3 hunks)
  • StockMate/StockMate/app/feature/notification/data/NotificationApi.swift (1 hunks)
  • StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift (1 hunks)
  • StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift (1 hunks)
  • StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1 hunks)
  • StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift (1 hunks)
  • StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift (1 hunks)
  • StockMate/StockMate/app/feature/user/ui/ProfileView.swift (1 hunks)
  • StockMate/StockMate/app/navigation/MainTabView.swift (2 hunks)
  • StockMate/StockMate/resources/Assets.xcassets/notiImage.imageset/Contents.json (1 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift

[Warning] 11-11: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (9)
StockMate/StockMate/resources/Assets.xcassets/notiImage.imageset/Contents.json (1)

1-21: SVG 에셋이 모든 스케일에서 올바르게 렌더링되는지 확인하세요.

1x 스케일만 SVG 파일을 지정하고 2x, 3x는 비어있습니다. SVG는 해상도 독립적이므로 이것이 의도된 것일 수 있지만, 모든 디바이스 스케일에서 알림 아이콘이 올바르게 표시되는지 테스트해보시기 바랍니다.

StockMate/StockMate/app/feature/user/ui/ProfileView.swift (1)

45-46: LGTM!

알림 화면으로의 네비게이션이 올바르게 구현되었습니다.

StockMate/StockMate/app/navigation/MainTabView.swift (1)

21-34: LGTM!

HomeView와 ProfileView의 NavigationStack 래핑을 제거한 것은 적절합니다. 각 뷰에서 자체적으로 네비게이션을 관리하는 구조로 변경된 것으로 보입니다.

StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift (1)

11-37: LGTM!

Repository 구현이 일관된 패턴을 따르고 있으며, 각 메서드가 명확한 책임을 가지고 있습니다.

StockMate/StockMate/app/feature/auth/ui/HomeView.swift (1)

47-66: 알림 배지 UI 구현이 잘 되어 있습니다.

NavigationLink를 활용하여 알림 아이콘에 읽지 않은 알림 개수를 배지로 표시하는 구현이 적절합니다.

StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1)

42-42: OrderViewModel 인스턴스 공유 방식을 재검토하세요.

네비게이션 시 새로운 OrderViewModel() 인스턴스를 생성하고 있습니다. 만약 앱의 다른 부분에서 OrderViewModel의 상태를 공유해야 한다면, 이 방식은 문제가 될 수 있습니다.

OrderDetailView가 주문 데이터를 독립적으로 fetch하는 경우라면 현재 구현이 적절하지만, 주문 목록과 상태를 공유해야 한다면 environmentObject나 다른 방식으로 기존 인스턴스를 전달하는 것을 고려하세요.

StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift (1)

12-27: LGTM!

프로토콜 정의가 명확하며, 알림 기능에 필요한 모든 작업이 잘 정의되어 있습니다. 각 메서드가 async로 선언되어 있어 비동기 처리가 적절히 고려되었습니다.

StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift (2)

33-41: 코드가 잘 작성되었습니다.

nil coalescing을 사용하여 안전하게 데이터를 처리하고 있으며, 에러 핸들링도 적절합니다.


26-26: 해당 리뷰 의견은 이 코드베이스에 적용되지 않습니다.

코드를 검증한 결과, createdAt은 ISO 8601 형식(YYYY-MM-DDTHH:MM:SS)으로 저장되고 있습니다. 이 형식은 사전순 문자열 비교로도 올바르게 정렬되므로, 현재 구현은 문제없습니다.

Likely an incorrect or invalid review comment.

@StateObject private var inventoryViewModel = InventoryViewModel()
// @EnvironmentObject var dashboardViewModel: DashboardViewModel //preview 용
@StateObject private var dashboardViewModel = DashboardViewModel()
@StateObject private var notificationViewModel = NotificationViewModel() // 🔴 추가

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

NotificationViewModel 인스턴스 관리를 확인하세요.

HomeView에서 NotificationViewModel을 생성하고 NotificationListViewenvironmentObject를 통해 전달하고 있지만, NotificationListView.swift의 Line 10에서 자체적으로 @StateObject로 새로운 인스턴스를 생성하고 있습니다. 이로 인해 두 개의 독립적인 ViewModel 인스턴스가 존재하게 되어, HomeView의 unread count와 NotificationListView의 데이터가 동기화되지 않습니다.

다음 스크립트를 실행하여 NotificationViewModel 사용 패턴을 확인하세요:


🏁 Script executed:

#!/bin/bash
# NotificationViewModel이 어떻게 사용되는지 확인
rg -n "NotificationViewModel" --type swift -C 3

Length of output: 2869


HomeView에서 notificationViewModel을 어떻게 사용하는지 그리고 NotificationListView가 환경 객체를 받는지 확인하겠습니다.

이제 HomeView에서 실제로 notificationViewModel을 environmentObject로 전달하는지, 그리고 NotificationListView가 어떻게 선언되어 있는지 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
# HomeView에서 notificationViewModel 사용 패턴 확인 (line 47-66 근처)
sed -n '45,70p' StockMate/StockMate/app/feature/auth/ui/HomeView.swift

Length of output: 1370


🏁 Script executed:

#!/bin/bash
# NotificationListView 선언 부분 확인 (line 8-12 근처)
sed -n '7,15p' StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift

Length of output: 236


NotificationListView에서 @EnvironmentObject를 사용하여 HomeView의 ViewModel 인스턴스를 받으세요.

HomeView에서 notificationViewModel.environmentObject(notificationViewModel)로 전달하고 있으나, NotificationListView의 Line 10에서 @StateObject로 새로운 인스턴스를 생성하고 있습니다. SwiftUI에서는 @StateObject@EnvironmentObject보다 우선순위가 높으므로, NotificationListView는 전달받은 환경 객체를 무시하고 자신의 독립적인 인스턴스만 사용합니다. 이로 인해 두 뷰의 데이터가 동기화되지 않습니다.

필수 수정:

NotificationListView.swift, Line 10:
@StateObject private var viewModel = NotificationViewModel()
↓
@EnvironmentObject var notificationViewModel: NotificationViewModel
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/auth/ui/HomeView.swift around line 17 and
NotificationListView.swift at line 10: HomeView currently creates and injects
notificationViewModel with .environmentObject(notificationViewModel) but
NotificationListView is creating its own @StateObject, which overrides the
environment object and prevents shared state; replace the @StateObject in
NotificationListView with an @EnvironmentObject property to receive the injected
instance (update the property declaration and any internal references to use
that environment object), and ensure HomeView continues to call
.environmentObject(notificationViewModel) when presenting NotificationListView
so both views share the same NotificationViewModel instance.

import SwiftUI

struct NotificationListView: View {
@StateObject private var viewModel = NotificationViewModel()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

ViewModel을 environmentObject로 주입받도록 수정하세요.

현재 NotificationListView에서 자체적으로 @StateObjectNotificationViewModel을 생성하고 있습니다. 하지만 HomeView.swift의 Line 48에서 이미 생성된 ViewModel을 environmentObject로 전달하고 있습니다.

이로 인해 두 가지 문제가 발생합니다:

  1. HomeView와 NotificationListView가 서로 다른 ViewModel 인스턴스를 사용하여 데이터가 동기화되지 않음
  2. HomeView의 unread count 배지가 알림을 읽어도 업데이트되지 않음

다음 diff를 적용하여 수정하세요:

-    @StateObject private var viewModel = NotificationViewModel()
+    @EnvironmentObject var viewModel: NotificationViewModel
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@StateObject private var viewModel = NotificationViewModel()
@EnvironmentObject var viewModel: NotificationViewModel
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift
around line 10, the view currently creates its own NotificationViewModel with
@StateObject which causes a different instance than the one passed from HomeView
via environmentObject; replace the @StateObject declaration with an
@EnvironmentObject var viewModel: NotificationViewModel so the view uses the
shared instance provided by HomeView, and remove any local initialization;
ensure previews or parent views inject the environmentObject where needed.

Comment on lines +44 to +62
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)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

NotificationItem 업데이트 로직을 개선하세요.

Lines 49-56에서 NotificationItem의 모든 필드를 수동으로 복사하여 새 인스턴스를 생성하고 있습니다. 이 패턴은 NotificationItem에 필드가 추가되거나 변경될 때 유지보수가 어렵고 실수하기 쉽습니다.

NotificationItem에 업데이트 메서드를 추가하는 것을 권장합니다:

NotificationApi.swift에 다음을 추가:

extension NotificationItem {
    func markingAsRead() -> NotificationItem {
        return NotificationItem(
            id: id,
            orderId: orderId,
            orderNumber: orderNumber,
            message: message,
            createdAt: createdAt,
            read: true
        )
    }
}

그런 다음 ViewModel에서 다음과 같이 사용:

 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
-            )
+            notifications[index] = notifications[index].markingAsRead()
         }
         unreadCount = max(0, unreadCount - 1)
     case .failure(let error):
         print("❌ 알림 읽음 처리 실패:", error.localizedDescription)
     }
 }
🤖 Prompt for AI Agents
StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift
lines 44-62: the ViewModel manually re-creates NotificationItem by copying every
field which is brittle; add a convenience instance method (e.g.,
markingAsRead()) on NotificationItem (place in NotificationApi.swift or the
model file) that returns a copy with read set to true, then replace the manual
construction with notifications[index] = notifications[index].markingAsRead();
keep the rest of the logic (index lookup and unreadCount decrement) unchanged.

Comment on lines +65 to +83
func markAllAsRead() async {
let result = await repository.markAllAsRead()
switch result {
case .success:
for i in 0..<notifications.count {
notifications[i] = NotificationItem(
id: notifications[i].id,
orderId: notifications[i].orderId,
orderNumber: notifications[i].orderNumber,
message: notifications[i].message,
createdAt: notifications[i].createdAt,
read: true
)
}
unreadCount = 0 // 🔴 전체 읽음 시 0으로 초기화
case .failure(let error):
print("❌ 전체 읽음 실패:", error.localizedDescription)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

markAsRead와 동일한 리팩토링을 적용하세요.

Lines 70-77에서도 NotificationItem을 수동으로 재생성하는 동일한 패턴이 반복됩니다. 앞서 제안한 markingAsRead() 메서드를 사용하면 이 메서드도 간결하게 개선할 수 있습니다.

 func markAllAsRead() async {
     let result = await repository.markAllAsRead()
     switch result {
     case .success:
-        for i in 0..<notifications.count {
-            notifications[i] = NotificationItem(
-                id: notifications[i].id,
-                orderId: notifications[i].orderId,
-                orderNumber: notifications[i].orderNumber,
-                message: notifications[i].message,
-                createdAt: notifications[i].createdAt,
-                read: true
-            )
-        }
+        notifications = notifications.map { $0.markingAsRead() }
         unreadCount = 0
     case .failure(let error):
         print("❌ 전체 읽음 실패:", error.localizedDescription)
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift
around lines 65 to 83, the method markAllAsRead repeats the manual
NotificationItem reconstruction loop (lines 70–77); replace that loop with the
previously introduced markingAsRead() refactor by mapping each notification to
its markingAsRead() variant and assign the resulting array back to
notifications, then set unreadCount = 0 on success; keep the existing repository
call and failure handling unchanged.

Comment thread StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift Outdated
@Yoo-Hyuna

Copy link
Copy Markdown
Collaborator Author

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.
Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.
You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.
✨ Finishing touches
🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#15

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai review

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 8, 2025

Copy link
Copy Markdown

@Yoo-Hyuna: 알림 기능 구현에 대한 리뷰를 시작하겠습니다! 🔍

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1)

10-10: [중요] ViewModel을 environmentObject로 주입받도록 수정하세요.

현재 NotificationListView에서 자체적으로 @StateObjectNotificationViewModel을 생성하고 있습니다. 하지만 AI 요약에 따르면 HomeView.swift에서 이미 생성된 ViewModel을 environmentObject로 전달하고 있습니다.

이로 인해 두 가지 문제가 발생합니다:

  1. HomeView와 NotificationListView가 서로 다른 ViewModel 인스턴스를 사용하여 데이터가 동기화되지 않음
  2. HomeView의 읽지 않은 알림 배지가 알림을 읽어도 업데이트되지 않음

다음 diff를 적용하여 수정하세요:

-    @StateObject private var notificationViewModel = NotificationViewModel()
+    @EnvironmentObject var notificationViewModel: NotificationViewModel
StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift (2)

50-57: NotificationItem 업데이트 로직을 개선하세요.

NotificationItem의 모든 필드를 수동으로 복사하여 새 인스턴스를 생성하고 있습니다. 이 패턴은 NotificationItem에 필드가 추가되거나 변경될 때 유지보수가 어렵고 실수하기 쉽습니다.

NotificationItem에 업데이트 메서드를 추가하는 것을 권장합니다. NotificationApi.swift에 다음 extension을 추가하세요:

extension NotificationItem {
    func markingAsRead() -> NotificationItem {
        return NotificationItem(
            id: id,
            orderId: orderId,
            orderNumber: orderNumber,
            message: message,
            createdAt: createdAt,
            read: true
        )
    }
}

그런 다음 ViewModel에서 다음과 같이 사용:

 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
-    )
+    notifications[index] = notifications[index].markingAsRead()
 }

71-78: markAsRead와 동일한 리팩토링을 적용하세요.

markAllAsRead에서도 NotificationItem을 수동으로 재생성하는 동일한 패턴이 반복됩니다. 앞서 제안한 markingAsRead() 메서드를 사용하면 이 메서드도 간결하게 개선할 수 있습니다.

다음과 같이 수정하세요:

 case .success:
-    for i in 0..<notifications.count {
-        notifications[i] = NotificationItem(
-            id: notifications[i].id,
-            orderId: notifications[i].orderId,
-            orderNumber: notifications[i].orderNumber,
-            message: notifications[i].message,
-            createdAt: notifications[i].createdAt,
-            read: true
-        )
-    }
+    notifications = notifications.map { $0.markingAsRead() }
     unreadCount = 0
🧹 Nitpick comments (2)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1)

11-11: 중복된 nil 초기화를 제거하세요.

옵셔널 변수는 기본적으로 nil로 초기화되므로 명시적으로 지정할 필요가 없습니다.

다음과 같이 수정하세요:

-    @State private var selectedOrderId: Int? = nil
+    @State private var selectedOrderId: Int?
StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift (1)

29-29: 사용자에게 에러를 알리는 메커니즘을 추가하는 것을 고려하세요.

현재 모든 에러 케이스에서 print로만 로깅하고 있어 사용자는 작업이 실패했는지 알 수 없습니다.

다음 옵션 중 하나를 고려하세요:

  1. Published 에러 상태 추가:
@Published var errorMessage: String?

// 사용 예시
case .failure(let error):
    errorMessage = "알림을 불러오는데 실패했습니다."
    print("❌ 알림 조회 실패:", error.localizedDescription)
  1. Alert 표시를 위한 flag 추가:
@Published var showError = false
@Published var errorMessage: String = ""

// View에서 .alert(isPresented: $viewModel.showError) 사용

이렇게 하면 사용자가 네트워크 오류나 API 실패를 명확하게 인지할 수 있습니다.

Also applies to: 40-40, 61-61, 82-82

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6a39ca and 2092887.

📒 Files selected for processing (3)
  • StockMate/StockMate/app/feature/auth/ui/HomeView.swift (3 hunks)
  • StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1 hunks)
  • StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • StockMate/StockMate/app/feature/auth/ui/HomeView.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift

[Warning] 11-11: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (1)
StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift (1)

41-43: OrderViewModel 인스턴스 생성 방식이 일관성 없이 구현되어 있습니다.

코드베이스에서 OrderViewModel 사용이 다음과 같이 불일치합니다:

  • OrderListViewOrderDetailView: 부모의 @StateObject orderViewModel 공유
  • NotificationListViewOrderDetailView: 새 OrderViewModel() 인스턴스 생성
  • InOutHistoryViewOrderDetailView: 새 OrderViewModel() 인스턴스 생성

cancelOrder() 메서드 호출 후 상태 업데이트(isLoading 등)가 해당 인스턴스에만 반영되므로, 새로 생성된 인스턴스와 기존 상태가 동기화되지 않습니다. 의도적인 설계라면 명확히 하고, 그렇지 않다면 다음 중 하나로 통일하세요:

  1. 공유 상태: 모든 곳에서 부모의 shared orderViewModel 사용
  2. 독립적 상태: 각 뷰가 필요한 데이터만 @ObservedObject로 받고 새 인스턴스 생성

Text(item.orderNumber)
.font(.system(size: 13, weight: .regular))
.foregroundColor(.gray)
Text(formattedDate(item.createdAt))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

formattedDate 함수가 정의되지 않았습니다.

formattedDate(item.createdAt) 함수를 호출하고 있지만, 이 함수가 현재 파일에서 정의되지 않았습니다. 이는 컴파일 에러를 발생시킬 수 있습니다.

다음을 확인하세요:

  1. formattedDate 함수가 extension이나 다른 파일에 정의되어 있는지 확인
  2. 정의되지 않았다면, 날짜 포맷팅 함수를 추가해야 합니다

다음 스크립트로 함수 정의를 찾을 수 있습니다:

만약 함수가 정의되지 않았다면, 다음과 같이 추가하세요:

// NotificationCardView 외부에 추가
private func formattedDate(_ date: String) -> String {
    let inputFormatter = ISO8601DateFormatter()
    let outputFormatter = DateFormatter()
    outputFormatter.dateFormat = "yyyy.MM.dd HH:mm"
    outputFormatter.locale = Locale(identifier: "ko_KR")
    
    if let date = inputFormatter.date(from: date) {
        return outputFormatter.string(from: date)
    }
    return date
}

🏁 Script executed:

#!/bin/bash
# formattedDate 함수 정의 검색
rg -n 'func formattedDate' --type=swift

Length of output: 359


🏁 Script executed:

#!/bin/bash
# NotificationListView.swift 파일에서 formattedDate 관련 내용 검색
rg -n -C5 'formattedDate' StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift

Length of output: 513


🏁 Script executed:

#!/bin/bash
# NotificationListView.swift 파일에서 import 문 및 함수 정의 확인
rg -n '^(import|func formattedDate|private func|extension)' StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift --type=swift

Length of output: 16


🏁 Script executed:

#!/bin/bash
# ReceiptView.swift의 formattedDate 함수 스코프 확인
rg -n -B2 'func formattedDate\(' StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift --type=swift

Length of output: 189


🏁 Script executed:

#!/bin/bash
# ReceiptView.swift의 formattedDate 함수 정의 주변 상세 확인
rg -n -A10 'func formattedDate\(' StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift --type=swift

Length of output: 940


🏁 Script executed:

#!/bin/bash
# NotificationListView.swift 전체 파일 구조 확인
cat StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift

Length of output: 2968


formattedDate 함수가 NotificationListView.swift에 정의되지 않았습니다. 컴파일 에러가 발생합니다.

line 70에서 formattedDate(item.createdAt) 함수를 호출하고 있지만, 이 함수가 현재 파일에 정의되지 않았습니다.

다음과 같이 NotificationCardView 구조체 외부에 함수를 추가해야 합니다:

private func formattedDate(_ date: String) -> String {
    let inputFormatter = ISO8601DateFormatter()
    let outputFormatter = DateFormatter()
    outputFormatter.dateFormat = "yyyy.MM.dd HH:mm"
    outputFormatter.locale = Locale(identifier: "ko_KR")
    
    if let date = inputFormatter.date(from: date) {
        return outputFormatter.string(from: date)
    }
    return date
}
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift
around line 70, the call to formattedDate(item.createdAt) fails because
formattedDate is not defined; add a private helper function outside the
NotificationCardView struct named formattedDate(_ date: String) -> String that
uses ISO8601DateFormatter to parse the input string, a DateFormatter with format
"yyyy.MM.dd HH:mm" and locale "ko_KR" to format the parsed date, returns the
formatted string when parsing succeeds and returns the original input string on
failure; place this function below the NotificationCardView declaration so the
view can call it.

Comment on lines +26 to +27
notifications = (response.data ?? []).sorted { $0.createdAt > $1.createdAt }
// notifications = response.data!.sorted { $0.createdAt > $1.createdAt }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

주석 처리된 코드를 제거하세요.

Line 26에서 안전한 처리로 수정된 것은 좋습니다. 하지만 Line 27의 주석 처리된 이전 코드는 더 이상 필요하지 않으므로 제거해야 합니다.

다음 diff를 적용하세요:

       notifications = (response.data ?? []).sorted { $0.createdAt > $1.createdAt }
-//            notifications = response.data!.sorted { $0.createdAt > $1.createdAt }
     case .failure(let error):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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):
🤖 Prompt for AI Agents
In
StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift
around lines 26-27, remove the leftover commented-out line "notifications =
response.data!.sorted { $0.createdAt > $1.createdAt }" so only the safe
nil-coalescing/sorted assignment remains; delete the commented code and ensure
the file compiles and formatting is unchanged.

@Yoo-Hyuna Yoo-Hyuna merged commit a47ae58 into dev Nov 8, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant