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

[feat] 알림 세팅 및 요청 관련 알림 구현 #152

Merged
merged 3 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/
application-secret.yaml
firebase-service-key.json

### STS ###
.apt_generated
Expand Down
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ dependencies {

// random
implementation("org.apache.commons:commons-lang3:3.12.0")

// notification
implementation("com.google.firebase:firebase-admin:9.2.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
}


Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/psr/psr/global/Constant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,11 @@ class Constant {
const val POPULAR = "인기순"
}
}

class NotiSentence{
companion object NotiSentence{
const val NEW_ORDER_SENTENCE = "님의 요청을 확인해주세요!"
const val TWO_MONTH_ORDER_SENTENCE = "님의 요청 상태를 확인해주세요!"
}
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/psr/psr/notification/dto/Data.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.psr.psr.notification.dto

data class Data(
val relatedId: Long,
val notiType: String
)
6 changes: 6 additions & 0 deletions src/main/kotlin/com/psr/psr/notification/dto/FcmMessage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.psr.psr.notification.dto

data class FcmMessage (
val validate_only: Boolean,
val message: Message
)
7 changes: 7 additions & 0 deletions src/main/kotlin/com/psr/psr/notification/dto/Message.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.psr.psr.notification.dto

data class Message(
val notification: Notification,
val token: String,
val data: Data
)
48 changes: 48 additions & 0 deletions src/main/kotlin/com/psr/psr/notification/dto/NotiAssembler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.psr.psr.notification.dto

import com.psr.psr.notification.entity.NotificationType
import com.psr.psr.notification.entity.PushNotification
import com.psr.psr.user.entity.User
import org.springframework.stereotype.Component


@Component
class NotiAssembler {
fun toEntity(receiver: User, title: String, content: String, relatedId: Long, type: NotificationType): PushNotification {
return PushNotification(
user = receiver,
title = title,
content = content,
relatedId = relatedId,
type = type
)
}
fun toMessageDTO(targetToken: String, title: String, body: String, relatedId: Long, notiType: String): Message {
return Message(
notification = toNotificationDTO(title, body),
token = targetToken,
data = toDataDTO(relatedId, notiType)
)
}

fun toNotificationDTO(title: String, body: String): Notification {
return Notification(
title = title,
body = body
)
}

fun toDataDTO(relatedId: Long, notiType: String): Data {
return Data(
relatedId = relatedId,
notiType = notiType
)
}

fun makeMessage(targetToken: String, title: String, body: String, relatedId: Long, notiType: String): FcmMessage {
return FcmMessage(
validate_only = false,
message = toMessageDTO(targetToken, title, body, relatedId, notiType)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.psr.psr.notification.dto

data class Notification(
val title: String,
val body: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.psr.psr.notification.entity

enum class NotificationType {
NEW_ORDER,
CHANGED_ORDER_STATUS,
TWO_MONTH_ORDER,
CHAT
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import jakarta.persistence.*
import org.jetbrains.annotations.NotNull

@Entity
data class Notification(
data class PushNotification(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long,
var id: Long? = null,

@ManyToOne
@JoinColumn(nullable = false, name = "user_id")
Expand All @@ -19,6 +19,12 @@ data class Notification(
var title: String,

@NotNull
var content: String
var content: String,

// 알림의 주체인 요청, 채팅 등의 ID
var relatedId: Long,

@NotNull
var type: NotificationType

) : BaseEntity()
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.psr.psr.notification.repository

import com.psr.psr.notification.entity.Notification
import com.psr.psr.notification.entity.PushNotification
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface NotificationRepository: JpaRepository<Notification, Long>, NotificationCustom {
interface NotificationRepository: JpaRepository<PushNotification, Long>, NotificationCustom {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.psr.psr.notification.repository

import com.psr.psr.notification.dto.NotiList
import com.psr.psr.notification.dto.NotificationListRes
import com.psr.psr.notification.entity.QNotification.notification
import com.psr.psr.notification.entity.QPushNotification.pushNotification
import com.psr.psr.user.entity.User
import com.querydsl.core.group.GroupBy.groupBy
import com.querydsl.core.group.GroupBy.list
Expand All @@ -23,19 +23,19 @@ class NotificationRepositoryImpl(
override fun findNotificationByUserGroupByDate(user: User, pageable: Pageable): Page<NotificationListRes> {
val formattedDate: StringTemplate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
notification.createdAt,
pushNotification.createdAt,
ConstantImpl.create("%Y-%m-%d")
)

val result = queryFactory
.selectFrom(notification)
.where(notification.user.eq(user))
.orderBy(notification.id.desc())
.selectFrom(pushNotification)
.where(pushNotification.user.eq(user))
.orderBy(pushNotification.id.desc())
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
.transform(groupBy(formattedDate)
.list(Projections.constructor(NotificationListRes::class.java, formattedDate,
list(Projections.constructor(NotiList::class.java, notification.title, notification.content)))))
list(Projections.constructor(NotiList::class.java, pushNotification.title, pushNotification.content)))))
return PageImpl(result, pageable, result.size.toLong())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,132 @@
package com.psr.psr.notification.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.auth.oauth2.GoogleCredentials
import com.psr.psr.global.Constant.NotiSentence.NotiSentence.NEW_ORDER_SENTENCE
import com.psr.psr.global.Constant.NotiSentence.NotiSentence.TWO_MONTH_ORDER_SENTENCE
import com.psr.psr.notification.dto.FcmMessage
import com.psr.psr.notification.dto.NotiAssembler
import com.psr.psr.notification.dto.NotificationListRes
import com.psr.psr.notification.entity.NotificationType
import com.psr.psr.notification.repository.NotificationRepository
import com.psr.psr.order.entity.OrderStatus
import com.psr.psr.user.entity.User
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.io.ClassPathResource
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Service

@Service
class NotificationService(
private val notificationRepository: NotificationRepository
private val notificationRepository: NotificationRepository,
private val notiAssembler: NotiAssembler,
@Value("\${firebase.sendUrl}") private val sendUrl: String,
private val objectMapper: ObjectMapper
) {
// 알림 목록 조회
fun getNotiList(user: User, pageable: Pageable): Page<NotificationListRes> {
return notificationRepository.findNotificationByUserGroupByDate(user, pageable)
}

// 새로운 요청 알림
fun sendNewOrderNoti(productName: String, orderReceiver: User, ordererName: String, orderId: Long) {
val messageBody = ordererName + NEW_ORDER_SENTENCE
notificationRepository.save(notiAssembler.toEntity(
orderReceiver,
productName,
messageBody,
orderId,
NotificationType.NEW_ORDER
))

if (isPushNotiAvailable(orderReceiver)) {
val message: FcmMessage = notiAssembler.makeMessage(
orderReceiver.deviceToken!!,
productName,
messageBody,
orderId,
NotificationType.NEW_ORDER.name
)
sendMessage(objectMapper.writeValueAsString(message))
}
}

// 요청 상태 변경 알림
fun sendChangeOrderStatusNoti(productName: String, orderer: User, orderStatus: OrderStatus, orderId: Long) {
val messageBody = orderStatus.notiSentence!!
notificationRepository.save(notiAssembler.toEntity(
orderer,
productName,
messageBody,
orderId,
NotificationType.CHANGED_ORDER_STATUS
))

if (isPushNotiAvailable(orderer)) {
val message: FcmMessage = notiAssembler.makeMessage(
orderer.deviceToken!!,
productName,
messageBody,
orderId,
NotificationType.CHANGED_ORDER_STATUS.name
)
sendMessage(objectMapper.writeValueAsString(message))
}
}

// 2달 뒤 요청상태 입력 요망 알림
fun send2MonthOrderNoti(productName: String, orderer: User, ordererName: String, orderId: Long) {
val messageBody = ordererName + TWO_MONTH_ORDER_SENTENCE
notificationRepository.save(notiAssembler.toEntity(
orderer,
productName,
messageBody,
orderId,
NotificationType.TWO_MONTH_ORDER
))

if (isPushNotiAvailable(orderer)) {
val message: FcmMessage = notiAssembler.makeMessage(
orderer.deviceToken!!,
productName,
messageBody,
orderId,
NotificationType.TWO_MONTH_ORDER.name
)
sendMessage(objectMapper.writeValueAsString(message))
}
}

// 알림 수신 상태 체크
fun isPushNotiAvailable(user: User): Boolean {
return user.deviceToken != null && user.notification
}

// firebase accessToken 발급
private fun getAccessToken(): String? {
val firebaseConfigPath = "firebase-service-key.json"
val googleCredentials = GoogleCredentials
.fromStream(ClassPathResource(firebaseConfigPath).inputStream)
.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
googleCredentials.refreshIfExpired()
return googleCredentials.accessToken.tokenValue
}

// 메세지 전송
private fun sendMessage(message: String): Response {
val client = OkHttpClient()
val requestBody: RequestBody = message.toRequestBody("application/json; charset=utf-8".toMediaType())
val request: Request = Request.Builder()
.url(sendUrl)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build()
return client.newCall(request).execute()
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/com/psr/psr/order/dto/OrderAssembler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class OrderAssembler {
fun toOrderResDTO(order: Order, isSeller: Boolean): OrderRes {
return OrderRes(
isSeller = isSeller,
status = order.orderStatus.statusName,
status = order.orderStatus.value,
orderUserId = order.user.id!!,
orderDate = order.createdAt.format(DateTimeFormatter.ISO_DATE),
productId = order.product.id!!,
Expand Down
15 changes: 8 additions & 7 deletions src/main/kotlin/com/psr/psr/order/entity/OrderStatus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package com.psr.psr.order.entity

import com.psr.psr.global.exception.BaseException
import com.psr.psr.global.exception.BaseResponseCode
import com.psr.psr.global.resolver.EnumType

enum class OrderStatus(val statusName: String) {
ORDER_WAITING("요청대기"),
PROGRESSING("진행중"),
COMPLETED("진행완료"),
CANCELED("요청취소");
enum class OrderStatus(override val value: String, val notiSentence: String?): EnumType {
ORDER_WAITING("요청대기", null),
PROGRESSING("진행중", "요청이 진행되었습니다"),
COMPLETED("진행완료", "요청이 진행 완료되었습니다"),
CANCELED("요청취소", "요청이 취소되었습니다");
Comment on lines +8 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

👏👏👏

Copy link
Member

Choose a reason for hiding this comment

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

🤍


companion object {
fun findByName(statusName: String): OrderStatus {
return OrderStatus.values().find { it.statusName == statusName }
fun findByValue(value: String): OrderStatus {
return enumValues<OrderStatus>().find { it.value == value }
?: throw BaseException(BaseResponseCode.INVALID_ORDER_STATUS)
}
}
Expand Down
Loading
Loading