From 63c465a1c343972f896b64ee34cf43b1df3b1b7c Mon Sep 17 00:00:00 2001 From: 0chil <0@chll.it> Date: Sun, 3 Nov 2024 17:04:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(messaging):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=EB=A5=BC=20=EB=93=B1=EB=A1=9D=ED=95=9C=EB=8B=A4=20(#1?= =?UTF-8?q?83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(messaging): 알림 발송을 위해 기기를 등록한다 * feat(messaging): 한 기기는 한 계정당 한 번만 등록할 수 있다 * feat(messaging.notification): endpoint 수정 * feat(messaging.notification): endpoint 수정 --- .../server/common/exception/Kind.java | 3 +- .../controller/NotificationController.kt | 25 ++++++++++++ .../messaging/notification/domain/Device.kt | 17 ++++++++ .../notification/domain/DeviceRepository.kt | 10 +++++ .../exception/DuplicatedDeviceException.kt | 5 +++ .../exception/NotificationException.kt | 9 +++++ .../service/NotificationService.kt | 26 +++++++++++++ .../service/dto/DeviceResponse.kt | 15 +++++++ .../service/dto/DeviceTokenRequest.kt | 10 +++++ .../controller/NotificationControllerTest.kt | 31 +++++++++++++++ .../service/NotificationServiceTest.kt | 39 +++++++++++++++++++ 11 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/snackgame/server/messaging/notification/controller/NotificationController.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/domain/Device.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/domain/DeviceRepository.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/exception/DuplicatedDeviceException.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/exception/NotificationException.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/service/NotificationService.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceResponse.kt create mode 100644 src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceTokenRequest.kt create mode 100644 src/test/java/com/snackgame/server/messaging/notification/controller/NotificationControllerTest.kt create mode 100644 src/test/java/com/snackgame/server/messaging/notification/service/NotificationServiceTest.kt diff --git a/src/main/java/com/snackgame/server/common/exception/Kind.java b/src/main/java/com/snackgame/server/common/exception/Kind.java index 71494177..5c259850 100644 --- a/src/main/java/com/snackgame/server/common/exception/Kind.java +++ b/src/main/java/com/snackgame/server/common/exception/Kind.java @@ -5,7 +5,8 @@ public enum Kind { INTERNAL_SERVER_ERROR(true, HttpStatus.INTERNAL_SERVER_ERROR), - BAD_REQUEST(false, HttpStatus.BAD_REQUEST); + BAD_REQUEST(false, HttpStatus.BAD_REQUEST), + IGNORE(false, HttpStatus.OK); private final boolean needsMessageToBeHidden; private final HttpStatus httpStatus; diff --git a/src/main/java/com/snackgame/server/messaging/notification/controller/NotificationController.kt b/src/main/java/com/snackgame/server/messaging/notification/controller/NotificationController.kt new file mode 100644 index 00000000..32c03c88 --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/controller/NotificationController.kt @@ -0,0 +1,25 @@ +package com.snackgame.server.messaging.notification.controller + +import com.snackgame.server.auth.token.support.Authenticated +import com.snackgame.server.member.domain.Member +import com.snackgame.server.messaging.notification.service.NotificationService +import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "🔔 알림") +@RestController +class NotificationController(private val notificationService: NotificationService) { + + @Operation(summary = "기기 등록", description = "기기를 등록한다") + @PostMapping("/notifications/devices") + fun registerDevice( + @Authenticated member: Member, + @RequestBody deviceTokenRequest: DeviceTokenRequest + ) { + notificationService.registerDeviceFor(member.id, deviceTokenRequest) + } +} diff --git a/src/main/java/com/snackgame/server/messaging/notification/domain/Device.kt b/src/main/java/com/snackgame/server/messaging/notification/domain/Device.kt new file mode 100644 index 00000000..e25c71a7 --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/domain/Device.kt @@ -0,0 +1,17 @@ +package com.snackgame.server.messaging.notification.domain + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.Table +import javax.persistence.UniqueConstraint + +@Entity +@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["ownerId", "token"])]) +class Device( + val ownerId: Long, + val token: String, + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0 +) diff --git a/src/main/java/com/snackgame/server/messaging/notification/domain/DeviceRepository.kt b/src/main/java/com/snackgame/server/messaging/notification/domain/DeviceRepository.kt new file mode 100644 index 00000000..4f227ce5 --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/domain/DeviceRepository.kt @@ -0,0 +1,10 @@ +package com.snackgame.server.messaging.notification.domain + +import org.springframework.data.jpa.repository.JpaRepository + +interface DeviceRepository : JpaRepository { + + fun existsByOwnerIdAndToken(ownerId: Long, token: String): Boolean + + fun findAllByOwnerId(ownerId: Long): List +} diff --git a/src/main/java/com/snackgame/server/messaging/notification/exception/DuplicatedDeviceException.kt b/src/main/java/com/snackgame/server/messaging/notification/exception/DuplicatedDeviceException.kt new file mode 100644 index 00000000..308c4a28 --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/exception/DuplicatedDeviceException.kt @@ -0,0 +1,5 @@ +package com.snackgame.server.messaging.notification.exception + +import com.snackgame.server.common.exception.Kind + +class DuplicatedDeviceException : NotificationException("이미 등록된 기기입니다", Kind.IGNORE) diff --git a/src/main/java/com/snackgame/server/messaging/notification/exception/NotificationException.kt b/src/main/java/com/snackgame/server/messaging/notification/exception/NotificationException.kt new file mode 100644 index 00000000..176fc1ce --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/exception/NotificationException.kt @@ -0,0 +1,9 @@ +package com.snackgame.server.messaging.notification.exception + +import com.snackgame.server.common.exception.BusinessException +import com.snackgame.server.common.exception.Kind + +abstract class NotificationException( + message: String, + kind: Kind = Kind.BAD_REQUEST +) : BusinessException(kind, message) diff --git a/src/main/java/com/snackgame/server/messaging/notification/service/NotificationService.kt b/src/main/java/com/snackgame/server/messaging/notification/service/NotificationService.kt new file mode 100644 index 00000000..ceef655d --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/service/NotificationService.kt @@ -0,0 +1,26 @@ +package com.snackgame.server.messaging.notification.service + +import com.snackgame.server.messaging.notification.domain.Device +import com.snackgame.server.messaging.notification.domain.DeviceRepository +import com.snackgame.server.messaging.notification.exception.DuplicatedDeviceException +import com.snackgame.server.messaging.notification.service.dto.DeviceResponse +import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest +import org.springframework.stereotype.Service + +@Service +class NotificationService(private val deviceRepository: DeviceRepository) { + + fun registerDeviceFor(ownerId: Long, deviceToken: DeviceTokenRequest) { + if (!deviceRepository.existsByOwnerIdAndToken(ownerId, deviceToken.token)) { + Device(ownerId, deviceToken.token) + .let { deviceRepository.save(it) } + return + } + throw DuplicatedDeviceException() + } + + fun getDevicesOf(ownerId: Long): List { + return deviceRepository.findAllByOwnerId(ownerId) + .map { DeviceResponse.of(it) } + } +} diff --git a/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceResponse.kt b/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceResponse.kt new file mode 100644 index 00000000..f9c121e7 --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceResponse.kt @@ -0,0 +1,15 @@ +package com.snackgame.server.messaging.notification.service.dto + +import com.snackgame.server.messaging.notification.domain.Device + +data class DeviceResponse( + val ownerId: Long, + val token: String, + val id: Long +) { + + companion object { + + fun of(device: Device) = DeviceResponse(device.ownerId, device.token, device.id) + } +} diff --git a/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceTokenRequest.kt b/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceTokenRequest.kt new file mode 100644 index 00000000..1db449ff --- /dev/null +++ b/src/main/java/com/snackgame/server/messaging/notification/service/dto/DeviceTokenRequest.kt @@ -0,0 +1,10 @@ +package com.snackgame.server.messaging.notification.service.dto + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls + +data class DeviceTokenRequest @JsonCreator constructor( + @JsonSetter(nulls = Nulls.FAIL) + val token: String +) diff --git a/src/test/java/com/snackgame/server/messaging/notification/controller/NotificationControllerTest.kt b/src/test/java/com/snackgame/server/messaging/notification/controller/NotificationControllerTest.kt new file mode 100644 index 00000000..86028a95 --- /dev/null +++ b/src/test/java/com/snackgame/server/messaging/notification/controller/NotificationControllerTest.kt @@ -0,0 +1,31 @@ +@file:Suppress("NonAsciiCharacters") + +package com.snackgame.server.messaging.notification.controller + +import com.snackgame.server.member.fixture.MemberFixture +import com.snackgame.server.member.fixture.MemberFixture.땡칠_인증정보 +import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest +import com.snackgame.server.support.restassured.RestAssuredTest +import com.snackgame.server.support.restassured.RestAssuredUtil +import io.restassured.http.ContentType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.http.HttpStatus + +@RestAssuredTest +class NotificationControllerTest { + + @BeforeEach + fun setUp() { + MemberFixture.saveAll() + } + + @Test + fun `기기를 등록한다`() { + RestAssuredUtil.givenAuthentication(땡칠_인증정보()) + .contentType(ContentType.JSON) + .body(DeviceTokenRequest("a_device_token")) + .`when`().post("/notifications/devices") + .then().statusCode(HttpStatus.OK.value()) + } +} diff --git a/src/test/java/com/snackgame/server/messaging/notification/service/NotificationServiceTest.kt b/src/test/java/com/snackgame/server/messaging/notification/service/NotificationServiceTest.kt new file mode 100644 index 00000000..e8386aba --- /dev/null +++ b/src/test/java/com/snackgame/server/messaging/notification/service/NotificationServiceTest.kt @@ -0,0 +1,39 @@ +@file:Suppress("NonAsciiCharacters") + +package com.snackgame.server.messaging.notification.service + +import com.snackgame.server.member.fixture.MemberFixture.땡칠 +import com.snackgame.server.messaging.notification.exception.DuplicatedDeviceException +import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest +import com.snackgame.server.support.general.ServiceTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +@ServiceTest +class NotificationServiceTest { + + @Autowired + private lateinit var notificationService: NotificationService + + @Test + fun `기기를 등록한다`() { + val deviceToken = "a_device_token" + notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken)) + + assertThat(notificationService.getDevicesOf(땡칠().id)) + .singleElement() + .matches { it.ownerId == 땡칠().id } + .matches { it.token == deviceToken } + } + + @Test + fun `한 기기는 한 계정당 한 번만 등록할 수 있다`() { + val deviceToken = "a_device_token" + notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken)) + + assertThatThrownBy { notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken)) } + .isInstanceOf(DuplicatedDeviceException::class.java) + } +}