Skip to content
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

[IDLE-515] 요양보호사 주변 공고 등록시 알림을 전송한다. #102

Merged
merged 8 commits into from
Nov 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension NotificationItemDTO: EntityRepresentable {
printIfDebug("\(NotificationItemDTO.self): 생성날짜 디코딩 실패")
}

var notificationDetail: NotificationDetailVO?
var notificationDetail: NotificationDestinationForInApp?
switch notificationType {
case .APPLICANT:
if let postId = (notificationDetails as? ApplicantInfluxDTO)?.toEntity() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct NotificationVO {
public let body: String
public let createdDate: Date
public let imageDownloadInfo: ImageDownLoadInfo?
public let notificationDetails: NotificationDetailVO?
public let notificationDetails: NotificationDestinationForInApp?

public init(
id: String,
Expand All @@ -24,7 +24,7 @@ public struct NotificationVO {
body: String,
createdDate: Date,
imageDownloadInfo: ImageDownLoadInfo?,
notificationDetails: NotificationDetailVO?
notificationDetails: NotificationDestinationForInApp?
) {
self.id = id
self.isRead = isRead
Expand All @@ -36,6 +36,7 @@ public struct NotificationVO {
}
}

public enum NotificationDetailVO {
public enum NotificationDestinationForInApp {
case applicant(id: String)
case postDetailForWorker(info: RecruitmentPostInfo)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
//
// NotificationBellView.swift
// CenterMainPageFeature
// DSKit
//
// Created by choijunios on 10/22/24.
// Created by choijunios on 11/13/24.
//

import UIKit

import DSKit

class NotificationBellView: UIView {
public class NotificationBellView: UIView {

let button: UIButton = {
public let button: UIButton = {
let button = UIButton()
button.setImage(DSIcon.notiBell.image, for: .normal)
button.imageView?.tintColor = DSColor.gray200.color
return button
}()

let unreadPoint: UIView = {
public let unreadPoint: UIView = {
let view: UIView = .init()
view.backgroundColor = DSColor.red200.color
view.layer.cornerRadius = 3
Expand All @@ -27,7 +25,7 @@ class NotificationBellView: UIView {
return view
}()

init() {
public init() {
super.init(frame: .zero)

setAutoLayout()
Expand Down Expand Up @@ -62,7 +60,7 @@ class NotificationBellView: UIView {
])
}

func setUnreadState(_ showUnreadPoint: Bool) {
public func setUnreadState(_ showUnreadPoint: Bool) {
UIView.animate(withDuration: 0.35) {
self.unreadPoint.alpha = showUnreadPoint ? 1 : 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ public extension AuthInOutStreamManager {
.flatMap { [useCase, input] _ in

let formatted = Self.formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value)
#if DEBUG
print("✅ 디버그모드에서 번호인증 요청 무조건 통과")
return Single.just(Result<String, DomainError>.success(formatted))
#endif

return useCase.requestPhoneNumberAuthentication(phoneNumber: formatted)
}
.share()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,43 @@ public protocol RemoteNotificationHelper {
var deeplinks: BehaviorSubject<DeeplinkBundle> { get }

/// 인앱에서 발생한 Notification을 처리합니다.
func handleNotificationInApp(detail: NotificationDetailVO)
func handleNotificationInApp(detail: NotificationDestinationForInApp)
}

public enum DeepLinkPathComponent {

// MARK: Center
case centerMainPage
case postApplicantPage
case splashPage

// MARK: Worker
case workerMainPage
case postDetailForWorkerPage
}

public enum PreDefinedDeeplinkPath: String {

/// 센터관리자가 등록한 공고에 요양보호사가 지원하는 상황
case postApplicant = "APPLICANT"

case newJobPostingForWorker = "NEW_JOB_POSTING"

public var outsideLinks: [DeepLinkPathComponent] {
switch self {
case .postApplicant:
[.centerMainPage, .postApplicantPage]
case .newJobPostingForWorker:
[.workerMainPage, .postDetailForWorkerPage]
}
}

public var insideLinks: [DeepLinkPathComponent] {
switch self {
case .postApplicant:
[.postApplicantPage]
case .newJobPostingForWorker:
[.postDetailForWorkerPage]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public class CreatePostViewController: BaseViewController {
}()
lazy var statusBar: ProcessStatusBar = {
let view = ProcessStatusBar(
processCount: RegisterRecruitmentPage.allCases.count,
processCount: RegisterRecruitmentPage.stages.count,
startIndex: 0
)
return view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ extension AppCoordinator {
coordinator.startFlow = { [weak self] destination in
guard let self else { return }
switch destination {
case .notificationPage:
userNotifications()
case .accountDeregisterPage:
accountDeregister(userType: .worker)
case .authFlow:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// PostDetailForWorkerDeepLink.swift
// Root
//
// Created by choijunios on 11/13/24.
//

import Foundation
import WorkerMainPageFeature
import PostDetailForWorkerFeature
import BaseFeature
import Domain

class PostDetailForWorkerDeepLink: DeeplinkExecutable {

var component: DeepLinkPathComponent = .postDetailForWorkerPage

var children: [DeeplinkExecutable] = []

var isDestination: Bool = false

init() { }

func execute(with coordinator: any BaseFeature.Coordinator, userInfo: [AnyHashable : Any]?) -> Coordinator? {


guard let appCoordinator = coordinator as? AppCoordinator else {
return nil
}

guard let postId = userInfo?["jobPostingId"] as? String else { return nil }

let postDetailForWorkerCoordinator = appCoordinator.postDetailForWorkerFlow(postInfo: .init(type: .native, id: postId))

return postDetailForWorkerCoordinator
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// WorkerMainPageDeepLink.swift
// Root
//
// Created by choijunios on 11/13/24.
//

import Foundation
import BaseFeature

class WorkerMainPageDeepLink: DeeplinkExecutable {

var component: DeepLinkPathComponent = .centerMainPage

var children: [DeeplinkExecutable] = [
PostDetailForWorkerDeepLink()
]

var isDestination: Bool = false

init() { }

func execute(with coordinator: any BaseFeature.Coordinator, userInfo: [AnyHashable : Any]?) -> Coordinator? {

guard let appCoordinator = coordinator as? AppCoordinator else {
return nil
}

let _ = appCoordinator.runWorkerMainPageFlow()

return appCoordinator
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ enum DeeplinkParserError: LocalizedError {

class DeeplinkParser {

/// [딥링크 동작]
/// 먼저 딥링크를 처리할 수 있는 루트와 스타팅 포인트를 탐색합니다. 스타팅포인트는 옵셔널 입니다.
/// 스타팅 포인트를 따로둔 이유는 알림 인앱처리시 루트가 코디네이터에 대한 처리가 필요없기 때문입니다.
func makeDeeplinkList(components: [DeepLinkPathComponent], startFromRoot: Bool = true) throws -> [DeeplinkExecutable] {

var deeplinks: [DeeplinkExecutable] = []
Expand All @@ -35,12 +38,14 @@ class DeeplinkParser {

if deeplinks.isEmpty {

// 딥링크의 경로의 첫번째 시작지점을 선정합니다.

var start: DeeplinkExecutable!

if startFromRoot {
start = try findRoot(component: component)
} else {
start = try findStartPoint(component: component)
start = try findFirstStartPointAboveApp(component: component)
}

deeplinks.append(start)
Expand All @@ -57,21 +62,29 @@ class DeeplinkParser {
return deeplinks
}

/// 딥링크의 루트를 의미합니다.
private func findRoot(component: DeepLinkPathComponent) throws -> DeeplinkExecutable {
switch component {
case .centerMainPage:
return CenterMainPageDeeplink()
case .workerMainPage:
return WorkerMainPageDeepLink()
default:
throw DeeplinkParserError.rootNotFound
}
}

private func findStartPoint(component: DeepLinkPathComponent) throws -> DeeplinkExecutable {
/// 루트는 아니지만 스타팅 포인트가 될 수 있는 지점을 의미합니다.
private func findFirstStartPointAboveApp(component: DeepLinkPathComponent) throws -> DeeplinkExecutable {
switch component {
case .centerMainPage:
return CenterMainPageDeeplink()
case .postApplicantPage:

return PostApplicantDeeplink()

case .postDetailForWorkerPage:

return PostDetailForWorkerDeepLink()

default:
throw DeeplinkParserError.startPointNotFound
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class DefaultRemoteNotificationHelper: NSObject, RemoteNotificationHelper
UNUserNotificationCenter.current().delegate = self
}

public func handleNotificationInApp(detail: Domain.NotificationDetailVO) {
public func handleNotificationInApp(detail: Domain.NotificationDestinationForInApp) {
switch detail {
case .applicant(let id):
let desination: PreDefinedDeeplinkPath = .postApplicant
Expand All @@ -41,32 +41,80 @@ public class DefaultRemoteNotificationHelper: NSObject, RemoteNotificationHelper
} catch {
printIfDebug("딥링크 파싱실패 \(error.localizedDescription)")
}
case .postDetailForWorker(let info):

// 미구현 기능
if info.type == .workNet { return }

let desination: PreDefinedDeeplinkPath = .newJobPostingForWorker
do {
let parsedLinks = try deeplinkParser.makeDeeplinkList(components: desination.insideLinks, startFromRoot: false)
deeplinks.onNext(.init(
deeplinks: parsedLinks,
userInfo: ["jobPostingId": info.id]
))
} catch {
printIfDebug("딥링크 파싱실패 \(error.localizedDescription)")
}
}
}
}

extension DefaultRemoteNotificationHelper: UNUserNotificationCenterDelegate {

private func parseNotificationToDestination(_ notification: UNNotification) -> PreDefinedDeeplinkPath? {

let userInfo = notification.request.content.userInfo

let _ = userInfo["notificationId"] as? String
let notificationType = userInfo["notificationType"] as? String

guard let notificationType, let desination = PreDefinedDeeplinkPath(rawValue: notificationType) else { return nil }

return desination
}

/// 앱이 포그라운드에 있는 경우, 노티페이케이션이 도착하기만 하면 호출된다.
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

// 유저 인터렉션 불가 내부 이벤트로 처리야해야함

// guard let destination = parseNotificationToDestination(notification) else {
// return
// }
//
// let userInfo = notification.request.content.userInfo
//
// var inAppDestination: NotificationDestinationForInApp?
//
// switch destination {
// case .postApplicant:
// guard let id = userInfo["jobPostingId"] as? String else { return }
// inAppDestination = .applicant(id: id)
// case .newJobPostingForWorker:
// guard let id = userInfo["jobPostingId"] as? String else { return }
// inAppDestination = .postDetailForWorker(info: .init(type: .native, id: id))
// }
//
// // 유저 인터렉션 불가 내부 이벤트로 처리야해야함
// if let inAppDestination {
//
// handleNotificationInApp(detail: inAppDestination)
// }
}

/// 앱이 백그라운드에 있는 경우, 유저가 노티피케이션을 통해 액션을 선택한 경우 호출
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

handleNotification(notification: response.notification)
}

private func handleNotification(notification: UNNotification) {

let notification = response.notification
let userInfo = notification.request.content.userInfo

let _ = userInfo["notificationId"] as? String
let notificationType = userInfo["notificationType"] as? String
guard let destination = parseNotificationToDestination(notification) else {
return
}

guard let notificationType, let desination = PreDefinedDeeplinkPath(rawValue: notificationType) else { return }
handleNotification(desination: destination, userInfo: userInfo)
}

private func handleNotification(desination: PreDefinedDeeplinkPath, userInfo: [AnyHashable: Any]?) {

do {
let parsedLinks = try deeplinkParser.makeDeeplinkList(components: desination.outsideLinks)
Expand Down
Loading
Loading