From 2caaa26737f91c6e8c976333f9708dc620fd4a81 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 16 Mar 2026 23:11:13 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=B0=A9=EC=B6=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Delegate/AppDelegate.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/DevLog/App/Delegate/AppDelegate.swift b/DevLog/App/Delegate/AppDelegate.swift index af8c618..8a0376d 100644 --- a/DevLog/App/Delegate/AppDelegate.swift +++ b/DevLog/App/Delegate/AppDelegate.swift @@ -90,7 +90,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { ) { if let fcmToken = fcmToken { logger.info("FCM token: \(fcmToken)") - NotificationCenter.default.post(name: .fcmToken, object: nil, userInfo: ["fcmToken": fcmToken]) } } } @@ -251,7 +250,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler() } } - -extension Notification.Name { - static let fcmToken = Notification.Name("fcmToken") -} From 271f21b479b8dcfea1c016ed13dd612733c70524 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 10:52:03 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=95=B1=EB=8D=B8=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=AD=ED=95=99=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Delegate/AppDelegate.swift | 128 -------------------------- 1 file changed, 128 deletions(-) diff --git a/DevLog/App/Delegate/AppDelegate.swift b/DevLog/App/Delegate/AppDelegate.swift index 8a0376d..7a4e033 100644 --- a/DevLog/App/Delegate/AppDelegate.swift +++ b/DevLog/App/Delegate/AppDelegate.swift @@ -8,17 +8,10 @@ import UIKit import Firebase import FirebaseAuth -import FirebaseFirestore -import FirebaseMessaging import GoogleSignIn -import Combine -import UserNotifications class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { private let logger = Logger(category: "AppDelegate") - private var store: Firestore { Firestore.firestore() } - private var authStateListenerHandle: AuthStateDidChangeListenerHandle? - private var cancellable: AnyCancellable? func application( _ app: UIApplication, @@ -52,7 +45,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { // Firebase Messaging 설정 Messaging.messaging().delegate = self - observeAuthState() // 앱이 완전 종료되어도, 알림을 통해 앱이 시작된 경우 처리 if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { @@ -80,10 +72,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { logger.error("Failed to register APNs token", error: error) } - func applicationDidBecomeActive(_ application: UIApplication) { - syncBadgeCount() - } - // FCMToken 갱신 func messaging( _ messaging: Messaging, didReceiveRegistrationToken fcmToken: String? @@ -107,122 +95,6 @@ private extension AppDelegate { } } } - - func observeAuthState() { - authStateListenerHandle = Auth.auth().addStateDidChangeListener { [weak self] _, user in - guard let self else { return } - - self.cancellable?.cancel() - - guard user != nil else { - self.updateBadgeCount(0) - return - } - - self.startObservingBadgeCount() - self.syncBadgeCount() - } - } - - func syncBadgeCount() { - Task { @MainActor [weak self] in - guard let self else { return } - guard Auth.auth().currentUser != nil else { - self.updateBadgeCount(0) - return - } - - do { - let unreadNotificationCount = try await self.fetchUnreadNotificationCount() - self.updateBadgeCount(unreadNotificationCount) - } catch { - self.logger.error("Failed to fetch unread notification count", error: error) - } - } - } - - private func startObservingBadgeCount() { - do { - cancellable = try observeUnreadNotificationCount() - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - guard let self else { return } - - if case .failure(let error) = completion { - self.logger.error("Failed to observe unread notification count", error: error) - } - }, - receiveValue: { [weak self] count in - self?.updateBadgeCount(count) - } - ) - } catch { - logger.error("Failed to start observing badge count", error: error) - } - } - - private func fetchUnreadNotificationCount() async throws -> Int { - logger.info("Fetching unread notification count") - - guard let uid = Auth.auth().currentUser?.uid else { - logger.error("User not authenticated") - throw AuthError.notAuthenticated - } - - do { - let snapshot = try await store.collection("users/\(uid)/notifications") - .whereField("isRead", isEqualTo: false) - .getDocuments() - - let unreadNotificationCount = snapshot.documents.count - logger.info("Unread notification count: \(unreadNotificationCount)") - return unreadNotificationCount - } catch { - logger.error("Failed to fetch unread notification count", error: error) - throw error - } - } - - private func observeUnreadNotificationCount() throws -> AnyPublisher { - logger.info("Observing unread notification count") - - guard let uid = Auth.auth().currentUser?.uid else { - logger.error("User not authenticated") - throw AuthError.notAuthenticated - } - - let subject = PassthroughSubject() - let listener = store.collection("users/\(uid)/notifications") - .whereField("isRead", isEqualTo: false) - .addSnapshotListener { [weak self] snapshot, error in - guard let self else { return } - if let error { - self.logger.error("Failed to observe unread notification count", error: error) - subject.send(completion: .failure(error)) - return - } - - guard let snapshot else { return } - - let unreadNotificationCount = snapshot.documents.count - self.logger.info("Observed unread notification count: \(unreadNotificationCount)") - subject.send(unreadNotificationCount) - } - - return subject - .handleEvents(receiveCancel: { listener.remove() }) - .eraseToAnyPublisher() - } - - @MainActor - private func updateBadgeCount(_ count: Int) { - UNUserNotificationCenter.current().setBadgeCount(count) { [weak self] error in - if let error { - self?.logger.error("Failed to update badge count", error: error) - } - } - } } extension AppDelegate: UNUserNotificationCenterDelegate { From 1fa9d72b0ddc6c86eb3731a2e921d4a1e35f7aa6 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 10:54:38 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EC=9D=BD=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20=EA=B0=AF?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=ED=83=AD=EB=B0=94=EC=99=80=20=EC=95=B1=20?= =?UTF-8?q?=EB=B0=B0=EC=A7=80=EB=A1=9C=20=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DomainAssembler.swift | 4 + DevLog/App/RootView.swift | 4 +- .../PushNotificationRepositoryImpl.swift | 5 + .../Protocol/PushNotificationRepository.swift | 1 + .../Fetch/ObserveUnreadPushCountUseCase.swift | 12 +++ .../ObserveUnreadPushCountUseCaseImpl.swift | 22 +++++ .../Service/PushNotificationService.swift | 21 ++++ .../ViewModel/MainViewModel.swift | 99 +++++++++++++++++++ DevLog/UI/Common/MainView.swift | 13 +++ 9 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCase.swift create mode 100644 DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCaseImpl.swift create mode 100644 DevLog/Presentation/ViewModel/MainViewModel.swift diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index 00397f3..30d60f1 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -103,6 +103,10 @@ private extension DomainAssembler { FetchPushNotificationsUseCaseImpl(container.resolve(PushNotificationRepository.self)) } + container.register(ObserveUnreadPushCountUseCase.self) { + ObserveUnreadPushCountUseCaseImpl(container.resolve(PushNotificationRepository.self)) + } + container.register(TogglePushNotificationReadUseCase.self) { TogglePushNotificationReadUseCaseImpl(container.resolve(PushNotificationRepository.self)) } diff --git a/DevLog/App/RootView.swift b/DevLog/App/RootView.swift index c7b7c0b..bbc559e 100644 --- a/DevLog/App/RootView.swift +++ b/DevLog/App/RootView.swift @@ -17,7 +17,9 @@ struct RootView: View { Color(UIColor.systemGroupedBackground).ignoresSafeArea() if let signIn = viewModel.state.signIn { if signIn && !viewModel.state.isFirstLaunch { - MainView() + MainView(viewModel: MainViewModel( + observeUnreadPushCountUseCase: container.resolve(ObserveUnreadPushCountUseCase.self) + )) } else { LoginView(viewModel: LoginViewModel( signInUseCase: container.resolve(SignInUseCase.self), diff --git a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift index e5b7502..34826ba 100644 --- a/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift +++ b/DevLog/Data/Repository/PushNotificationRepositoryImpl.swift @@ -51,6 +51,11 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { .eraseToAnyPublisher() } + func observeUnreadPushCount() throws -> AnyPublisher { + try service.observeUnreadPushCount() + .eraseToAnyPublisher() + } + // 푸시 알림 기록 삭제 func deleteNotification(_ notificationID: String) async throws { try await service.deleteNotification(notificationID) diff --git a/DevLog/Domain/Protocol/PushNotificationRepository.swift b/DevLog/Domain/Protocol/PushNotificationRepository.swift index 4c689b6..c58522f 100644 --- a/DevLog/Domain/Protocol/PushNotificationRepository.swift +++ b/DevLog/Domain/Protocol/PushNotificationRepository.swift @@ -20,6 +20,7 @@ protocol PushNotificationRepository { _ query: PushNotificationQuery, limit: Int ) throws -> AnyPublisher + func observeUnreadPushCount() throws -> AnyPublisher func deleteNotification(_ notificationID: String) async throws func undoDeleteNotification(_ notificationID: String) async throws func toggleNotificationRead(_ todoId: String) async throws diff --git a/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCase.swift b/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCase.swift new file mode 100644 index 0000000..795d4cc --- /dev/null +++ b/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCase.swift @@ -0,0 +1,12 @@ +// +// ObserveUnreadPushCountUseCase.swift +// DevLog +// +// Created by opfic on 3/17/26. +// + +import Combine + +protocol ObserveUnreadPushCountUseCase { + func execute() throws -> AnyPublisher +} diff --git a/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCaseImpl.swift b/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCaseImpl.swift new file mode 100644 index 0000000..75fb4a8 --- /dev/null +++ b/DevLog/Domain/UseCase/PushNotification/Fetch/ObserveUnreadPushCountUseCaseImpl.swift @@ -0,0 +1,22 @@ +// +// ObserveUnreadPushCountUseCaseImpl.swift +// DevLog +// +// Created by opfic on 3/17/26. +// + +import Combine + +final class ObserveUnreadPushCountUseCaseImpl: ObserveUnreadPushCountUseCase { + private let repository: PushNotificationRepository + + init(_ repository: PushNotificationRepository) { + self.repository = repository + } + + func execute() throws -> AnyPublisher { + try repository.observeUnreadPushCount() + .removeDuplicates() + .eraseToAnyPublisher() + } +} diff --git a/DevLog/Infra/Service/PushNotificationService.swift b/DevLog/Infra/Service/PushNotificationService.swift index 66d7eb7..bdf5190 100644 --- a/DevLog/Infra/Service/PushNotificationService.swift +++ b/DevLog/Infra/Service/PushNotificationService.swift @@ -178,6 +178,27 @@ final class PushNotificationService { .eraseToAnyPublisher() } + func observeUnreadPushCount() throws -> AnyPublisher { + guard let uid = Auth.auth().currentUser?.uid else { throw AuthError.notAuthenticated } + + let subject = PassthroughSubject() + let listener = store.collection("users/\(uid)/notifications") + .whereField("isRead", isEqualTo: false) + .addSnapshotListener { snapshot, error in + if let error { + subject.send(completion: .failure(error)) + return + } + + guard let snapshot else { return } + subject.send(snapshot.documents.count) + } + + return subject + .handleEvents(receiveCancel: { listener.remove() }) + .eraseToAnyPublisher() + } + /// 푸시 알림 기록 삭제 func deleteNotification(_ notificationID: String) async throws { do { diff --git a/DevLog/Presentation/ViewModel/MainViewModel.swift b/DevLog/Presentation/ViewModel/MainViewModel.swift new file mode 100644 index 0000000..c76ca7e --- /dev/null +++ b/DevLog/Presentation/ViewModel/MainViewModel.swift @@ -0,0 +1,99 @@ +// +// MainViewModel.swift +// DevLog +// +// Created by opfic on 3/17/26. +// + +import Foundation +import Combine +import UserNotifications + +@Observable +final class MainViewModel: Store { + struct State: Equatable { + var unreadPushCount = 0 + var showAlert = false + var alertTitle = "" + var alertMessage = "" + } + + enum Action { + case setUnreadPushCount(Int) + case setAlert(Bool) + } + + enum SideEffect { + case updateBadgeCount(Int) + } + + private(set) var state = State() + private var cancellables = Set() + private let observeUnreadPushCountUseCase: ObserveUnreadPushCountUseCase + + init( + observeUnreadPushCountUseCase: ObserveUnreadPushCountUseCase + ) { + self.observeUnreadPushCountUseCase = observeUnreadPushCountUseCase + observeUnreadPushCount() + } + + func reduce(with action: Action) -> [SideEffect] { + var state = self.state + var sideEffects: [SideEffect] = [] + + switch action { + case .setUnreadPushCount(let count): + state.unreadPushCount = count + sideEffects = [.updateBadgeCount(count)] + case .setAlert(let isPresented): + setAlert(&state, isPresented: isPresented) + } + + if self.state != state { self.state = state } + return sideEffects + } + + func run(_ effect: SideEffect) { + switch effect { + case .updateBadgeCount(let count): + updateBadgeCount(count) + } + } +} + +private extension MainViewModel { + func setAlert( + _ state: inout State, + isPresented: Bool + ) { + state.alertTitle = "오류" + state.alertMessage = "알림 배지를 불러오는 중 문제가 발생했습니다." + state.showAlert = isPresented + } + + func observeUnreadPushCount() { + do { + try observeUnreadPushCountUseCase.execute() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [weak self] completion in + guard let self else { return } + if case .failure = completion { + self.send(.setAlert(true)) + } + }, + receiveValue: { [weak self] count in + self?.send(.setUnreadPushCount(count)) + } + ) + .store(in: &cancellables) + } catch { + send(.setAlert(true)) + } + } + + func updateBadgeCount(_ count: Int) { + UNUserNotificationCenter.current().setBadgeCount(count) { _ in } + } +} diff --git a/DevLog/UI/Common/MainView.swift b/DevLog/UI/Common/MainView.swift index c26676c..a87fe18 100644 --- a/DevLog/UI/Common/MainView.swift +++ b/DevLog/UI/Common/MainView.swift @@ -9,6 +9,7 @@ import SwiftUI struct MainView: View { @Environment(\.diContainer) var container: DIContainer + @State var viewModel: MainViewModel var body: some View { TabView { @@ -47,6 +48,7 @@ struct MainView: View { Image(systemName: "bell.fill") Text("알림") } + .badge(viewModel.state.unreadPushCount) ProfileView(viewModel: ProfileViewModel( fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self), fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), @@ -59,5 +61,16 @@ struct MainView: View { Text("프로필") } } + .alert( + viewModel.state.alertTitle, + isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setAlert($0)) } + ) + ) { + Button("확인", role: .cancel) { } + } message: { + Text(viewModel.state.alertMessage) + } } } From 7e472ed6dbdc6c8ac3e3e480e10f8998689bdbda Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 10:58:06 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix:=20=EB=A6=AC=EC=8A=A4=EB=84=88?= =?UTF-8?q?=EC=97=90=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EC=9E=84=EC=8B=9C=20=ED=91=B8=EC=8B=9C=EC=95=8C?= =?UTF-8?q?=EB=9E=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=9D=B8=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EB=A5=BC=20=EA=B1=B8=EB=9F=AC=EB=82=B4=EC=96=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=98=88=EC=A0=95=EC=9D=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B0=AF=EC=88=98=EB=8A=94=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Infra/Service/PushNotificationService.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DevLog/Infra/Service/PushNotificationService.swift b/DevLog/Infra/Service/PushNotificationService.swift index bdf5190..dd03ae3 100644 --- a/DevLog/Infra/Service/PushNotificationService.swift +++ b/DevLog/Infra/Service/PushNotificationService.swift @@ -191,7 +191,10 @@ final class PushNotificationService { } guard let snapshot else { return } - subject.send(snapshot.documents.count) + let unreadPushCount = snapshot.documents.filter { document in + !(document.data()[Key.deletingAt.rawValue] is Timestamp) + }.count + subject.send(unreadPushCount) } return subject From b0fea8a88ee318e81cf354e47a3487d4dc89225d Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 11:32:25 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EC=95=B1=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=EC=97=90=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=9E=8C=20=EB=B0=B0?= =?UTF-8?q?=EC=A7=80=EA=B0=80=20=EB=96=A0=EC=9E=88=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/RootView.swift | 5 +---- .../Presentation/ViewModel/RootViewModel.swift | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/DevLog/App/RootView.swift b/DevLog/App/RootView.swift index bbc559e..53e2c68 100644 --- a/DevLog/App/RootView.swift +++ b/DevLog/App/RootView.swift @@ -27,10 +27,7 @@ struct RootView: View { sessionUseCase: container.resolve(AuthSessionUseCase.self)) ) .onAppear { - if viewModel.state.isFirstLaunch { - viewModel.send(.setFirstLaunch(false)) - viewModel.send(.signOutAuto) - } + viewModel.send(.onAppear) } } } else { diff --git a/DevLog/Presentation/ViewModel/RootViewModel.swift b/DevLog/Presentation/ViewModel/RootViewModel.swift index 1f9b09b..645ed29 100644 --- a/DevLog/Presentation/ViewModel/RootViewModel.swift +++ b/DevLog/Presentation/ViewModel/RootViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Combine +import UserNotifications @Observable final class RootViewModel: Store { @@ -21,6 +22,7 @@ final class RootViewModel: Store { } enum Action { + case onAppear case setAlert(Bool) case networkStatusChanged(Bool) case setFirstLaunch(Bool) @@ -30,6 +32,7 @@ final class RootViewModel: Store { } enum SideEffect { + case clearApplicationBadgeCount case signOut } @@ -70,6 +73,13 @@ final class RootViewModel: Store { var effects: [SideEffect] = [] switch action { + case .onAppear: + effects = [.clearApplicationBadgeCount] + if state.isFirstLaunch { + state.isFirstLaunch = false + updateFirstLaunchUseCase.execute(false) + effects.append(.signOut) + } case .setAlert(let isPresented): setAlert(&state, isPresented: isPresented) case .networkStatusChanged(let isConnected): @@ -95,6 +105,8 @@ final class RootViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .clearApplicationBadgeCount: + clearApplicationBadgeCount() case .signOut: Task { try? await signOutUseCase.execute() @@ -107,6 +119,10 @@ final class RootViewModel: Store { // MARK: - Helper Methods private extension RootViewModel { + func clearApplicationBadgeCount() { + UNUserNotificationCenter.current().setBadgeCount(0) { _ in } + } + func setAlert( _ state: inout State, isPresented: Bool From 38cd53fb2aa9c60325c75dc2a949a9b49f31dd1a Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 11:37:53 +0900 Subject: [PATCH 06/11] =?UTF-8?q?test:=20=EC=BD=94=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=EC=9A=A9=20AGENTS.md=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..577d329 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +## Review Guidelines + +- Write all review comments in Korean. +- Keep review comments concise and high-signal. +- Prioritize findings about bugs, performance, and readability. +- Do not explain obvious, trivial, or low-signal issues. +- When useful, begin the review with a short summary of the main changes. +- Focus on actionable feedback rather than broad commentary. From 98dd93f09f788ed8530261fcbfe64e65e6ac5383 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 11:53:53 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 577d329..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,8 +0,0 @@ -## Review Guidelines - -- Write all review comments in Korean. -- Keep review comments concise and high-signal. -- Prioritize findings about bugs, performance, and readability. -- Do not explain obvious, trivial, or low-signal issues. -- When useful, begin the review with a short summary of the main changes. -- Focus on actionable feedback rather than broad commentary. From 5fa5a1ec7c7c07cf4d008a11a4c30d29898f5f3e Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 11:54:26 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gemini/styleguide.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index 6db7107..d4de7ea 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -1,2 +1,9 @@ -# 지침사항 -당신은 iOS 수석 개발자 입니다. 당신은 한국인 이므로 코드 리뷰 및 요약을 한국어로 해야 합니다. +## Review Guidelines + +- Write all review comments in Korean. +- Keep review comments concise and high-signal. +- Prioritize findings about bugs, performance, and readability. +- Do not explain obvious, trivial, or low-signal issues. +- When useful, begin the review with a short summary of the main changes. +- Focus on actionable feedback rather than broad commentary. + From 5f11c56453a7852070ef9ad4d3e7f7872a64a66e Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 12:04:50 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EB=B7=B0=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=83=9D=EC=84=B1=EB=90=A0=20=EB=95=8C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EA=B0=80=20=ED=98=B8=EC=B6=9C=EB=90=98=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/MainViewModel.swift | 11 ++++++++++- DevLog/UI/Common/MainView.swift | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/DevLog/Presentation/ViewModel/MainViewModel.swift b/DevLog/Presentation/ViewModel/MainViewModel.swift index c76ca7e..9a19e5d 100644 --- a/DevLog/Presentation/ViewModel/MainViewModel.swift +++ b/DevLog/Presentation/ViewModel/MainViewModel.swift @@ -19,23 +19,25 @@ final class MainViewModel: Store { } enum Action { + case onAppear case setUnreadPushCount(Int) case setAlert(Bool) } enum SideEffect { + case observeUnreadPushCount case updateBadgeCount(Int) } private(set) var state = State() private var cancellables = Set() + private var isObservingUnreadPushCount = false private let observeUnreadPushCountUseCase: ObserveUnreadPushCountUseCase init( observeUnreadPushCountUseCase: ObserveUnreadPushCountUseCase ) { self.observeUnreadPushCountUseCase = observeUnreadPushCountUseCase - observeUnreadPushCount() } func reduce(with action: Action) -> [SideEffect] { @@ -43,6 +45,11 @@ final class MainViewModel: Store { var sideEffects: [SideEffect] = [] switch action { + case .onAppear: + if !isObservingUnreadPushCount { + isObservingUnreadPushCount = true + sideEffects = [.observeUnreadPushCount] + } case .setUnreadPushCount(let count): state.unreadPushCount = count sideEffects = [.updateBadgeCount(count)] @@ -56,6 +63,8 @@ final class MainViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .observeUnreadPushCount: + observeUnreadPushCount() case .updateBadgeCount(let count): updateBadgeCount(count) } diff --git a/DevLog/UI/Common/MainView.swift b/DevLog/UI/Common/MainView.swift index a87fe18..aabecd7 100644 --- a/DevLog/UI/Common/MainView.swift +++ b/DevLog/UI/Common/MainView.swift @@ -61,6 +61,9 @@ struct MainView: View { Text("프로필") } } + .onAppear { + viewModel.send(.onAppear) + } .alert( viewModel.state.alertTitle, isPresented: Binding( From 27fb444d174011035665687eb95cbc1de1e9b284 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 12:07:10 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/MainViewModel.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/DevLog/Presentation/ViewModel/MainViewModel.swift b/DevLog/Presentation/ViewModel/MainViewModel.swift index 9a19e5d..25b786b 100644 --- a/DevLog/Presentation/ViewModel/MainViewModel.swift +++ b/DevLog/Presentation/ViewModel/MainViewModel.swift @@ -30,6 +30,7 @@ final class MainViewModel: Store { } private(set) var state = State() + private let logger = Logger(category: "MainViewModel") private var cancellables = Set() private var isObservingUnreadPushCount = false private let observeUnreadPushCountUseCase: ObserveUnreadPushCountUseCase @@ -88,7 +89,8 @@ private extension MainViewModel { .sink( receiveCompletion: { [weak self] completion in guard let self else { return } - if case .failure = completion { + if case .failure(let error) = completion { + logger.error("Failed to observe unread push count", error: error) self.send(.setAlert(true)) } }, @@ -98,11 +100,16 @@ private extension MainViewModel { ) .store(in: &cancellables) } catch { + logger.error("Failed to start observing unread push count", error: error) send(.setAlert(true)) } } func updateBadgeCount(_ count: Int) { - UNUserNotificationCenter.current().setBadgeCount(count) { _ in } + UNUserNotificationCenter.current().setBadgeCount(count) { [weak self] error in + if let error { + self?.logger.error("Failed to update application badge count", error: error) + } + } } } From 21073cd7e9451d9f8850d594c33d805feb010bf1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 17 Mar 2026 12:17:12 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20error=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=EC=9D=B4=20=ED=8F=AC=ED=95=A8=EB=90=98=EC=97=88?= =?UTF-8?q?=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EB=B9=8C=EB=93=9C=EA=B0=80=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=ED=95=B4=EB=8F=84=20CI=EA=B0=80=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=ED=96=88=EB=8B=A4=EA=B3=A0=20=EB=9C=A8=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e200b8..0b768b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -206,15 +206,6 @@ jobs: XC_STATUS=${PIPESTATUS[0]} set -e - if [ -f build.log ]; then - echo "== error: lines ==" - if grep -i "error:" build.log; then - if [ "$XC_STATUS" -eq 0 ]; then - XC_STATUS=1 - fi - fi - fi - exit $XC_STATUS - name: Comment build failure on PR @@ -228,9 +219,9 @@ jobs: if (fs.existsSync(path)) { const log = fs.readFileSync(path, 'utf8'); const lines = log.split(/\r?\n/); - const errorLines = lines.filter((line) => /error:/i.test(line)); + const errorLines = lines.filter((line) => /^(.*?):(\d+):(\d+):\s+error:/i.test(line)); if (errorLines.length > 0) { - body += "Lines containing 'error:':\n\n```\n" + errorLines.join('\n') + '\n```\n'; + body += "Compiler error lines:\n\n```\n" + errorLines.join('\n') + '\n```\n'; const repoRoot = process.env.GITHUB_WORKSPACE || process.cwd(); const pathMod = require('path'); @@ -258,7 +249,7 @@ jobs: body += "\nCode excerpts:\n\n```\n" + snippets.join('\n\n') + "\n```\n"; } } else { - body += "No lines containing 'error:' were found in build.log."; + body += "No compiler-style error diagnostics were found in build.log."; } } else { body += 'build.log not found.';