-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/notification #472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: refactor/home
Are you sure you want to change the base?
Feat/notification #472
Changes from all commits
43c051f
56c6fde
74f80b6
59c315f
556b75f
d64df27
e19f6f6
5b7f41b
b2c8b91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // | ||
| // NotificationHistoryError.swift | ||
| // koin | ||
| // | ||
| // Created by 홍기정 on 7/7/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| struct NotificationHistoryError: Error { | ||
| let message: String | ||
|
|
||
| static let calendarDidFail = Self.init(message: "캘린더 오류 발생") | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,145 @@ | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // NotificationContainer.swift | ||||||||||||||||||||||||||||||||||||||||
| // koin | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // Created by 홍기정 on 7/6/26. | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import SwiftData | ||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| protocol NotificationHistoryService { | ||||||||||||||||||||||||||||||||||||||||
| func insert(record: NotificationRecord) async throws | ||||||||||||||||||||||||||||||||||||||||
| func fetchAll() async throws -> [NotificationRecord] | ||||||||||||||||||||||||||||||||||||||||
| func markAsRead(messageId: String) async throws | ||||||||||||||||||||||||||||||||||||||||
| func markAllAsRead() async throws | ||||||||||||||||||||||||||||||||||||||||
| func delete(messageId: String) async throws | ||||||||||||||||||||||||||||||||||||||||
| func deleteAll() async throws | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| final class DefaultNotificationHistoryService: NotificationHistoryService { | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Properties | ||||||||||||||||||||||||||||||||||||||||
| private let container: ModelContainer? | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Initializer | ||||||||||||||||||||||||||||||||||||||||
| init() { | ||||||||||||||||||||||||||||||||||||||||
| container = try? ModelContainer( | ||||||||||||||||||||||||||||||||||||||||
| for: NotificationRecord.self, | ||||||||||||||||||||||||||||||||||||||||
| configurations: .init(groupContainer: .identifier("group.com.bcsdlab.koin")) | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Create | ||||||||||||||||||||||||||||||||||||||||
| func insert(record: NotificationRecord) async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| switch record.category { | ||||||||||||||||||||||||||||||||||||||||
| case .shop, .dining, .keyword, .chat, .callvan, .callvanChat: | ||||||||||||||||||||||||||||||||||||||||
| try? await MainActor.run { [weak container] in | ||||||||||||||||||||||||||||||||||||||||
| container?.mainContext.insert(record) | ||||||||||||||||||||||||||||||||||||||||
| try container?.mainContext.save() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🩹 Proposed fix switch record.category {
case .shop, .dining, .keyword, .chat, .callvan, .callvanChat:
- try? await MainActor.run { [weak container] in
- container?.mainContext.insert(record)
- try container?.mainContext.save()
- }
+ try await MainActor.run {
+ container.mainContext.insert(record)
+ try container.mainContext.save()
+ }
default:
break
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Read | ||||||||||||||||||||||||||||||||||||||||
| func fetchAll() async throws -> [NotificationRecord] { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await deleteExpiredNotifications() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| var descriptor = FetchDescriptor<NotificationRecord>( | ||||||||||||||||||||||||||||||||||||||||
| sortBy: [SortDescriptor(\.createdAt, order: .reverse)] | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| descriptor.fetchLimit = .max | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return try container.mainContext.fetch(descriptor) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Update | ||||||||||||||||||||||||||||||||||||||||
| func markAsRead(messageId: String) async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| var descriptor = FetchDescriptor<NotificationRecord>( | ||||||||||||||||||||||||||||||||||||||||
| predicate: #Predicate { notification in | ||||||||||||||||||||||||||||||||||||||||
| notification.messageId == messageId | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| descriptor.fetchLimit = 1 | ||||||||||||||||||||||||||||||||||||||||
| if let notification = try container.mainContext.fetch(descriptor).first { | ||||||||||||||||||||||||||||||||||||||||
| notification.isRead = true | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.save() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func markAllAsRead() async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| var descriptor = FetchDescriptor<NotificationRecord>() | ||||||||||||||||||||||||||||||||||||||||
| descriptor.fetchLimit = .max | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.enumerate(descriptor) { notification in | ||||||||||||||||||||||||||||||||||||||||
| notification.isRead = true | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.save() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // MARK: - Delete | ||||||||||||||||||||||||||||||||||||||||
| func delete(messageId: String) async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.delete(model: NotificationRecord.self, where: #Predicate { notification in | ||||||||||||||||||||||||||||||||||||||||
| notification.messageId == messageId | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.save() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func deleteAll() async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.delete(model: NotificationRecord.self) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
Per SwiftData's documented behavior, batch deletion via 🩹 Proposed fix try await MainActor.run {
try container.mainContext.delete(model: NotificationRecord.self)
+ try container.mainContext.save()
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| extension DefaultNotificationHistoryService { | ||||||||||||||||||||||||||||||||||||||||
| private func deleteExpiredNotifications() async throws { | ||||||||||||||||||||||||||||||||||||||||
| guard let container else { | ||||||||||||||||||||||||||||||||||||||||
| throw SwiftDataError.loadIssueModelContainer | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| guard let expirationDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) else { | ||||||||||||||||||||||||||||||||||||||||
| throw NotificationHistoryError.calendarDidFail | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.delete(model: NotificationRecord.self, where: #Predicate { notification in | ||||||||||||||||||||||||||||||||||||||||
| notification.createdAt < expirationDate | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| try container.mainContext.save() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+128
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Retention window is 7 days, but PR intent states 14 days. The commit summary describes "deleting notification records older than 14 days when reading them," but this cleanup uses 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // NotificationRecord.swift | ||||||||||||||||||||||||||||||||||||||
| // koin | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // Created by 홍기정 on 7/6/26. | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import SwiftData | ||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @Model | ||||||||||||||||||||||||||||||||||||||
| final class NotificationRecord: Sendable { | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== File list ==\n'
git ls-files 'Common/NotificationHistory/*' 'Common/*Notification*' '*/NotificationService.swift' '*/NotificationHistoryService.swift'
printf '\n== Outline: NotificationRecord.swift ==\n'
ast-grep outline Common/NotificationHistory/NotificationRecord.swift --view expanded || true
printf '\n== NotificationRecord.swift ==\n'
cat -n Common/NotificationHistory/NotificationRecord.swift
printf '\n== NotificationService.swift matches ==\n'
rg -n "NotificationRecord|Task\s*\{|MainActor\.run|insert\(" Common/NotificationHistory NotificationService.swift Common -g '*.swift' || true
printf '\n== NotificationHistoryService.swift matches ==\n'
rg -n "NotificationRecord|Task\s*\{|MainActor\.run|insert\(" Common/NotificationHistory NotificationHistoryService.swift Common -g '*.swift' || trueRepository: BCSDLab/KOIN_iOS Length of output: 11592 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== Relevant Swift files ==\n'
fd -a 'Notification(Service|HistoryService|Record)\.swift$' . || true
printf '\n== Search for Sendable / `@Model` / unique ==\n'
rg -n "`@Model`|Sendable|`@Attribute`\\(\\.unique\\)|messageId|PersistentIdentifier" Common -g '*.swift' || trueRepository: BCSDLab/KOIN_iOS Length of output: 1546 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== NotificationHistoryService.swift (1-220) ==\n'
cat -n Common/NotificationHistory/NotificationHistoryService.swift
printf '\n== NotificationService.swift (1-220) ==\n'
cat -n NotificationService/NotificationService.swiftRepository: BCSDLab/KOIN_iOS Length of output: 10492 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [
Path('Common/NotificationHistory/NotificationHistoryService.swift'),
Path('NotificationService/NotificationService.swift'),
]:
print(f"\n== {path} ==")
for i, line in enumerate(path.read_text().splitlines(), 1):
if 1 <= i <= 220:
print(f"{i:4d}: {line}")
PYRepository: BCSDLab/KOIN_iOS Length of output: 10278 Drop
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| var body: String | ||||||||||||||||||||||||||||||||||||||
| var title: String | ||||||||||||||||||||||||||||||||||||||
| var category: AppPath | ||||||||||||||||||||||||||||||||||||||
| var schemeUri: String | ||||||||||||||||||||||||||||||||||||||
| var messageId: String | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var isRead: Bool = false | ||||||||||||||||||||||||||||||||||||||
| var createdAt: Date = Date() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git ls-files | rg 'Common/NotificationHistory|NotificationItem|insert\(record:|NotificationRecord'Repository: BCSDLab/KOIN_iOS Length of output: 315 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== NotificationRecord.swift ==\n'
cat -n Common/NotificationHistory/NotificationRecord.swift
printf '\n== NotificationHistoryService.swift ==\n'
cat -n Common/NotificationHistory/NotificationHistoryService.swift
printf '\n== NotificationItem.swift ==\n'
cat -n Koin/Domain/Model/NewHome/NotificationItem.swiftRepository: BCSDLab/KOIN_iOS Length of output: 9785 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "messageId|NotificationRecord|insert\\(record:|delete\\(messageId:|markAsRead\\(messageId:" Common Koin -g '!**/build/**' -g '!**/DerivedData/**'Repository: BCSDLab/KOIN_iOS Length of output: 3411 Make 🛡️ Proposed fix final class NotificationRecord: Sendable {
var body: String
var title: String
var category: AppPath
var schemeUri: String
- var messageId: String
+ `@Attribute`(.unique) var messageId: String📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| init( | ||||||||||||||||||||||||||||||||||||||
| body: String, | ||||||||||||||||||||||||||||||||||||||
| title: String, | ||||||||||||||||||||||||||||||||||||||
| category: AppPath, | ||||||||||||||||||||||||||||||||||||||
| schemeUri: String, | ||||||||||||||||||||||||||||||||||||||
| messageId: String | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| self.body = body | ||||||||||||||||||||||||||||||||||||||
| self.title = title | ||||||||||||||||||||||||||||||||||||||
| self.category = category | ||||||||||||||||||||||||||||||||||||||
| self.schemeUri = schemeUri | ||||||||||||||||||||||||||||||||||||||
| self.messageId = messageId | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // | ||
| // DefaultNotificationHistoryRepository.swift | ||
| // koin | ||
| // | ||
| // Created by 홍기정 on 7/7/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| final class DefaultNotificationHistoryRepository: NotificationHistoryRepository { | ||
|
|
||
| private let service: NotificationHistoryService | ||
|
|
||
| init(service: NotificationHistoryService) { | ||
| self.service = service | ||
| } | ||
|
|
||
| func insert(record: NotificationRecord) async throws { | ||
| try await service.insert(record: record) | ||
| } | ||
|
|
||
| func fetchAll() async throws -> [NotificationItem] { | ||
| try await service.fetchAll() | ||
| .compactMap { | ||
| NotificationItem.init(from: $0) | ||
| } | ||
| } | ||
|
|
||
| func deleteAll() async throws { | ||
| try await service.deleteAll() | ||
| } | ||
|
|
||
| func delete(id: String) async throws { | ||
| try await service.delete(messageId: id) | ||
| } | ||
|
|
||
| func markAsRead(id: String) async throws { | ||
| try await service.markAsRead(messageId: id) | ||
| } | ||
|
|
||
| func makrAllAsRead() async throws { | ||
| try await service.markAllAsRead() | ||
| } | ||
|
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Typo: Naming typo in the public repository API. Since this must match the 🤖 Prompt for AI Agents |
||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,8 +8,8 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct NotificationItem { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let id: Int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let isRead: Bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let id: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var isRead: Bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let icon: ImageAsset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let appPath: AppPath | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let uri: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -18,7 +18,7 @@ struct NotificationItem { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let dateText: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| init( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: Int, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isRead: Bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon: ImageAsset, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appPath: AppPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,3 +37,59 @@ struct NotificationItem { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.dateText = dateText | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extension NotificationItem { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| init?(from record: NotificationRecord) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let icon = NotificationItem.icon(for: record.category) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.id = record.messageId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.isRead = record.isRead | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.icon = icon | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.appPath = record.category | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.uri = record.schemeUri | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.title = record.title | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.content = record.body | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.dateText = NotificationItem.dateText(for: record.createdAt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static func icon(for appPath: AppPath) -> ImageAsset? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch appPath { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .shop: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationShop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .dining: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationDining | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .keyword: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationLostItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .chat: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationChat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .callvan: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationCallVan | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .callvanChat: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ImageAsset.notificationChat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Notifications for
Consider either providing a fallback icon for the remaining cases, or making the switch exhaustive (dropping ♻️ Example fix static func icon(for appPath: AppPath) -> ImageAsset? {
switch appPath {
case .shop:
return ImageAsset.notificationShop
case .dining:
return ImageAsset.notificationDining
case .keyword:
return ImageAsset.notificationLostItem
case .chat:
return ImageAsset.notificationChat
case .callvan:
return ImageAsset.notificationCallVan
case .callvanChat:
return ImageAsset.notificationChat
- default:
- return nil
+ case .home, .login, .club, .timeTable:
+ return ImageAsset.notificationDefault // or an appropriate fallback
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static func dateText(for createdAt: Date) -> String { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let now = Date() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let compareComponents = Calendar.current.dateComponents( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [.day, .hour, .minute], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from: createdAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to: now | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let day = compareComponents.day, 1 <= day { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "\(day)일 전" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let hour = compareComponents.hour, 1 <= hour { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "\(hour)시간 전" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let minute = compareComponents.minute, 1 <= minute { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "\(minute)분 전" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "지금" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // | ||
| // NotificationHistoryRepository.swift | ||
| // koin | ||
| // | ||
| // Created by 홍기정 on 7/7/26. | ||
| // | ||
|
|
||
| import Combine | ||
|
|
||
| protocol NotificationHistoryRepository { | ||
| func fetchAll() async throws -> [NotificationItem] | ||
| func deleteAll() async throws | ||
| func delete(id: String) async throws | ||
| func markAsRead(id: String) async throws | ||
| func makrAllAsRead() async throws | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Typo: This new protocol method name is misspelled. It's consistently used in 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // | ||
| // DeleteNotificationHistoryUseCase.swift | ||
| // koin | ||
| // | ||
| // Created by 홍기정 on 7/7/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| protocol DeleteNotificationHistoryUseCase { | ||
| func delete(id: String) async throws | ||
| func deleteAll() async throws | ||
| } | ||
|
|
||
| final class DefaultDeleteNotificationHistoryUseCase: DeleteNotificationHistoryUseCase { | ||
|
|
||
| private let repository: NotificationHistoryRepository | ||
|
|
||
| init(repository: NotificationHistoryRepository) { | ||
| self.repository = repository | ||
| } | ||
|
|
||
| func delete(id: String) async throws { | ||
| try await repository.delete(id: id) | ||
| } | ||
|
|
||
| func deleteAll() async throws { | ||
| try await repository.deleteAll() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: BCSDLab/KOIN_iOS
Length of output: 1046
🏁 Script executed:
Repository: BCSDLab/KOIN_iOS
Length of output: 245
🌐 Web query:
Swift Error localizedDescription non LocalizedError generic system message documentation💡 Result:
In Swift, the
localizedDescriptionproperty on theErrorprotocol is not a customization point [1][2]. Because it is defined as an extension on theErrorprotocol within the Foundation framework rather than a requirement of the protocol itself, it cannot be overridden directly [3][4]. When you accesslocalizedDescriptionon an error, the default implementation performs a bridge toNSError[3][4]. If your error type does not conform toLocalizedError, this default implementation provides a generic system message (typically indicating that the error domain and code do not have a localized description) [2][3]. To provide a custom, localized message thatlocalizedDescriptionwill return, your error type must conform to theLocalizedErrorprotocol [2][5]. 1. Conforming to LocalizedError: When you conform your type toLocalizedError, you implement theerrorDescriptionproperty [6][7]. 2. How it works: The Foundation framework's bridging logic checks if an error conforms toLocalizedError[6][8]. If it does, and iferrorDescriptionreturns a string, that string is used to populate theNSLocalizedDescriptionKeyin the underlyingNSError's user info dictionary, whichlocalizedDescriptionthen returns [6][8][7]. 3. Default Behavior: If your type does not conform toLocalizedError, or if theerrorDescriptionproperty returnsnil,localizedDescriptionfalls back to the generic system behavior provided byNSError[2]. Therefore, attempting to overridelocalizedDescriptiondirectly in your error type will not work as intended for dynamic dispatch; you must instead implement theerrorDescriptionrequirement defined by theLocalizedErrorprotocol [3][5].Citations:
🏁 Script executed:
Repository: BCSDLab/KOIN_iOS
Length of output: 154
🏁 Script executed:
Repository: BCSDLab/KOIN_iOS
Length of output: 12862
🏁 Script executed:
Repository: BCSDLab/KOIN_iOS
Length of output: 12862
Adopt
LocalizedErrorso the toast showsmessage.NotificationViewModel.loadNotifications()surfaces this error witherror.localizedDescription, butNotificationHistoryErroronly conforms toError, so the toast falls back to a generic system message instead of"캘린더 오류 발생".🩹 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents