Skip to content

Commit c214e37

Browse files
authored
Merge pull request #294 from YAPP-Github/feat/#293
[feat #293] 공유 포킷 관련 푸시발송 구현
2 parents 2a3cee0 + b3b1735 commit c214e37

File tree

19 files changed

+297
-13
lines changed

19 files changed

+297
-13
lines changed

adapters/in-web/src/main/kotlin/com/pokit/notification/dto/response/NotificationResponse.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ data class NotificationResponse(
2020
@Schema(description = "알림 본문", example = "OO님이 추가한 링크를 지금 확인해보세요")
2121
val body: String,
2222

23-
@Schema(description = "썸네일 URL (없을 수 있음)", nullable = true)
24-
val thumbnailUrl: String?,
23+
@Schema(description = "카테고리 이미지 URL (없을 수 있음)", nullable = true)
24+
val categoryImageUrl: String?,
2525

2626
@Schema(description = "읽음 여부")
2727
val isRead: Boolean,
@@ -41,7 +41,7 @@ internal fun Notification.toResponse() = NotificationResponse(
4141
notificationType = this.notificationType,
4242
title = this.title,
4343
body = this.body,
44-
thumbnailUrl = this.thumbnailUrl,
44+
categoryImageUrl = this.categoryImageUrl,
4545
isRead = this.isRead,
4646
navigationType = this.navigationType,
4747
deepLink = this.deepLink,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.pokit.out.persistence.notification.impl
2+
3+
import com.pokit.notification.model.NotificationType
4+
import com.pokit.notification.model.PushMessageTemplate
5+
import com.pokit.notification.port.out.PushMessageTemplatePort
6+
import com.pokit.out.persistence.notification.persist.PushMessageTemplateRepository
7+
import com.pokit.out.persistence.notification.persist.toDomain
8+
import org.springframework.stereotype.Repository
9+
10+
@Repository
11+
class PushMessageTemplateAdapter(
12+
private val pushMessageTemplateRepository: PushMessageTemplateRepository,
13+
) : PushMessageTemplatePort {
14+
15+
override fun loadByType(type: NotificationType): PushMessageTemplate? {
16+
return pushMessageTemplateRepository.findByNotificationType(type)?.toDomain()
17+
}
18+
}

adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/notification/persist/NotificationEntity.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class NotificationEntity(
2727
@Column(name = "body", nullable = false, columnDefinition = "TEXT")
2828
val body: String,
2929

30-
@Column(name = "thumbnail_url", length = 2048)
31-
val thumbnailUrl: String? = null,
30+
@Column(name = "category_image_url", length = 2048)
31+
val categoryImageUrl: String? = null,
3232

3333
@Column(name = "is_read", nullable = false)
3434
val isRead: Boolean = false,
@@ -51,7 +51,7 @@ class NotificationEntity(
5151
notificationType = notification.notificationType,
5252
title = notification.title,
5353
body = notification.body,
54-
thumbnailUrl = notification.thumbnailUrl,
54+
categoryImageUrl = notification.categoryImageUrl,
5555
isRead = notification.isRead,
5656
navigationType = notification.navigationType,
5757
deepLink = notification.deepLink,
@@ -66,7 +66,7 @@ fun NotificationEntity.toDomain() = Notification(
6666
notificationType = this.notificationType,
6767
title = this.title,
6868
body = this.body,
69-
thumbnailUrl = this.thumbnailUrl,
69+
categoryImageUrl = this.categoryImageUrl,
7070
isRead = this.isRead,
7171
navigationType = this.navigationType,
7272
deepLink = this.deepLink,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.pokit.out.persistence.notification.persist
2+
3+
import com.pokit.notification.model.NavigationType
4+
import com.pokit.notification.model.NotificationType
5+
import com.pokit.notification.model.PushMessageTemplate
6+
import com.pokit.out.persistence.BaseEntity
7+
import jakarta.persistence.*
8+
9+
@Table(name = "push_message_template")
10+
@Entity
11+
class PushMessageTemplateEntity(
12+
@Id
13+
@GeneratedValue(strategy = GenerationType.IDENTITY)
14+
@Column(name = "id")
15+
val id: Long = 0L,
16+
17+
@Enumerated(EnumType.STRING)
18+
@Column(name = "notification_type", nullable = false, length = 50)
19+
val notificationType: NotificationType,
20+
21+
@Column(name = "title", nullable = false, length = 255)
22+
val title: String,
23+
24+
@Column(name = "body", nullable = false, columnDefinition = "TEXT")
25+
val body: String,
26+
27+
@Enumerated(EnumType.STRING)
28+
@Column(name = "navigation_type", nullable = false, length = 50)
29+
val navigationType: NavigationType,
30+
) : BaseEntity() {
31+
32+
companion object {
33+
fun of(pushMessageTemplate: PushMessageTemplate) = PushMessageTemplateEntity(
34+
id = pushMessageTemplate.id,
35+
notificationType = pushMessageTemplate.notificationType,
36+
title = pushMessageTemplate.title,
37+
body = pushMessageTemplate.body,
38+
navigationType = pushMessageTemplate.navigationType,
39+
)
40+
}
41+
}
42+
43+
fun PushMessageTemplateEntity.toDomain() = PushMessageTemplate(
44+
id = this.id,
45+
notificationType = this.notificationType,
46+
title = this.title,
47+
body = this.body,
48+
navigationType = this.navigationType,
49+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.pokit.out.persistence.notification.persist
2+
3+
import com.pokit.notification.model.NotificationType
4+
import org.springframework.data.jpa.repository.JpaRepository
5+
6+
interface PushMessageTemplateRepository : JpaRepository<PushMessageTemplateEntity, Long> {
7+
fun findByNotificationType(notificationType: NotificationType): PushMessageTemplateEntity?
8+
}

adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/FcmTokenAdapter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ class FcmTokenAdapter(
2424
override fun loadByUserIdAndToken(userId: Long, token: String): FcmToken? {
2525
return fcmTokenRepository.findByUserIdAndTokenAndDeleted(userId, token, false)?.toDomain()
2626
}
27+
28+
override fun loadLatestByUserId(userId: Long): FcmToken? {
29+
return fcmTokenRepository.findTopByUserIdAndDeletedOrderByIdDesc(userId, false)?.toDomain()
30+
}
2731
}

adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/FcmTokenRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ interface FcmTokenRepository : JpaRepository<FcmTokenEntity, Long> {
66
fun findByUserId(userId: Long): List<FcmTokenEntity>
77

88
fun findByUserIdAndTokenAndDeleted(userId: Long, token: String, isDeleted: Boolean): FcmTokenEntity?
9+
10+
fun findTopByUserIdAndDeletedOrderByIdDesc(userId: Long, deleted: Boolean): FcmTokenEntity?
911
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.pokit.notification.impl
2+
3+
import com.google.auth.oauth2.GoogleCredentials
4+
import com.google.firebase.FirebaseApp
5+
import com.google.firebase.FirebaseOptions
6+
import com.google.firebase.messaging.FirebaseMessaging
7+
import com.google.firebase.messaging.FirebaseMessagingException
8+
import com.google.firebase.messaging.Message
9+
import com.pokit.notification.model.Notification
10+
import com.pokit.notification.port.out.NotificationSender
11+
import io.github.oshai.kotlinlogging.KotlinLogging
12+
import jakarta.annotation.PostConstruct
13+
import org.springframework.core.io.ClassPathResource
14+
import org.springframework.stereotype.Component
15+
import com.google.firebase.messaging.Notification as FirebaseNotification
16+
17+
@Component
18+
class FcmNotificationSender : NotificationSender {
19+
@PostConstruct
20+
fun init() {
21+
if (FirebaseApp.getApps().isEmpty()) {
22+
val firebaseCredentials = ClassPathResource("/firebase/push-account-key.json").inputStream
23+
val options = FirebaseOptions.builder()
24+
.setCredentials(GoogleCredentials.fromStream(firebaseCredentials))
25+
.build()
26+
FirebaseApp.initializeApp(options)
27+
}
28+
}
29+
30+
private val logger = KotlinLogging.logger { }
31+
32+
companion object {
33+
const val IMAGE_PATH = "https://pokit-storage.s3.ap-northeast-2.amazonaws.com/logo/pokit.png" // 앱 로고
34+
}
35+
36+
override fun send(notification: Notification, tokens: List<String>) {
37+
val fcmNotification = FirebaseNotification.builder()
38+
.setTitle(notification.title)
39+
.setBody(notification.body)
40+
.setImage(IMAGE_PATH)
41+
.build()
42+
43+
val messages = tokens.map { token ->
44+
Message.builder()
45+
.setNotification(fcmNotification)
46+
.putData("deepLink", notification.deepLink)
47+
.setToken(token)
48+
.build()
49+
}
50+
51+
try {
52+
messages.forEach {
53+
FirebaseMessaging.getInstance().sendAsync(it)
54+
}
55+
} catch (e: FirebaseMessagingException) {
56+
logger.warn { "Failed to send notification: ${e.message}" }
57+
}
58+
}
59+
}

application/src/main/kotlin/com/pokit/category/port/service/CategoryService.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import com.pokit.common.exception.ClientValidationException
1818
import com.pokit.common.exception.InvalidRequestException
1919
import com.pokit.common.exception.NotFoundCustomException
2020
import com.pokit.content.port.out.ContentPort
21+
import com.pokit.notification.model.DeepLinkBuilder
22+
import com.pokit.notification.model.NotificationType
23+
import com.pokit.notification.port.`in`.NotificationUseCase
24+
import com.pokit.notification.port.out.PushMessageTemplatePort
2125
import com.pokit.user.model.User
2226
import com.pokit.user.port.out.UserPort
2327
import org.springframework.data.domain.PageRequest
@@ -36,6 +40,8 @@ class CategoryService(
3640
private val sharedCategoryPort: SharedCategoryPort,
3741
private val userPort: UserPort,
3842
private val bookmarkPort: BookmarkPort,
43+
private val notificationUseCase: NotificationUseCase,
44+
private val pushMessageTemplatePort: PushMessageTemplatePort,
3945
) : CategoryUseCase {
4046
companion object {
4147
private const val MAX_CATEGORY_COUNT = 30
@@ -202,6 +208,26 @@ class CategoryService(
202208
categoryId = category.categoryId,
203209
)
204210
sharedCategoryPort.persist(sharedCategory)
211+
212+
val template = pushMessageTemplatePort.loadByType(NotificationType.NEW_MEMBER_JOINED)
213+
if (template != null) {
214+
val joiningUser = userPort.loadById(userId)
215+
if (joiningUser != null) {
216+
val sharedMembers = sharedCategoryPort.loadByCategoryId(category.categoryId)
217+
sharedMembers
218+
.filter { it.userId != userId }
219+
.forEach { member ->
220+
val notification = template.toNotification(
221+
userId = member.userId,
222+
categoryName = category.categoryName,
223+
nickname = joiningUser.nickName,
224+
categoryImageUrl = category.categoryImage.imageUrl,
225+
deepLink = DeepLinkBuilder.forCategory(category.categoryId, userId),
226+
)
227+
notificationUseCase.createAndSend(notification)
228+
}
229+
}
230+
}
205231
}
206232

207233
@Transactional
@@ -217,6 +243,16 @@ class CategoryService(
217243

218244
category.minusUserCount() // 포킷 인원수 감소
219245
categoryPort.persist(category)
246+
247+
val template = pushMessageTemplatePort.loadByType(NotificationType.POKIT_USE_RESTRICTION)
248+
if (template != null) {
249+
val notification = template.toNotification(
250+
userId = resignUserId,
251+
categoryName = category.categoryName,
252+
categoryImageUrl = category.categoryImage.imageUrl,
253+
)
254+
notificationUseCase.createAndSend(notification)
255+
}
220256
}
221257

222258
@Transactional

application/src/main/kotlin/com/pokit/content/port/service/ContentService.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.pokit.category.model.CategoryStatus
1111
import com.pokit.category.model.CategoryStatus.UNCATEGORIZED
1212
import com.pokit.category.model.OpenType
1313
import com.pokit.category.port.out.CategoryPort
14+
import com.pokit.category.port.out.SharedCategoryPort
1415
import com.pokit.common.exception.AlreadyExistsException
1516
import com.pokit.common.exception.ClientValidationException
1617
import com.pokit.common.exception.NotFoundCustomException
@@ -30,6 +31,10 @@ import com.pokit.content.port.out.ReportedContentPort
3031
import com.pokit.log.model.LogType
3132
import com.pokit.log.model.UserLog
3233
import com.pokit.log.port.out.UserLogPort
34+
import com.pokit.notification.model.DeepLinkBuilder
35+
import com.pokit.notification.model.NotificationType
36+
import com.pokit.notification.port.`in`.NotificationUseCase
37+
import com.pokit.notification.port.out.PushMessageTemplatePort
3338
import com.pokit.user.exception.UserErrorCode
3439
import com.pokit.user.model.InterestType
3540
import com.pokit.user.model.User
@@ -56,6 +61,9 @@ class ContentService(
5661
private val interestPort: InterestPort,
5762
private val userPort: UserPort,
5863
private val reportedContentPort: ReportedContentPort,
64+
private val sharedCategoryPort: SharedCategoryPort,
65+
private val notificationUseCase: NotificationUseCase,
66+
private val pushMessageTemplatePort: PushMessageTemplatePort,
5967
) : ContentUseCase {
6068
companion object {
6169
private const val MIN_CONTENT_COUNT = 3
@@ -86,6 +94,25 @@ class ContentService(
8694
publisher.publishEvent(CreateAlertRequest(userId = user.id, contetId = contentResult.contentId))
8795
}
8896

97+
if (category.isShared) {
98+
val template = pushMessageTemplatePort.loadByType(NotificationType.LINK_ADDED)
99+
if (template != null) {
100+
val sharedMembers = sharedCategoryPort.loadByCategoryId(category.categoryId)
101+
sharedMembers
102+
.filter { it.userId != user.id }
103+
.forEach { member ->
104+
val notification = template.toNotification(
105+
userId = member.userId,
106+
categoryName = category.categoryName,
107+
nickname = user.nickName,
108+
categoryImageUrl = category.categoryImage.imageUrl,
109+
deepLink = DeepLinkBuilder.forContent(category.categoryId, contentResult.contentId),
110+
)
111+
notificationUseCase.createAndSend(notification)
112+
}
113+
}
114+
}
115+
89116
return contentResult
90117
}
91118

0 commit comments

Comments
 (0)