Skip to content

Commit

Permalink
Merge pull request #135 from WhereAreYouPJ/feat/notification
Browse files Browse the repository at this point in the history
fix(#121): 1차 QA 버그사항 수정 - 알림 읽음 상태 관리 구현
  • Loading branch information
juhee-dev authored Feb 26, 2025
2 parents 9dd63ca + 60ab762 commit 1cfe8c4
Show file tree
Hide file tree
Showing 23 changed files with 467 additions and 80 deletions.
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 456e82e4e2b1a658b67bf057c00b1e04330276dd

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
16 changes: 16 additions & 0 deletions Where_Are_You.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@
D5DB6C6D2CDA297300D89360 /* ScheduleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB6C6C2CDA297300D89360 /* ScheduleDetailView.swift */; };
D5DB6C6F2CDA2A0900D89360 /* ScheduleDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB6C6E2CDA2A0900D89360 /* ScheduleDetailViewModel.swift */; };
D5DCBF342C7435E2003F0246 /* SearchFriendsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DCBF332C7435E2003F0246 /* SearchFriendsView.swift */; };
D5DCF8C02D69C14E0045030C /* NotificationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DCF8BF2D69C14E0045030C /* NotificationStorage.swift */; };
D5DCF8C42D69CA770045030C /* NotificationBadgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DCF8C32D69CA770045030C /* NotificationBadgeViewModel.swift */; };
D5E3D7D82C63BF730041B28D /* CreateScheduleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E3D7D72C63BF730041B28D /* CreateScheduleViewModel.swift */; };
D5FC8ED42CE31151000A08C2 /* PostScheduleResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FC8ED32CE31151000A08C2 /* PostScheduleResponse.swift */; };
D5FFF2A22D6734830067C3FE /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FFF2A12D6734830067C3FE /* LocationManager.swift */; };
Expand Down Expand Up @@ -701,6 +703,8 @@
D5DB6C6C2CDA297300D89360 /* ScheduleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailView.swift; sourceTree = "<group>"; };
D5DB6C6E2CDA2A0900D89360 /* ScheduleDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailViewModel.swift; sourceTree = "<group>"; };
D5DCBF332C7435E2003F0246 /* SearchFriendsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFriendsView.swift; sourceTree = "<group>"; };
D5DCF8BF2D69C14E0045030C /* NotificationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStorage.swift; sourceTree = "<group>"; };
D5DCF8C32D69CA770045030C /* NotificationBadgeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBadgeViewModel.swift; sourceTree = "<group>"; };
D5E3D7D72C63BF730041B28D /* CreateScheduleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleViewModel.swift; sourceTree = "<group>"; };
D5FC8ED32CE31151000A08C2 /* PostScheduleResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostScheduleResponse.swift; sourceTree = "<group>"; };
D5FFF2A12D6734830067C3FE /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1360,6 +1364,7 @@
99D5060B2C08A77900A6CF7B /* Data */ = {
isa = PBXGroup;
children = (
D5DCF8BE2D69C1350045030C /* Storage */,
9910B4302C76BE2000DBE375 /* Photo */,
990F57942C27EE3600F6E1AD /* Models */,
9950C64F2C103031003155A7 /* Network */,
Expand Down Expand Up @@ -1619,6 +1624,7 @@
isa = PBXGroup;
children = (
D541BBEF2D32C238001C4C0C /* NotificationViewModel.swift */,
D5DCF8C32D69CA770045030C /* NotificationBadgeViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
Expand Down Expand Up @@ -1865,6 +1871,14 @@
path = UIKit_Deprecated;
sourceTree = "<group>";
};
D5DCF8BE2D69C1350045030C /* Storage */ = {
isa = PBXGroup;
children = (
D5DCF8BF2D69C14E0045030C /* NotificationStorage.swift */,
);
path = Storage;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -2163,6 +2177,7 @@
D5CD68A42D351EA000B790E9 /* RefuseInvitedScheduleUseCase.swift in Sources */,
D5BA30922CE745A700958BF5 /* PostFavoriteFriendBody.swift in Sources */,
D5B79A162C5B83CD004EDDF2 /* (null) in Sources */,
D5DCF8C42D69CA770045030C /* NotificationBadgeViewModel.swift in Sources */,
D534B0FE2D06F0FD004864E0 /* CoordinateService.swift in Sources */,
99C9213D2D34B72A00FC2333 /* TokenReissueUseCase.swift in Sources */,
9953583D2C00C555005BF799 /* AppDelegate.swift in Sources */,
Expand All @@ -2171,6 +2186,7 @@
99E9E18B2D3E129B008A81BF /* GetDailyScheduleUseCase.swift in Sources */,
992F57E52C5B623F00219C6B /* CheckEmailUseCase.swift in Sources */,
99647EF12D33DB2600D63B4B /* CompleteDeleteViewController.swift in Sources */,
D5DCF8C02D69C14E0045030C /* NotificationStorage.swift in Sources */,
9911F9842C3F5AA600D0FDC9 /* BannerView.swift in Sources */,
99EDF5882CE874AB0008B19A /* SnsSignUpUseCase.swift in Sources */,
D5DCBF342C7435E2003F0246 /* SearchFriendsView.swift in Sources */,
Expand Down
57 changes: 57 additions & 0 deletions Where_Are_You/Data/Storage/NotificationStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// NotificationStorage.swift
// Where_Are_You
//
// Created by juhee on 22.02.25.
//

import Foundation

// MARK: 알림 관련 데이터만 관리하는 클래스
class NotificationStorage {
static let shared = NotificationStorage() // 싱글턴 패턴

private let defaults = UserDefaults.standard
private let notificationsKey = "notificationIds"

private init() {} // 기본 생성자를 private으로 변경하여 외부에서 인스턴스 생성 방지

// MARK: 로컬에 저장된 알림 ID 목록
var savedNotificationIds: Set<String> { // Int -> String으로 변경
get {
let array = defaults.array(forKey: notificationsKey) as? [String] ?? []
return Set(array)
}
set {
defaults.set(Array(newValue), forKey: notificationsKey)
}
}

// MARK: 알림을 로컬 저장소에 저장 - 읽음 처리
func saveAllNotificationIds(notificationIds: [String]) {
var ids = savedNotificationIds
notificationIds.forEach { ids.insert($0) }
savedNotificationIds = ids
}

// MARK: 알림을 로컬 저장소에서 삭제 - 수락/거절 완료
func removeScheduleInvitation(scheduleSeq: Int) {
var currentIds = savedNotificationIds
currentIds.remove("schedule_\(scheduleSeq)")
savedNotificationIds = currentIds
}

func removeFriendRequest(friendRequestSeq: Int) {
var currentIds = savedNotificationIds
currentIds.remove("friend_\(friendRequestSeq)")
savedNotificationIds = currentIds
}

// MARK: 새로운 알림이 있는지 확인
func hasNewNotifications(scheduleInvites: [Schedule], friendRequests: [FriendRequest]) -> Bool {
let currentIds = savedNotificationIds

return scheduleInvites.contains { !currentIds.contains("schedule_\($0.scheduleSeq)") } ||
friendRequests.contains { !currentIds.contains("friend_\($0.friendRequestSeq)") }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

// TODO: GetInvitedListResponse -> Schedule convert 로직 UseCase에서 실행하도록
protocol GetInvitedListUseCase {
func execute(completion: @escaping (Result<GetInvitedListResponse, Error>) -> Void)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import Foundation
import UIKit
import SwiftUI
import Combine

class FriendFeedViewController: UIViewController {
// MARK: - Properties

private var friendsHostingController: UIHostingController<FriendsView>?
private let feedsViewController = FeedsViewController()
private let notificationBadgeViewModel = NotificationBadgeViewModel.shared
private var cancellables = Set<AnyCancellable>()

private let segmentControl: UISegmentedControl = {
let sc = UISegmentedControl()
Expand Down Expand Up @@ -72,7 +75,6 @@ class FriendFeedViewController: UIViewController {
return stackView
}()

// 1. 친구 관련 옵션 버튼 추가
private let friendOptionView: UIHostingController = {
let view = MultiOptionButtonView {
OptionButton(
Expand Down Expand Up @@ -133,10 +135,17 @@ class FriendFeedViewController: UIViewController {
setupConstraints()
setupActions()
updateUIForSelectedSegment()
setupNotificationObserver()

self.navigationController?.navigationBar.shadowImage = UIImage()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

notificationBadgeViewModel.checkForNewNotifications() // 서버에서 알림 정보 가져오기
}

// MARK: - UI Setup
private func setupUI() {
view.backgroundColor = .white
Expand Down Expand Up @@ -229,13 +238,24 @@ class FriendFeedViewController: UIViewController {
view.addGestureRecognizer(tapGesture)
}


private func setupNavigationBar() {
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: segmentControl)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: barButtonStack)
segmentControl.addTarget(self, action: #selector(handleSegmentChange), for: .valueChanged)
}

private func setupNotificationObserver() {
// 사용자 정의 알림을 구독
NotificationCenter.default.addObserver(
self,
selector: #selector(updateNotificationIcon),
name: .unreadNotificationsChanged,
object: nil
)

updateNotificationIcon()
}

// MARK: - Helpers
private func updateUIForSelectedSegment() {
if segmentControl.selectedSegmentIndex == 0 {
Expand Down Expand Up @@ -296,4 +316,10 @@ class FriendFeedViewController: UIViewController {
friendOptionView.view.isHidden = true
}
}

@objc private func updateNotificationIcon() {
// 읽지 않은 알림이 있는지 확인
let imageName = notificationBadgeViewModel.hasUnreadNotifications ? "icon-notification-badge" : "icon-notification"
notificationButton.setImage(UIImage(named: imageName), for: .normal)
}
}
32 changes: 31 additions & 1 deletion Where_Are_You/Presentation/Main/Home/Views/TitleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@
//

import UIKit
import Combine

class TitleView: UIView {
// MARK: - Properties
private let viewModel: NotificationBadgeViewModel
private var cancellables = Set<AnyCancellable>()

init(viewModel: NotificationBadgeViewModel = .shared) {
self.viewModel = viewModel
super.init(frame: .zero)
setupObserver()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

let titleLabel: UIImageView = {
let imageView = UIImageView()
Expand All @@ -28,4 +40,22 @@ class TitleView: UIView {
}
return button
}()

private func setupObserver() {
NotificationCenter.default.publisher(for: .unreadNotificationsChanged) // NotificationCenter를 사용하여 알림 상태 변경 구독
.receive(on: DispatchQueue.main)
.sink { [weak self] notification in
if let hasUnread = notification.userInfo?["hasUnread"] as? Bool {
self?.updateNotificationIcon(hasUnread: hasUnread)
}
}
.store(in: &cancellables)

updateNotificationIcon(hasUnread: viewModel.hasUnreadNotifications) // 초기 상태 설정
}

private func updateNotificationIcon(hasUnread: Bool) {
let imageName = hasUnread ? "icon-notification-badge" : "icon-notification"
notificationButton.setImage(UIImage(named: imageName), for: .normal)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
struct ScheduleView: View {
// TODO: 초대받은 일정 캘린더에 안뜸
@StateObject var viewModel: ScheduleViewModel
@StateObject private var notificationBadgeViewModel = NotificationBadgeViewModel.shared

@State private var showNotification = false
@State private var showOptionMenu = false
Expand All @@ -34,7 +35,7 @@ struct ScheduleView: View {
Button(action: {
showNotification = true
}, label: {
Image("icon-notification")
Image(notificationBadgeViewModel.hasUnreadNotifications ? "icon-notification-badge" : "icon-notification")
.frame(width: LayoutAdapter.shared.scale(value: 34), height: LayoutAdapter.shared.scale(value: 34))
})
.padding(0)
Expand All @@ -57,8 +58,7 @@ struct ScheduleView: View {
.padding(.horizontal, LayoutAdapter.shared.scale(value: 10))

if showOptionMenu {
// 배경 터치시 메뉴 닫기
Color.clear
Color.clear // 배경 터치시 메뉴 닫기
.contentShape(Rectangle())
.onTapGesture {
showOptionMenu = false
Expand Down Expand Up @@ -103,6 +103,7 @@ struct ScheduleView: View {
.environment(\.font, .pretendard(NotoSans: .regular, fontSize: LayoutAdapter.shared.scale(value: 14)))
.onAppear(perform: {
viewModel.getMonthlySchedule()
notificationBadgeViewModel.checkForNewNotifications()
})
}

Expand Down
Loading

0 comments on commit 1cfe8c4

Please sign in to comment.