Skip to content

Commit fcbfc49

Browse files
feat : prod 반영 (#184)
Co-authored-by: Kijun Kwon <39583312+kkjsw17@users.noreply.github.com>
1 parent 1d4adab commit fcbfc49

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1019
-9
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.depromeet.clog.server.api.fcm.application
2+
3+
import org.depromeet.clog.server.domain.fcm.FcmTokenRepository
4+
import org.depromeet.clog.server.domain.fcm.SaveFcmTokenCommand
5+
import org.springframework.stereotype.Service
6+
import org.springframework.transaction.annotation.Transactional
7+
8+
@Service
9+
@Transactional
10+
class SaveFcmToken(
11+
private val fcmTokenRepository: FcmTokenRepository
12+
) {
13+
operator fun invoke(command: SaveFcmTokenCommand) {
14+
fcmTokenRepository.upsert(command)
15+
}
16+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.depromeet.clog.server.api.fcm.presentation
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.tags.Tag
5+
import org.depromeet.clog.server.api.configuration.ApiConstants
6+
import org.depromeet.clog.server.api.fcm.application.SaveFcmToken
7+
import org.depromeet.clog.server.api.user.UserContext
8+
import org.depromeet.clog.server.domain.common.ClogApiResponse
9+
import org.springframework.web.bind.annotation.PostMapping
10+
import org.springframework.web.bind.annotation.RequestBody
11+
import org.springframework.web.bind.annotation.RequestMapping
12+
import org.springframework.web.bind.annotation.RestController
13+
14+
@Tag(name = "FCM API", description = "FCM 푸시 토큰 저장 관련 API")
15+
@RequestMapping("${ApiConstants.API_BASE_PATH_V1}/fcm")
16+
@RestController
17+
class FcmTokenCommandController(
18+
private val saveFcmToken: SaveFcmToken
19+
) {
20+
21+
@Operation(summary = "FCM 토큰 저장", description = "앱에서 발급받은 FCM 토큰을 저장합니다.")
22+
@PostMapping("/token")
23+
fun saveFcmToken(
24+
userContext: UserContext,
25+
@RequestBody request: SaveFcmTokenRequest,
26+
): ClogApiResponse<Unit> {
27+
val command = request.toCommand(userContext.userId)
28+
saveFcmToken(command)
29+
return ClogApiResponse.from(Unit)
30+
}
31+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.depromeet.clog.server.api.fcm.presentation
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import org.depromeet.clog.server.domain.fcm.SaveFcmTokenCommand
5+
6+
@Schema(description = "FCM 토큰 저장 요청")
7+
data class SaveFcmTokenRequest(
8+
9+
@Schema(description = "FCM 토큰", example = "fcm_token_example_123")
10+
val token: String,
11+
12+
@Schema(description = "디바이스 정보", example = "iPhone 15 Pro")
13+
val device: String? = null,
14+
) {
15+
fun toCommand(userId: Long): SaveFcmTokenCommand {
16+
return SaveFcmTokenCommand(
17+
userId = userId,
18+
token = this.token,
19+
device = this.device
20+
)
21+
}
22+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.depromeet.clog.server.api.notification.application
2+
3+
import org.depromeet.clog.server.domain.notification.NotificationRepository
4+
import org.springframework.stereotype.Service
5+
import org.springframework.transaction.annotation.Transactional
6+
7+
@Service
8+
@Transactional(readOnly = true)
9+
class CheckUnreadNotificationExist(
10+
private val notificationRepository: NotificationRepository
11+
) {
12+
operator fun invoke(userId: Long): Boolean {
13+
return notificationRepository.existsUnreadByUserId(userId)
14+
}
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.depromeet.clog.server.api.notification.application
2+
3+
import org.depromeet.clog.server.domain.notification.NotificationNotFoundException
4+
import org.depromeet.clog.server.domain.notification.NotificationRepository
5+
import org.springframework.stereotype.Service
6+
import org.springframework.transaction.annotation.Transactional
7+
8+
@Service
9+
@Transactional
10+
class DeleteNotification(
11+
private val notificationRepository: NotificationRepository
12+
) {
13+
operator fun invoke(userId: Long, notificationId: Long) {
14+
val deletedCount = notificationRepository.deleteByIdAndUserId(notificationId, userId)
15+
if (deletedCount == 0) {
16+
throw NotificationNotFoundException()
17+
}
18+
}
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.depromeet.clog.server.api.notification.application
2+
3+
import org.depromeet.clog.server.api.notification.presentation.NotificationResponse
4+
import org.depromeet.clog.server.domain.notification.NotificationRepository
5+
import org.depromeet.clog.server.domain.notification.NotificationType
6+
import org.springframework.stereotype.Service
7+
import org.springframework.transaction.annotation.Transactional
8+
import java.time.LocalDateTime
9+
10+
@Service
11+
@Transactional
12+
class GetNotifications(
13+
private val notificationRepository: NotificationRepository,
14+
) {
15+
operator fun invoke(
16+
userId: Long,
17+
page: Int,
18+
size: Int,
19+
type: NotificationType?
20+
): List<NotificationResponse> {
21+
val thresholdDate = LocalDateTime.now().minusDays(30)
22+
notificationRepository.clearNewFlags(userId)
23+
24+
val notifications = if (type != null) {
25+
notificationRepository.findByUserIdAndType(userId, type, thresholdDate, page, size)
26+
} else {
27+
notificationRepository.findRecentByUserId(userId, thresholdDate, page, size)
28+
}
29+
30+
return notifications.map { NotificationResponse.from(it) }
31+
}
32+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.depromeet.clog.server.api.notification.application
2+
3+
import org.depromeet.clog.server.domain.notification.NotificationNotFoundException
4+
import org.depromeet.clog.server.domain.notification.NotificationRepository
5+
import org.springframework.stereotype.Service
6+
import org.springframework.transaction.annotation.Transactional
7+
8+
@Service
9+
@Transactional
10+
class MarkNotificationRead(
11+
private val notificationRepository: NotificationRepository
12+
) {
13+
operator fun invoke(userId: Long, notificationId: Long) {
14+
val affected = notificationRepository.markAsRead(userId, notificationId)
15+
if (affected == 0) {
16+
throw NotificationNotFoundException()
17+
}
18+
}
19+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.depromeet.clog.server.api.notification.presentation
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.tags.Tag
5+
import org.depromeet.clog.server.api.configuration.ApiConstants
6+
import org.depromeet.clog.server.api.notification.application.DeleteNotification
7+
import org.depromeet.clog.server.api.notification.application.MarkNotificationRead
8+
import org.depromeet.clog.server.api.user.UserContext
9+
import org.depromeet.clog.server.domain.common.ClogApiResponse
10+
import org.springframework.web.bind.annotation.*
11+
12+
@Tag(name = "Notification API", description = "알림 관련 API")
13+
@RequestMapping("${ApiConstants.API_BASE_PATH_V1}/notifications")
14+
@RestController
15+
class NotificationCommandController(
16+
private val deleteNotification: DeleteNotification,
17+
private val markNotificationAsRead: MarkNotificationRead
18+
) {
19+
20+
@Operation(summary = "알림 삭제", description = "특정 알림을 삭제합니다.")
21+
@DeleteMapping("/{id}")
22+
fun deleteNotification(
23+
userContext: UserContext,
24+
@PathVariable id: Long,
25+
): ClogApiResponse<Unit> {
26+
deleteNotification(userContext.userId, id)
27+
return ClogApiResponse.from(Unit)
28+
}
29+
30+
@Operation(
31+
summary = "알림 읽음 처리",
32+
description = "알림탭에서 알림 클릭으로 직접 이동 시 호출"
33+
)
34+
@PatchMapping("/{id}/read")
35+
fun markAsRead(
36+
userContext: UserContext,
37+
@PathVariable id: Long
38+
): ClogApiResponse<Unit> {
39+
markNotificationAsRead(userContext.userId, id)
40+
return ClogApiResponse.from(Unit)
41+
}
42+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.depromeet.clog.server.api.notification.presentation
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.enums.ParameterIn
6+
import io.swagger.v3.oas.annotations.tags.Tag
7+
import org.depromeet.clog.server.api.configuration.ApiConstants
8+
import org.depromeet.clog.server.api.notification.application.CheckUnreadNotificationExist
9+
import org.depromeet.clog.server.api.notification.application.GetNotifications
10+
import org.depromeet.clog.server.api.user.UserContext
11+
import org.depromeet.clog.server.domain.common.ClogApiResponse
12+
import org.depromeet.clog.server.domain.notification.NotificationType
13+
import org.springframework.web.bind.annotation.GetMapping
14+
import org.springframework.web.bind.annotation.RequestMapping
15+
import org.springframework.web.bind.annotation.RequestParam
16+
import org.springframework.web.bind.annotation.RestController
17+
18+
@Tag(name = "Notification API", description = "알림 조회 API")
19+
@RequestMapping("${ApiConstants.API_BASE_PATH_V1}/notifications")
20+
@RestController
21+
class NotificationQueryController(
22+
private val getNotifications: GetNotifications,
23+
private val checkUnreadNotificationExist: CheckUnreadNotificationExist
24+
) {
25+
26+
@Operation(summary = "알림 전체 조회", description = "최근 30일 이내의 알림을 최신순으로 조회.")
27+
@GetMapping
28+
fun getNotifications(
29+
userContext: UserContext,
30+
@RequestParam(defaultValue = "0") page: Int,
31+
@RequestParam(defaultValue = "10") size: Int,
32+
@Parameter(
33+
description = "알림 타입 필터 (FOLLOW : 친구추가 / EVENT : 이벤트 / null : 전체)",
34+
`in` = ParameterIn.QUERY
35+
)
36+
@RequestParam(required = false) type: NotificationType? = null
37+
): ClogApiResponse<List<NotificationResponse>> {
38+
val notifications = getNotifications(userContext.userId, page, size, type)
39+
return ClogApiResponse.from(notifications)
40+
}
41+
42+
@Operation(
43+
summary = "새 알림 존재 여부 조회(앱 뱃지와 레드닷 표시에 사용)",
44+
description = "새 알림이 하나라도 있으면 true, 아니면 false 반환"
45+
)
46+
@GetMapping("/has-unread")
47+
fun unreadExist(
48+
userContext: UserContext
49+
): ClogApiResponse<UnreadExistResponse> {
50+
val exists = checkUnreadNotificationExist(userContext.userId)
51+
return ClogApiResponse.from(UnreadExistResponse(exists))
52+
}
53+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.depromeet.clog.server.api.notification.presentation
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import org.depromeet.clog.server.domain.notification.NotificationQuery
5+
import java.time.LocalDateTime
6+
7+
@Schema(description = "알림 응답")
8+
data class NotificationResponse(
9+
@Schema(description = "알림 ID") val id: Long,
10+
@Schema(description = "알림 타입(FOLLOW / EVENT)") val type: String,
11+
@Schema(description = "대상 리디렉션 ID") val targetId: Long,
12+
@Schema(description = "제목") val title: String,
13+
@Schema(description = "메시지") val message: String,
14+
@Schema(description = "읽음 여부") val isRead: Boolean,
15+
@Schema(description = "알림 생성 시각") val createdAt: LocalDateTime,
16+
) {
17+
companion object {
18+
fun from(notificationQuery: NotificationQuery): NotificationResponse {
19+
return NotificationResponse(
20+
id = notificationQuery.id,
21+
type = notificationQuery.type.name,
22+
targetId = notificationQuery.targetId,
23+
title = notificationQuery.title,
24+
message = notificationQuery.message,
25+
isRead = notificationQuery.isRead,
26+
createdAt = notificationQuery.createdAt
27+
)
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)