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-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다. #183

Merged
merged 18 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
df01ceb
[IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다.
wonjunYou Oct 3, 2024
942374a
[IDLE-400] fcm 모듈 설정 추가
wonjunYou Oct 14, 2024
03c319e
[IDLE-400] 공고 지원자 발생 시, 모든 센터 관리자들에게 다중 알림을 발송한다.
wonjunYou Oct 14, 2024
e405542
[IDLE-400] 채용 공고 지원자 발생 시, 센터 관리자에게 알림을 발송한다.
wonjunYou Oct 3, 2024
0f84a00
[IDLE-400] fcm 모듈 설정 추가
wonjunYou Oct 14, 2024
8257e43
[IDLE-400] 공고 지원자 발생 시, 모든 센터 관리자들에게 다중 알림을 발송한다.
wonjunYou Oct 14, 2024
0f88be5
[IDLE-415] 알림 조회 처리 API
wonjunYou Oct 16, 2024
6f185c2
[IDLE-417] 읽지 않은 알림 수 집계 API
wonjunYou Oct 18, 2024
eaa9122
[IDLE-418] 알림 목록 조회 API
wonjunYou Oct 19, 2024
83d0c69
[IDLE-418] 피드백 반영
wonjunYou Oct 19, 2024
bd9ce68
[IDLE-423] soft-delete가 적용된 즐겨찾기 entity에서, 즐겨찾기 해제 후 다시 설정하는 경우 발생하는 …
wonjunYou Oct 19, 2024
6d967a9
[IDLE-424] 요양 보호사 공고 전체 조회 시, 삭제된 공고가 함께 보이는 문제 해결
wonjunYou Oct 19, 2024
33c1a79
Merge remote-tracking branch 'origin/feat/IDLE-400' into feat/IDLE-400
wonjunYou Oct 19, 2024
4bfe1a4
[IDLE-400] 알림 명세 변경
wonjunYou Oct 19, 2024
e4b9ede
[IDLE-400] fcm 설정 파일 디렉토리 path 변경
wonjunYou Oct 19, 2024
6bdf1bf
[IDLE-400] ddl 옵션 변경
wonjunYou Oct 19, 2024
d3ef466
[IDLE-400] ci/cd 옵션 변경
wonjunYou Oct 19, 2024
452723a
[IDLE-400] prod 환경 fcm service 설정을 위한 path 수정
wonjunYou Oct 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/dev-server-deployer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ jobs:
--cidr ${{ steps.publicip.outputs.ip }}/32

- name: Set Up Firebase Service Key
run : |
mkdir ./src/main/resources/firebase
echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > ./src/main/resources/firebase/adminkey.json
run: |
mkdir ./idle-infrastructure/fcm/src/main/resources/firebase
echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > /idle-infrastructure/fcm/src/main/resources/firebase/adminkey.json

- name: Copy Docker Compose file to server
uses: appleboy/scp-action@master
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dev-server-integrator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Develop Server Integrator (CI)
on:
push:
branches:
- develop
- feat/IDLE-400

jobs:
build_and_push:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/prod-server-deployer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ jobs:

- name: Set Up Firebase Service Key
run: |
mkdir ./idle-infrastructure/fcm/main/resources/firebase
echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > /idle-infrastructure/fcm/main/resources/firebase/adminkey.json
mkdir ./idle-infrastructure/fcm/src/main/resources/firebase
echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > ./idle-infrastructure/fcm/src/main/resources/firebase/adminkey.json

- name: Run Docker
uses: appleboy/ssh-action@master
Expand Down
1 change: 1 addition & 0 deletions idle-application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
implementation(project(":idle-infrastructure:aws"))
implementation(project(":idle-infrastructure:client"))
implementation(project(":idle-infrastructure:sms"))
implementation(project(":idle-infrastructure:fcm"))
implementation(project(":idle-support:common"))
implementation(project(":idle-support:security"))
implementation(project(":idle-support:transfer"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swm.idle.application.applys.domain

import com.swm.idle.domain.applys.event.ApplyEvent
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service

@Service
class CarerApplyEventPublisher(
private val eventPublisher: ApplicationEventPublisher,
) {

fun publish(applyEvent: ApplyEvent) {
eventPublisher.publishEvent(applyEvent)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class CarerApplyService(
jobPostingId: UUID,
carerId: UUID,
applyMethodType: ApplyMethodType,
) {
carerApplyJpaRepository.save(
): Applys {
return carerApplyJpaRepository.save(
Applys(
jobPostingId = jobPostingId,
carerId = carerId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,101 @@
package com.swm.idle.application.applys.facade

import com.swm.idle.application.applys.domain.CarerApplyEventPublisher
import com.swm.idle.application.applys.domain.CarerApplyService
import com.swm.idle.application.applys.vo.CarerApplyNotificationInfo
import com.swm.idle.application.common.security.getUserAuthentication
import com.swm.idle.application.jobposting.domain.JobPostingService
import com.swm.idle.application.notification.domain.DeviceTokenService
import com.swm.idle.application.notification.domain.NotificationService
import com.swm.idle.application.user.carer.domain.CarerService
import com.swm.idle.application.user.center.service.domain.CenterManagerService
import com.swm.idle.application.user.center.service.domain.CenterService
import com.swm.idle.domain.applys.event.ApplyEvent
import com.swm.idle.domain.applys.exception.ApplyException
import com.swm.idle.domain.common.enums.EntityStatus
import com.swm.idle.domain.jobposting.entity.jpa.JobPosting
import com.swm.idle.domain.jobposting.enums.ApplyMethodType
import com.swm.idle.domain.notification.enums.NotificationType
import com.swm.idle.domain.user.center.enums.CenterManagerAccountStatus
import com.swm.idle.domain.user.center.vo.BusinessRegistrationNumber
import com.swm.idle.domain.user.common.vo.BirthYear
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
@Transactional(readOnly = true)
class CarerApplyFacadeService(
private val carerApplyService: CarerApplyService,
private val carerApplyEventPublisher: CarerApplyEventPublisher,
private val deviceTokenService: DeviceTokenService,
private val jobPostingService: JobPostingService,
private val carerService: CarerService,
private val centerManagerService: CenterManagerService,
private val centerService: CenterService,
private val notificationService: NotificationService,
) {

@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createApply(
jobPostingId: UUID,
applyMethodType: ApplyMethodType,
) {
val carerId = getUserAuthentication().userId
val carer = getUserAuthentication().userId.let {
carerService.getById(it)
}
val jobPosting = jobPostingService.getById(jobPostingId)

if (carerApplyService.existsByJobPostingIdAndCarerId(
jobPostingId = jobPostingId,
carerId = carerId,
)
) {
if (carerApplyService.existsByJobPostingIdAndCarerId(jobPostingId, carer.id)) {
throw ApplyException.AlreadyApplied()
}

carerApplyService.create(
jobPostingId = jobPostingId,
carerId = carerId,
applyMethodType = applyMethodType,
)
val centerManagers = centerService.getById(jobPosting.centerId).let {
centerManagerService.findAllByCenterBusinessRegistrationNumber(
BusinessRegistrationNumber(it.businessRegistrationNumber)
)?.filter { centerManager ->
centerManager.status == CenterManagerAccountStatus.APPROVED && centerManager.entityStatus == EntityStatus.ACTIVE
}
}

centerManagers?.map { centerManager ->
val deviceToken = deviceTokenService.findByUserId(centerManager.id)

if (deviceToken != null) {
val notificationInfo = CarerApplyNotificationInfo(
title = "${carer.name} 님이 공고에 지원하였습니다.",
body = createBodyMessage(jobPosting),
receiverId = centerManager.id,
notificationType = NotificationType.APPLICANT,
imageUrl = carer.profileImageUrl,
notificationDetails = mapOf(
"jobPostingId" to jobPostingId,
)
)

val notification = notificationService.create(notificationInfo)

ApplyEvent.createApplyEvent(
deviceToken = deviceToken,
notificationId = notification.id,
notificationInfo = notificationInfo
).also {
carerApplyEventPublisher.publish(it)
}
}
}
Comment on lines +62 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null 안전성 개선 및 오류 로깅 추가 제안

centerManagers가 null일 경우를 처리하지 않고 있습니다. 또한, 알림 발송 과정에서 발생할 수 있는 예외 처리가 없습니다. 다음과 같은 개선을 제안합니다:

  1. Null 안전성 개선:
centerManagers?.forEach { centerManager ->
    // ... 기존 코드 ...
} ?: run {
    logger.warn("No approved center managers found for job posting $jobPostingId")
}
  1. 오류 로깅 추가:
try {
    // 알림 생성 및 이벤트 발행 코드
} catch (e: Exception) {
    logger.error("Failed to send notification for job posting $jobPostingId", e)
}

이러한 변경으로 코드의 안정성과 디버깅 용이성이 향상될 것입니다.

carerApplyService.create(jobPostingId, carer.id, applyMethodType)
}

private fun createBodyMessage(jobPosting: JobPosting): String {
val filteredLotNumberAddress = jobPosting.lotNumberAddress.split(" ")
.take(3)
.joinToString(" ")

return "$filteredLotNumberAddress " +
"${jobPosting.careLevel}등급 " +
"${BirthYear.calculateAge(jobPosting.birthYear)}세 " +
jobPosting.gender.value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.swm.idle.application.applys.vo

import com.swm.idle.domain.notification.enums.NotificationType
import com.swm.idle.domain.notification.jpa.NotificationInfo
import java.util.*

data class CarerApplyNotificationInfo(
override val title: String,
override val body: String,
override val receiverId: UUID,
override val notificationType: NotificationType,
override val imageUrl: String?,
override val notificationDetails: Map<String, Any>?,
) : NotificationInfo {

companion object {

fun create(
title: String,
body: String,
receiverId: UUID,
notificationType: NotificationType,
imageUrl: String?,
jobPostingId: UUID,
): CarerApplyNotificationInfo {
val notificationDetails = mapOf(
"jobPostingId" to jobPostingId,
)
return CarerApplyNotificationInfo(
title,
body,
receiverId,
notificationType,
imageUrl,
notificationDetails,
)
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import com.swm.idle.application.user.center.service.domain.CenterService
import com.swm.idle.domain.common.dto.JobPostingPreviewDto
import com.swm.idle.domain.common.enums.EntityStatus
import com.swm.idle.domain.user.carer.entity.jpa.Carer
import com.swm.idle.support.transfer.common.CursorScrollRequest
import com.swm.idle.support.transfer.jobposting.carer.CarerAppliedJobPostingScrollResponse
import com.swm.idle.support.transfer.jobposting.carer.CarerJobPostingResponse
import com.swm.idle.support.transfer.jobposting.carer.CarerJobPostingScrollResponse
import com.swm.idle.support.transfer.jobposting.carer.CursorScrollRequest
import com.swm.idle.support.transfer.jobposting.carer.JobPostingFavoriteResponse
import org.locationtech.jts.geom.Point
import org.springframework.stereotype.Service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import com.swm.idle.application.user.carer.domain.CarerService
import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto
import com.swm.idle.domain.user.carer.entity.jpa.Carer
import com.swm.idle.infrastructure.client.geocode.service.GeoCodeService
import com.swm.idle.support.transfer.common.CursorScrollRequest
import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingFavoriteResponse
import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingScrollResponse
import com.swm.idle.support.transfer.jobposting.carer.CursorScrollRequest
import com.swm.idle.support.transfer.jobposting.common.CrawlingJobPostingResponse
import org.locationtech.jts.geom.Point
import org.springframework.stereotype.Service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ data class JobPostingFavoriteFacadeService(
carerId: UUID,
jobPostingType: JobPostingType,
) {
jobPostingFavoriteService.create(
jobPostingFavoriteService.findByJobPostingIdAndCarerId(
jobPostingId = jobPostingId,
carerId = carerId,
)?.let {
it.active()
} ?: jobPostingFavoriteService.create(
jobPostingId = jobPostingId,
carerId = carerId,
jobPostingType = jobPostingType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.swm.idle.application.notification.domain

import com.swm.idle.domain.notification.jpa.DeviceToken
import com.swm.idle.domain.notification.repository.DeviceTokenJpaRepository
import com.swm.idle.domain.notification.repository.jpa.DeviceTokenJpaRepository
import com.swm.idle.domain.user.common.enum.UserType
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -17,6 +17,10 @@ class DeviceTokenService(
return deviceTokenJpaRepository.findByDeviceToken(deviceToken)
}

fun findByUserId(userId: UUID): DeviceToken? {
return deviceTokenJpaRepository.findByUserId(userId)
}

@Transactional
fun updateDeviceTokenUserId(
deviceToken: DeviceToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.swm.idle.application.notification.domain

import com.swm.idle.domain.common.dto.NotificationQueryDto
import com.swm.idle.domain.common.exception.PersistenceException
import com.swm.idle.domain.notification.jpa.Notification
import com.swm.idle.domain.notification.jpa.NotificationInfo
import com.swm.idle.domain.notification.repository.jpa.NotificationJpaRepository
import com.swm.idle.domain.notification.repository.querydsl.NotificationQueryRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
class NotificationService(
private val notificationJpaRepository: NotificationJpaRepository,
private val notificationQueryRepository: NotificationQueryRepository,
) {

@Transactional
fun create(notificationInfo: NotificationInfo): Notification {
return notificationJpaRepository.save(
Notification(
isRead = false,
title = notificationInfo.title,
body = notificationInfo.body,
notificationType = notificationInfo.notificationType,
receiverId = notificationInfo.receiverId,
imageUrl = notificationInfo.imageUrl,
notificationDetails = notificationInfo.notificationDetails
)
)
}
Comment on lines +21 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Notification 생성 시 필드 검증 로직 추가 필요

notificationInfo의 필드 값에 대한 유효성 검사가 이루어지지 않고 있습니다. 중요한 필드에 대한 Null 체크나 형식 검증 등을 추가하여 데이터 무결성을 확보하는 것이 좋습니다.


fun getById(notificationId: UUID): Notification {
return notificationJpaRepository.findByIdOrNull(notificationId)
?: throw PersistenceException.ResourceNotFound("Notification(=$notificationId) 를 찾을 수 없습니다.")
}

@Transactional
fun read(notification: Notification) {
notification.read()
}

fun countUnreadNotificationByUserId(userId: UUID): Int {
return notificationJpaRepository.countByUserIdWithUnreadStatus(userId)
}

fun findAllByUserId(
next: UUID?,
limit: Long,
userId: UUID,
): List<NotificationQueryDto> {
return notificationQueryRepository.findAllByUserId(
next = next,
limit = limit,
userId = userId,
)
}
Comment on lines +49 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

'limit' 파라미터에 대한 유효성 검사 추가 권장

limit 값이 비정상적으로 큰 경우 시스템 성능에 영향이 있을 수 있습니다. 최대 허용 범위를 설정하고 이에 대한 유효성 검사를 추가하여 안전성을 강화하는 것이 좋습니다.


}
Loading
Loading