From e94675db2204a3e4cc8a73431901abdaef633cc2 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 20:39:24 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=ED=8F=AC=EA=B7=B8=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EB=B3=B4=EA=B3=A0=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=AC=B5=EC=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Apps/AppDelegate.swift | 72 +++++++++++++++++++ .../CallVanChatViewController.swift | 1 + .../LostItemChatViewController.swift | 3 +- .../RecruitChatViewController.swift | 1 + .../RecruitChat/RecruitChatViewModel.swift | 2 +- 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/Koin/Apps/AppDelegate.swift b/Koin/Apps/AppDelegate.swift index 3362b8df5..322e57c2e 100644 --- a/Koin/Apps/AppDelegate.swift +++ b/Koin/Apps/AppDelegate.swift @@ -73,6 +73,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD name: NSNotification.Name("Notification Sent"), object: nil ) + // 현재 보고 있는 채팅방의 푸시는 묵살 + if await shouldSuppressForegroundNotification(userInfo: notification.request.content.userInfo) { + return [] + } // 푸시 알림 옵션 반환 return [[.banner, .list, .sound]] } @@ -98,3 +102,71 @@ extension AppDelegate: MessagingDelegate { KeychainWorker.shared.create(key: .fcm, token: fcmToken) } } + +// MARK: - Foreground 채팅 푸시 묵살 여부 +/// 채팅 중 푸시알림이 계속 도착하는 것을 방지한다. 채팅 수정 예정이므로 임시구현한다. +extension AppDelegate { + private func shouldSuppressForegroundNotification(userInfo: [AnyHashable: Any]) async -> Bool { + guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate, + let topViewController = sceneDelegate.navigationController?.topViewController else { + return false + } + + // 팀원모집 채팅 + if let type = userInfo["type"] as? String, + RecruitNotificationType(rawValue: type) == .newChatMessage { + guard let recruitChatViewController = topViewController as? RecruitChatViewController, + let notificationId = Int(userInfo["notification_id"] as? String) else { + return false + } + // 푸시에 채팅방 정보가 없으므로 알림 목록에서 notificationId 로 조회한다. + let fetchRecruitNotificationListUseCase = DefaultFetchRecruitNotificationListUseCase( + repository: DefaultRecruitRepository(service: DefaultRecruitService()) + ) + guard let notificationList = try? await fetchRecruitNotificationListUseCase.execute(page: 1), + let notification = notificationList.notifications.first(where: { $0.id == notificationId }), + sceneDelegate.navigationController?.topViewController === recruitChatViewController else { + return false + } + // RecruitNotificationListViewController 의 채팅방 진입 규칙과 동일하게 비교한다. + switch recruitChatViewController.roomSource { + case let .direct(recruitmentId, applicationId): + return notification.recruitmentId == recruitmentId + && notification.applicationId == applicationId + case let .team(recruitmentId, chatRoomId): + return notification.recruitmentId == recruitmentId + && notification.applicationId == nil + && notification.chatRoomId == chatRoomId + } + } + + // 분실물 채팅, 콜밴 채팅 + guard let aps = userInfo["aps"] as? [String: AnyObject], + let category = aps["category"] as? String, + let appPath = AppPath(rawValue: category), + let schemeUri = userInfo["schemeUri"] as? String, + let queryItems = URLComponents(string: schemeUri)?.queryItems else { + return false + } + let query = queryItems.reduce(into: [String: String]()) { result, item in + result[item.name] = item.value + } + + switch appPath { + case .chat: + guard let lostItemChatViewController = topViewController as? LostItemChatViewController, + let chatRoomId = Int(query["chatRoomId"]) else { + return false + } + return lostItemChatViewController.chatRoomId == chatRoomId + case .callvanChat: + guard let callVanChatViewController = topViewController as? CallVanChatViewController, + let postId = Int(query["postId"]) else { + return false + } + return callVanChatViewController.postId == postId + default: + return false + } + } +} diff --git a/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift b/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift index 3dc7395d6..2f0e76b40 100644 --- a/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift +++ b/Koin/Presentation/CallVan/CallVanChat/CallVanChatViewController.swift @@ -17,6 +17,7 @@ final class CallVanChatViewController: UIViewController { private let inputSubject = PassthroughSubject() private let viewModel: CallVanChatViewModel private var subscriptions: Set = [] + var postId: Int { viewModel.postId } // MARK: - TitleView private lazy var titleLabel = UILabel() diff --git a/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift index 14f1e93cd..71923813f 100644 --- a/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift +++ b/Koin/Presentation/LostItem/LostItemChat/LostItemChatViewController.swift @@ -10,13 +10,14 @@ import PhotosUI import UIKit final class LostItemChatViewController: UIViewController, UITextViewDelegate, PHPickerViewControllerDelegate { - // + // MARK: - Properties private let viewModel: LostItemChatViewModel private let inputSubject: PassthroughSubject = .init() private var subscriptions: Set = [] private var messageInputBottomConstraint: NSLayoutConstraint! private var textViewHeightConstraint: NSLayoutConstraint! + var chatRoomId: Int { viewModel.chatRoomId } // MARK: - UI Components private let bottomBackgroundView = UIView().then { diff --git a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift index 11b23223c..47b2f39e4 100644 --- a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift +++ b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift @@ -17,6 +17,7 @@ final class RecruitChatViewController: UIViewController { private let viewModel: RecruitChatViewModel private let inputSubject = PassthroughSubject() private var subscriptions = Set() + var roomSource: RecruitChatViewModel.RoomSource { viewModel.roomSource } // MARK: - UI Components private let chatListView = ChatListView() diff --git a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewModel.swift b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewModel.swift index 3cb21fcc0..453935604 100644 --- a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewModel.swift +++ b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewModel.swift @@ -39,7 +39,7 @@ final class RecruitChatViewModel: ViewModelProtocol { private var pollingTask: Task? - private let roomSource: RoomSource + let roomSource: RoomSource private var chatRoomId: Int? private var recruitmentId: Int { switch roomSource { From eb0848d667a667822c1baec436a78218bf66e28f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 20:42:04 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=ED=8C=80=EC=9B=90=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=20=ED=8C=80=EC=B1=84=ED=8C=85=20navigationTitle=EC=97=90=20?= =?UTF-8?q?=EC=9D=B8=EC=9B=90=EC=88=98=EA=B0=80=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Recruit/RecruitChat/RecruitChatViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift index 47b2f39e4..8dc08b8d1 100644 --- a/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift +++ b/Koin/Presentation/Recruit/RecruitChat/RecruitChatViewController.swift @@ -121,6 +121,8 @@ extension RecruitChatViewController { let currentMemberCount = data.currentMemberCount, let maximumMemberCount = data.maximumMemberCount { participantsLabel.text = "\(currentMemberCount)/\(maximumMemberCount)" + peopleImageView.isHidden = false + participantsLabel.isHidden = false } else { peopleImageView.isHidden = true participantsLabel.isHidden = true From 272fdcee180fb4223124b11dff7f44deda584a88 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 21:46:44 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20unreadCount=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/Models/ChatMessageRowModel.swift | 1 + .../Views/ChatTableView/ChatLeftCell.swift | 26 ++++++++++++++++--- .../Views/ChatTableView/ChatRightCell.swift | 26 ++++++++++++++++--- .../Recruit/RecruitChatDataDto.swift | 5 +++- .../Recruit/RecruitChatMessageDto.swift | 4 ++- .../Model/Recruit/RecruitChatData.swift | 1 + .../Model/Recruit/RecruitChatMessage.swift | 2 +- .../Support/ChatListModel+CallVanChat.swift | 3 ++- .../Mapper/ChatListModel+RecruitChat.swift | 3 ++- 9 files changed, 58 insertions(+), 13 deletions(-) diff --git a/Koin/Core/View/UIKit/Chat/Models/ChatMessageRowModel.swift b/Koin/Core/View/UIKit/Chat/Models/ChatMessageRowModel.swift index ed35aaf3e..0a34f2e05 100644 --- a/Koin/Core/View/UIKit/Chat/Models/ChatMessageRowModel.swift +++ b/Koin/Core/View/UIKit/Chat/Models/ChatMessageRowModel.swift @@ -15,6 +15,7 @@ struct ChatMessageRowModel { let showsProfile: Bool let isLeftUser: Bool let profileImage: UIImage? + let unreadCount: String? } enum ChatMessageAlignment { diff --git a/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatLeftCell.swift b/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatLeftCell.swift index 0f1981706..b63cc7ba3 100644 --- a/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatLeftCell.swift +++ b/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatLeftCell.swift @@ -38,11 +38,13 @@ final class ChatLeftCell: UITableViewCell { private let messageImageWrapperView = UIView() private let messageImageView = UIImageView() private let messageImageTimeLabel = UILabel() + private let messageImageUnreadCountLabel = UILabel() private let messageTextWrapperView = UIView() private let messageTextView = UIView() private let messageTextLabel = UILabel() private let messageTextTimeLabel = UILabel() + private let messageTextUnreadCountLabel = UILabel() // MARK: - Initializer override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -71,6 +73,7 @@ final class ChatLeftCell: UITableViewCell { self.imageUrl = imageUrl messageImageView.loadImageWithSpinner(from: imageUrl) messageImageTimeLabel.text = message.timeText + messageImageUnreadCountLabel.text = message.unreadCount ?? "" case .text(let text): messageImageWrapperView.isHidden = true messageTextWrapperView.isHidden = false @@ -80,6 +83,7 @@ final class ChatLeftCell: UITableViewCell { attributes: messageTextLabelAttributes ) messageTextTimeLabel.text = message.timeText + messageTextUnreadCountLabel.text = message.unreadCount ?? "" } } } @@ -150,6 +154,10 @@ extension ChatLeftCell { $0.font = UIFont.appFont(.pretendardRegular, size: 12) $0.textColor = UIColor.appColor(.neutral500) } + messageImageUnreadCountLabel.do { + $0.font = .appFont(.pretendardRegular, size: 12) + $0.textColor = .appColor(.neutral700) + } // MARK: 텍스트 messageTextView.do { @@ -163,16 +171,20 @@ extension ChatLeftCell { $0.font = UIFont.appFont(.pretendardRegular, size: 12) $0.textColor = UIColor.appColor(.neutral500) } + messageTextUnreadCountLabel.do { + $0.font = .appFont(.pretendardRegular, size: 12) + $0.textColor = .appColor(.neutral700) + } } private func setUpLayouts() { [profileImageView, nicknameLabel, leftUserLabel].forEach { profileWrapperView.addSubview($0) } - [messageImageView, messageImageTimeLabel].forEach { + [messageImageView, messageImageTimeLabel, messageImageUnreadCountLabel].forEach { messageImageWrapperView.addSubview($0) } - [messageTextView, messageTextLabel, messageTextTimeLabel].forEach { + [messageTextView, messageTextLabel, messageTextTimeLabel, messageTextUnreadCountLabel].forEach { messageTextWrapperView.addSubview($0) } [profileWrapperView, messageImageWrapperView, messageTextWrapperView].forEach { @@ -211,10 +223,13 @@ extension ChatLeftCell { $0.top.leading.bottom.equalTo(messageImageWrapperView) } messageImageTimeLabel.snp.makeConstraints { - $0.height.equalTo(19) $0.leading.equalTo(messageImageView.snp.trailing).offset(8) $0.bottom.trailing.equalTo(messageImageWrapperView) } + messageImageUnreadCountLabel.snp.makeConstraints { + $0.leading.equalTo(messageImageTimeLabel) + $0.bottom.equalTo(messageImageTimeLabel.snp.top) + } // MARK: 텍스트 messageTextView.snp.makeConstraints { @@ -226,9 +241,12 @@ extension ChatLeftCell { $0.leading.trailing.equalTo(messageTextView).inset(12) } messageTextTimeLabel.snp.makeConstraints { - $0.height.equalTo(19) $0.leading.equalTo(messageTextView.snp.trailing).offset(8) $0.bottom.trailing.equalTo(messageTextWrapperView) } + messageTextUnreadCountLabel.snp.makeConstraints { + $0.leading.equalTo(messageTextTimeLabel) + $0.bottom.equalTo(messageTextTimeLabel.snp.top) + } } } diff --git a/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatRightCell.swift b/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatRightCell.swift index 353f0191a..02cdb25c2 100644 --- a/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatRightCell.swift +++ b/Koin/Core/View/UIKit/Chat/Views/ChatTableView/ChatRightCell.swift @@ -35,11 +35,13 @@ final class ChatRightCell: UITableViewCell { private let messageImageWrapperView = UIView() private let messageImageView = UIImageView() private let messageImageTimeLabel = UILabel() + private let messageImageUnreadCountLabel = UILabel() private let messageTextWrapperView = UIView() private let messageTextView = UIView() private let messageTextLabel = UILabel() private let messageTextTimeLabel = UILabel() + private let messageTextUnreadCountLabel = UILabel() // MARK: - Initializer override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -65,6 +67,7 @@ final class ChatRightCell: UITableViewCell { self.imageUrl = imageUrl messageImageView.loadImageWithSpinner(from: imageUrl) messageImageTimeLabel.text = message.timeText + messageImageUnreadCountLabel.text = message.unreadCount ?? "" case .text(let text): messageImageWrapperView.isHidden = true messageTextWrapperView.isHidden = false @@ -74,6 +77,7 @@ final class ChatRightCell: UITableViewCell { attributes: messageTextLabelAttributes ) messageTextTimeLabel.text = message.timeText + messageTextUnreadCountLabel.text = message.unreadCount ?? "" } } } @@ -133,6 +137,10 @@ extension ChatRightCell { $0.font = UIFont.appFont(.pretendardRegular, size: 12) $0.textColor = UIColor.appColor(.neutral500) } + messageImageUnreadCountLabel.do { + $0.font = .appFont(.pretendardRegular, size: 12) + $0.textColor = .appColor(.neutral700) + } // MARK: 텍스트 messageTextView.do { @@ -146,13 +154,17 @@ extension ChatRightCell { $0.font = UIFont.appFont(.pretendardRegular, size: 12) $0.textColor = UIColor.appColor(.neutral500) } + messageTextUnreadCountLabel.do { + $0.font = .appFont(.pretendardRegular, size: 12) + $0.textColor = .appColor(.neutral700) + } } private func setUpLayouts() { - [messageImageView, messageImageTimeLabel].forEach { + [messageImageView, messageImageTimeLabel, messageImageUnreadCountLabel].forEach { messageImageWrapperView.addSubview($0) } - [messageTextView, messageTextLabel, messageTextTimeLabel].forEach { + [messageTextView, messageTextLabel, messageTextTimeLabel, messageTextUnreadCountLabel].forEach { messageTextWrapperView.addSubview($0) } [profileWrapperView, messageImageWrapperView, messageTextWrapperView].forEach { @@ -181,10 +193,13 @@ extension ChatRightCell { $0.top.trailing.bottom.equalTo(messageImageWrapperView) } messageImageTimeLabel.snp.makeConstraints { - $0.height.equalTo(19) $0.trailing.equalTo(messageImageView.snp.leading).offset(-8) $0.bottom.leading.equalTo(messageImageWrapperView) } + messageImageUnreadCountLabel.snp.makeConstraints { + $0.trailing.equalTo(messageImageTimeLabel) + $0.bottom.equalTo(messageImageTimeLabel.snp.top) + } // MARK: 텍스트 messageTextView.snp.makeConstraints { @@ -196,9 +211,12 @@ extension ChatRightCell { $0.leading.trailing.equalTo(messageTextView).inset(12) } messageTextTimeLabel.snp.makeConstraints { - $0.height.equalTo(19) $0.trailing.equalTo(messageTextView.snp.leading).offset(-8) $0.bottom.leading.equalTo(messageTextWrapperView) } + messageTextUnreadCountLabel.snp.makeConstraints { + $0.trailing.equalTo(messageTextTimeLabel) + $0.bottom.equalTo(messageTextTimeLabel.snp.top) + } } } diff --git a/Koin/Data/DTOs/Decodable/Recruit/RecruitChatDataDto.swift b/Koin/Data/DTOs/Decodable/Recruit/RecruitChatDataDto.swift index bade6d2eb..f4dd3ca5f 100644 --- a/Koin/Data/DTOs/Decodable/Recruit/RecruitChatDataDto.swift +++ b/Koin/Data/DTOs/Decodable/Recruit/RecruitChatDataDto.swift @@ -13,6 +13,7 @@ struct RecruitChatDataDto: Decodable { let roomType: RecruitChatRoomTypeDto let memberCount: Int? let maxMemberCount: Int? + let unreadCount: Int? enum CodingKeys: String, CodingKey { case chatRoomId = "chat_room_id" @@ -20,6 +21,7 @@ struct RecruitChatDataDto: Decodable { case roomType = "room_type" case memberCount = "member_count" case maxMemberCount = "max_member_count" + case unreadCount = "unread_count" } } @@ -35,7 +37,8 @@ extension RecruitChatDataDto { chatRoomName: roomName, chatRoomType: roomType.toDomain(), currentMemberCount: memberCount, - maximumMemberCount: maxMemberCount + maximumMemberCount: maxMemberCount, + unreadCount: unreadCount ) } } diff --git a/Koin/Data/DTOs/Decodable/Recruit/RecruitChatMessageDto.swift b/Koin/Data/DTOs/Decodable/Recruit/RecruitChatMessageDto.swift index 789271e1b..5ce183b32 100644 --- a/Koin/Data/DTOs/Decodable/Recruit/RecruitChatMessageDto.swift +++ b/Koin/Data/DTOs/Decodable/Recruit/RecruitChatMessageDto.swift @@ -55,6 +55,8 @@ extension Array where Element == RecruitChatMessageDto { let showProfile = !(dto.userId == previousUserId && date == previousDate) previousUserId = dto.userId previousDate = date + + let unreadCount = dto.unreadCount == 0 ? nil : "\(dto.unreadCount)" return RecruitChatMessage( messageId: dto.messageId, @@ -63,7 +65,7 @@ extension Array where Element == RecruitChatMessageDto { content: dto.content, timestamp: timestamp, isImage: dto.isImage, - unreadCount: dto.unreadCount, + unreadCount: unreadCount, isMine: dto.userId == myUserId, showProfile: showProfile, profileImage: profileImages[profileIndex] diff --git a/Koin/Domain/Model/Recruit/RecruitChatData.swift b/Koin/Domain/Model/Recruit/RecruitChatData.swift index d7a18ffb4..e8cc511e4 100644 --- a/Koin/Domain/Model/Recruit/RecruitChatData.swift +++ b/Koin/Domain/Model/Recruit/RecruitChatData.swift @@ -13,6 +13,7 @@ struct RecruitChatData { let chatRoomType: RecruitChatRoomType let currentMemberCount: Int? let maximumMemberCount: Int? + let unreadCount: Int? } enum RecruitChatRoomType: String { diff --git a/Koin/Domain/Model/Recruit/RecruitChatMessage.swift b/Koin/Domain/Model/Recruit/RecruitChatMessage.swift index 9e08b3977..749ce78ed 100644 --- a/Koin/Domain/Model/Recruit/RecruitChatMessage.swift +++ b/Koin/Domain/Model/Recruit/RecruitChatMessage.swift @@ -14,7 +14,7 @@ struct RecruitChatMessage { let content: String let timestamp: Date let isImage: Bool - let unreadCount: Int + let unreadCount: String? let isMine: Bool let showProfile: Bool let profileImage: ImageAsset? diff --git a/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift b/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift index 0181c2da9..4ece01ac7 100644 --- a/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift +++ b/Koin/Presentation/CallVan/CallVanChat/Support/ChatListModel+CallVanChat.swift @@ -25,7 +25,8 @@ extension ChatMessageRowModel { timeText: message.time, showsProfile: message.showProfile, isLeftUser: message.isLeftUser, - profileImage: message.profileImage + profileImage: message.profileImage, + unreadCount: nil ) } } diff --git a/Koin/Presentation/Recruit/RecruitChat/Mapper/ChatListModel+RecruitChat.swift b/Koin/Presentation/Recruit/RecruitChat/Mapper/ChatListModel+RecruitChat.swift index b8c16548a..f9a7643a7 100644 --- a/Koin/Presentation/Recruit/RecruitChat/Mapper/ChatListModel+RecruitChat.swift +++ b/Koin/Presentation/Recruit/RecruitChat/Mapper/ChatListModel+RecruitChat.swift @@ -27,7 +27,8 @@ extension ChatMessageRowModel { timeText: message.displayTime, showsProfile: message.showProfile, isLeftUser: false, - profileImage: message.profileImage.flatMap { UIImage.appImage(asset: $0) } + profileImage: message.profileImage.flatMap { UIImage.appImage(asset: $0) }, + unreadCount: message.unreadCount ) } } From b58aa9734831f667b40ab538bea9c9f9770f57e3 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 22:06:42 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EC=83=81?= =?UTF-8?q?=EB=8C=80=EB=B0=A9=EC=9D=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=84=EC=86=A1=ED=96=88=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20body=EC=97=90=20static?= =?UTF-8?q?=20image=20url=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NotificationService/NotificationService.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 7ffa92300..9124596ee 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -26,11 +26,13 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(request.content) return } - + + self.bestAttemptContent = modifyBodyIfNeeded(of: bestAttemptContent) + Task { // MARK: - SwiftData에 Notification 데이터를 기록한다. do { - try await saveNotificationIfAvailable(userInfo: bestAttemptContent.userInfo) + try await saveNotificationIfAvailable(content: bestAttemptContent) } catch { print(error.localizedDescription) } @@ -58,11 +60,20 @@ class NotificationService: UNNotificationServiceExtension { } extension NotificationService { - private func saveNotificationIfAvailable(userInfo: [AnyHashable: Any]) async throws { + // MARK: - 채팅 이미지 메시지의 경우 body에 이미지 URL이 담겨오므로 치환한다. + private func modifyBodyIfNeeded(of content: UNMutableNotificationContent) -> UNMutableNotificationContent { + if content.body.contains("static.koreatech.in") { + content.body = "사진을 보냈습니다." + } + return content + } + + private func saveNotificationIfAvailable(content: UNNotificationContent) async throws { + let userInfo = content.userInfo + let body = content.body + let title = content.title guard let aps = userInfo["aps"] as? [String: Any], - let alert = aps["alert"] as? [String: Any], - let body = alert["body"] as? String, - let title = alert["title"] as? String, + aps["alert"] is [String: Any], let category = aps["category"] as? String, let appPath = AppPath(rawValue: category), let schemeUri = userInfo["schemeUri"] as? String, From f202744261672b77749558db6f4d7d4229812aae Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 23:06:49 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=20=EC=B2=A8=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NotificationService/NotificationService.swift | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 9124596ee..f26d4d8ea 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -27,6 +27,8 @@ class NotificationService: UNNotificationServiceExtension { return } + let originalBody = bestAttemptContent.body + self.bestAttemptContent = modifyBodyIfNeeded(of: bestAttemptContent) Task { @@ -38,10 +40,8 @@ class NotificationService: UNNotificationServiceExtension { } // MARK: - 푸시알림이 이미지를 포함하는 경우 처리 - if let fcmOptionsUserInfo = bestAttemptContent.userInfo["fcm_options"] as? [String: Any], - let imageURLString = fcmOptionsUserInfo["image"] as? String, - let imageURL = URL(string: imageURLString), - let imageData = try? Data(contentsOf: imageURL), + if let imageURL = imageURL(fromFCMOptions: bestAttemptContent.userInfo) ?? imageURL(fromBody: originalBody), + let imageData = await downloadImage(from: imageURL), let attachment = UNNotificationAttachment.saveImageToDisk(identifier: "certificationImage.jpg", data: imageData, options: nil) { bestAttemptContent.attachments = [attachment] } @@ -67,6 +67,28 @@ extension NotificationService { } return content } + + // MARK: - fcm_options에 담긴 이미지 URL을 꺼낸다. + private func imageURL(fromFCMOptions userInfo: [AnyHashable: Any]) -> URL? { + guard let fcmOptionsUserInfo = userInfo["fcm_options"] as? [String: Any], + let imageURLString = fcmOptionsUserInfo["image"] as? String else { + return nil + } + return URL(string: imageURLString) + } + + // MARK: - String 타입 ImageUrl을 URL 타입으로 변환한다. + private func imageURL(fromBody body: String) -> URL? { + guard body.contains("static.koreatech.in") else { + return nil + } + return URL(string: body.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + // MARK: - 이미지를 다운로드한다. + private func downloadImage(from url: URL) async -> Data? { + return try? await URLSession.shared.data(from: url).0 + } private func saveNotificationIfAvailable(content: UNNotificationContent) async throws { let userInfo = content.userInfo From 829375de4a9ce3fb11809bd6b6441fab671f37b6 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 23:07:02 +0900 Subject: [PATCH 6/8] =?UTF-8?q?style:=20NotificationService=20MARK=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NotificationService/NotificationService.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index f26d4d8ea..9b2d06e2c 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -10,9 +10,11 @@ import FirebaseMessaging class NotificationService: UNNotificationServiceExtension { + // MARK: - Properties var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNNotificationContent? + // MARK: - DidReceive override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void @@ -20,15 +22,16 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler self.bestAttemptContent = request.content + // MARK: - TypeCasting guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else { - // 만약 mutableCopy가 UNMutableNotificationContent로 변환되지 않는다면, - // 바로 이 함수에서 빠져나갑니다. contentHandler(request.content) return } + // MARK: - Cache let originalBody = bestAttemptContent.body + // MARK: - Modify self.bestAttemptContent = modifyBodyIfNeeded(of: bestAttemptContent) Task { @@ -50,9 +53,8 @@ class NotificationService: UNNotificationServiceExtension { } } + // MARK: - WillExpire override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } @@ -60,7 +62,7 @@ class NotificationService: UNNotificationServiceExtension { } extension NotificationService { - // MARK: - 채팅 이미지 메시지의 경우 body에 이미지 URL이 담겨오므로 치환한다. + // MARK: - body에 담긴 Image URL을 텍스트로 치환한다. private func modifyBodyIfNeeded(of content: UNMutableNotificationContent) -> UNMutableNotificationContent { if content.body.contains("static.koreatech.in") { content.body = "사진을 보냈습니다." @@ -89,7 +91,10 @@ extension NotificationService { private func downloadImage(from url: URL) async -> Data? { return try? await URLSession.shared.data(from: url).0 } +} +extension NotificationService { + // MARK: - SwiftData에 푸시알림을 기록한다. private func saveNotificationIfAvailable(content: UNNotificationContent) async throws { let userInfo = content.userInfo let body = content.body @@ -116,6 +121,7 @@ extension NotificationService { } extension UNNotificationAttachment { + // MARK: - 이미지를 Attachment로 변환한다. static func saveImageToDisk(identifier: String, data: Data, options: [AnyHashable : Any]? = nil) -> UNNotificationAttachment? { let fileManager = FileManager.default let folderName = ProcessInfo.processInfo.globallyUniqueString From 416b3808c3581165fb199a7cb3add0843387306f Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 23:27:17 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=ED=8C=80=EC=9B=90?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=20=EB=AA=A9=EB=A1=9D=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EB=9C=A8=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Recruit/RecruitList/RecruitListViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Koin/Presentation/Recruit/RecruitList/RecruitListViewModel.swift b/Koin/Presentation/Recruit/RecruitList/RecruitListViewModel.swift index 428428b1a..5bd1b3fa1 100644 --- a/Koin/Presentation/Recruit/RecruitList/RecruitListViewModel.swift +++ b/Koin/Presentation/Recruit/RecruitList/RecruitListViewModel.swift @@ -203,7 +203,10 @@ extension RecruitListViewModel { Task { do { myRecruitProfile = try await fetchMyRecruitProfileUseCase.execute() - } catch { + } catch let error as ErrorResponse where error.statusCode == 401 { + return + } + catch { errorMessage = (error as? ErrorResponse)?.message ?? error.localizedDescription } } From a6de24ecdbb361154d99f774b54b2f455d749689 Mon Sep 17 00:00:00 2001 From: GI JEONG HONG Date: Tue, 13 Oct 2026 23:36:54 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=ED=8C=80=EC=9B=90=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=95=99=EB=B2=88=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Koin/Domain/Model/Recruit/BasicInfo.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Koin/Domain/Model/Recruit/BasicInfo.swift b/Koin/Domain/Model/Recruit/BasicInfo.swift index 6f3b2c0c4..4ddcae525 100644 --- a/Koin/Domain/Model/Recruit/BasicInfo.swift +++ b/Koin/Domain/Model/Recruit/BasicInfo.swift @@ -20,6 +20,9 @@ extension BasicInfo { let studentNumber, !studentNumber.isEmpty else { return false } + guard StudentNumberInput.isValid(studentNumber) else { + return false + } return true } }