From 2ad102b96841e01d67dea3748df8b3232708b9b5 Mon Sep 17 00:00:00 2001 From: Gayeong Park Date: Mon, 18 May 2026 00:43:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix=20:=20=ED=8A=B9=EC=A0=95=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=B0=8C=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=EA=B0=80=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/NotificationEventListener.kt | 2 +- .../notification/event/NotificationEvents.kt | 5 ++- .../yapp/love/application/poke/PokeService.kt | 5 +-- .../event/NotificationEventListenerTest.kt | 11 +++++-- .../love/application/poke/PokeServiceTest.kt | 17 ++++++---- .../com/yapp/love/web/poke/PokeApiSpec.kt | 33 ++++++++++++++++++- .../com/yapp/love/web/poke/PokeController.kt | 14 +++++++- 7 files changed, 72 insertions(+), 15 deletions(-) diff --git a/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEventListener.kt b/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEventListener.kt index e39ded8..100566f 100644 --- a/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEventListener.kt +++ b/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEventListener.kt @@ -47,7 +47,7 @@ class NotificationEventListener( bodyArgs = arrayOf(event.senderNickname), deepLinkParams = mapOf( "goalId" to event.goalId.toString(), - "date" to LocalDate.now().toString(), + "date" to event.verificationDate.toString(), ), ) } catch (e: Exception) { diff --git a/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEvents.kt b/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEvents.kt index e7a8848..5e66a19 100644 --- a/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEvents.kt +++ b/love/application/src/main/kotlin/com/yapp/love/application/notification/event/NotificationEvents.kt @@ -1,5 +1,7 @@ package com.yapp.love.application.notification.event +import java.time.LocalDate + data class PartnerConnectedEvent( val targetUserId: Long, val senderUserId: Long, @@ -10,6 +12,7 @@ data class PokedEvent( val senderNickname: String, val goalId: Long, val goalName: String, + val verificationDate: LocalDate, ) data class PhotologCreatedEvent( @@ -28,7 +31,7 @@ data class ReactionCreatedEvent( val reactorUserId: Long, val photologOwnerId: Long, val goalId: Long, - val verificationDate: java.time.LocalDate, + val verificationDate: LocalDate, ) data class DailyGoalAchievedEvent( diff --git a/love/application/src/main/kotlin/com/yapp/love/application/poke/PokeService.kt b/love/application/src/main/kotlin/com/yapp/love/application/poke/PokeService.kt index 9963510..147ca08 100644 --- a/love/application/src/main/kotlin/com/yapp/love/application/poke/PokeService.kt +++ b/love/application/src/main/kotlin/com/yapp/love/application/poke/PokeService.kt @@ -13,8 +13,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional - -private val logger = KotlinLogging.logger {} +import java.time.LocalDate @Service class PokeService( @@ -28,6 +27,7 @@ class PokeService( fun poke( senderId: Long, goalId: Long, + verificationDate: LocalDate, ) { // 1. 커플 정보 확인 val coupleInfo = @@ -71,6 +71,7 @@ class PokeService( senderNickname = senderNickname, goalId = goalId, goalName = goal.name, + verificationDate = verificationDate, ) ) } diff --git a/love/application/src/test/kotlin/com/yapp/love/application/notification/event/NotificationEventListenerTest.kt b/love/application/src/test/kotlin/com/yapp/love/application/notification/event/NotificationEventListenerTest.kt index c5aa4fb..a3f4731 100644 --- a/love/application/src/test/kotlin/com/yapp/love/application/notification/event/NotificationEventListenerTest.kt +++ b/love/application/src/test/kotlin/com/yapp/love/application/notification/event/NotificationEventListenerTest.kt @@ -84,6 +84,7 @@ class NotificationEventListenerTest : DescribeSpec({ senderNickname = "철수", goalId = 10L, goalName = "운동하기", + verificationDate = LocalDate.of(2026, 5, 18), ) listener.handlePoked(event) @@ -94,13 +95,19 @@ class NotificationEventListenerTest : DescribeSpec({ type = NotificationType.POKE, titleArgs = arrayOf("철수", "운동하기"), bodyArgs = arrayOf("철수"), - deepLinkParams = mapOf("goalId" to "10", "date" to LocalDate.now().toString()), + deepLinkParams = mapOf("goalId" to "10", "date" to "2026-05-18"), ) } } it("예외 발생 시 메서드가 정상 종료된다") { - val event = PokedEvent(targetUserId = 2L, senderNickname = "철수", goalId = 10L, goalName = "운동하기") + val event = PokedEvent( + targetUserId = 2L, + senderNickname = "철수", + goalId = 10L, + goalName = "운동하기", + verificationDate = LocalDate.of(2026, 5, 18), + ) every { notificationService.sendNotification(any(), any(), any(), any(), any()) } throws RuntimeException("DB 오류") listener.handlePoked(event) diff --git a/love/application/src/test/kotlin/com/yapp/love/application/poke/PokeServiceTest.kt b/love/application/src/test/kotlin/com/yapp/love/application/poke/PokeServiceTest.kt index 0769fa9..201f08d 100644 --- a/love/application/src/test/kotlin/com/yapp/love/application/poke/PokeServiceTest.kt +++ b/love/application/src/test/kotlin/com/yapp/love/application/poke/PokeServiceTest.kt @@ -40,6 +40,7 @@ class PokeServiceTest : DescribeSpec({ val receiverId = 2L val coupleId = 100L val goalId = 10L + val verificationDate = LocalDate.of(2026, 5, 18) val coupleInfo = CoupleInfo( id = coupleId, @@ -78,7 +79,7 @@ class PokeServiceTest : DescribeSpec({ } it("찌르기가 저장된다") { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) verify { pokeRepository.save(match { @@ -90,7 +91,7 @@ class PokeServiceTest : DescribeSpec({ } it("상대방에게 PokedEvent가 발행된다") { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) verify { eventPublisher.publishEvent( @@ -99,6 +100,7 @@ class PokeServiceTest : DescribeSpec({ senderNickname = "철수", goalId = goalId, goalName = "운동하기", + verificationDate = verificationDate, ) ) } @@ -107,7 +109,7 @@ class PokeServiceTest : DescribeSpec({ it("닉네임이 없으면 '상대방'으로 이벤트가 발행된다") { every { userAdditionInfoRepository.findByUserId(senderId) } returns null - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) verify { eventPublisher.publishEvent( @@ -116,6 +118,7 @@ class PokeServiceTest : DescribeSpec({ senderNickname = "상대방", goalId = goalId, goalName = "운동하기", + verificationDate = verificationDate, ) ) } @@ -127,7 +130,7 @@ class PokeServiceTest : DescribeSpec({ every { coupleInfoRepository.findByUserId(senderId) } returns null shouldThrow { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) } } } @@ -138,7 +141,7 @@ class PokeServiceTest : DescribeSpec({ every { goalRepository.findById(goalId) } returns null shouldThrow { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) } } } @@ -161,7 +164,7 @@ class PokeServiceTest : DescribeSpec({ every { goalRepository.findById(goalId) } returns otherGoal shouldThrow { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) } } } @@ -184,7 +187,7 @@ class PokeServiceTest : DescribeSpec({ every { goalRepository.findById(goalId) } returns completedGoal shouldThrow { - pokeService.poke(senderId, goalId) + pokeService.poke(senderId, goalId, verificationDate) } } } diff --git a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt index db0fb0f..269b9a7 100644 --- a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt +++ b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.ExampleObject import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses @@ -12,7 +13,29 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses @Retention(AnnotationRetention.RUNTIME) @Operation( summary = "찌르기", - description = "상대방을 찔러서 목표 인증을 독려합니다.", + description = "상대방을 찔러서 특정 날짜의 목표 인증을 독려합니다. " + + "date 필드로 어떤 날짜의 인증을 독려할지 명시하며, 알림 딥링크에 그대로 전달됩니다.", + requestBody = RequestBody( + required = true, + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = PokeRequest::class), + examples = [ + ExampleObject( + name = "오늘 인증 독려", + summary = "오늘 날짜의 목표 인증을 찌릅니다.", + value = """{"date": "2026-05-18"}""", + ), + ExampleObject( + name = "과거 날짜 인증 독려", + summary = "캘린더에서 과거 미달성 날짜를 보고 찌릅니다.", + value = """{"date": "2026-05-15"}""", + ), + ], + ), + ], + ), ) @ApiResponses( value = [ @@ -32,6 +55,14 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses name = "진행중이지 않은 목표", value = """{"status": 400, "code": "G4000", "message": "진행중이지 않은 목표는 찌를 수 없습니다."}""", ), + ExampleObject( + name = "날짜 누락", + value = """{"status": 400, "code": "G4000", "message": "입력값이 올바르지 않습니다."}""", + ), + ExampleObject( + name = "JSON 형식 오류", + value = """{"status": 400, "code": "G4002", "message": "JSON 형식이 올바르지 않습니다."}""", + ), ], ), ], diff --git a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt index a484f66..c4b755b 100644 --- a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt +++ b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt @@ -3,11 +3,16 @@ package com.yapp.love.web.poke import com.yapp.love.application.poke.PokeService import com.yapp.love.web.auth.AuthUser import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import org.springframework.format.annotation.DateTimeFormat import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate @Tag(name = "Poke", description = "찌르기 API") @RestController @@ -20,12 +25,19 @@ class PokeController( fun poke( @AuthUser userId: Long, @PathVariable goalId: Long, + @Valid @RequestBody request: PokeRequest, ): ResponseEntity { - pokeService.poke(userId, goalId) + pokeService.poke(userId, goalId, request.date) return ResponseEntity.ok(PokeResponse(message = "찌르기를 보냈습니다.")) } } +data class PokeRequest( + @field:NotNull(message = "인증 날짜는 필수입니다.") + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + val date: LocalDate, +) + data class PokeResponse( val message: String, ) From 4b30c64a946c5e9d533dd851d4f37543c3f56669 Mon Sep 17 00:00:00 2001 From: Gayeong Park Date: Mon, 18 May 2026 02:03:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore=20:=20swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/yapp/love/web/poke/PokeApiSpec.kt | 14 ++++++++------ .../com/yapp/love/web/poke/PokeController.kt | 9 +++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt index 269b9a7..0eea7b4 100644 --- a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt +++ b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeApiSpec.kt @@ -14,9 +14,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses @Operation( summary = "찌르기", description = "상대방을 찔러서 특정 날짜의 목표 인증을 독려합니다. " + - "date 필드로 어떤 날짜의 인증을 독려할지 명시하며, 알림 딥링크에 그대로 전달됩니다.", + "date 필드로 어떤 날짜의 인증을 독려할지 명시하며, 알림 딥링크에 그대로 전달됩니다. " + + "body를 생략하거나 date가 null이면 오늘 날짜로 처리됩니다(구버전 클라이언트 호환).", requestBody = RequestBody( - required = true, + required = false, content = [ Content( mediaType = "application/json", @@ -32,6 +33,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses summary = "캘린더에서 과거 미달성 날짜를 보고 찌릅니다.", value = """{"date": "2026-05-15"}""", ), + ExampleObject( + name = "구버전 클라이언트 (body 생략)", + summary = "body 없이 호출하면 서버가 오늘 날짜로 처리합니다.", + value = "", + ), ], ), ], @@ -55,10 +61,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses name = "진행중이지 않은 목표", value = """{"status": 400, "code": "G4000", "message": "진행중이지 않은 목표는 찌를 수 없습니다."}""", ), - ExampleObject( - name = "날짜 누락", - value = """{"status": 400, "code": "G4000", "message": "입력값이 올바르지 않습니다."}""", - ), ExampleObject( name = "JSON 형식 오류", value = """{"status": 400, "code": "G4002", "message": "JSON 형식이 올바르지 않습니다."}""", diff --git a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt index c4b755b..d97c849 100644 --- a/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt +++ b/love/web/src/main/kotlin/com/yapp/love/web/poke/PokeController.kt @@ -3,8 +3,6 @@ package com.yapp.love.web.poke import com.yapp.love.application.poke.PokeService import com.yapp.love.web.auth.AuthUser import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.Valid -import jakarta.validation.constraints.NotNull import org.springframework.format.annotation.DateTimeFormat import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable @@ -25,17 +23,16 @@ class PokeController( fun poke( @AuthUser userId: Long, @PathVariable goalId: Long, - @Valid @RequestBody request: PokeRequest, + @RequestBody(required = false) request: PokeRequest?, ): ResponseEntity { - pokeService.poke(userId, goalId, request.date) + pokeService.poke(userId, goalId, request?.date ?: LocalDate.now()) return ResponseEntity.ok(PokeResponse(message = "찌르기를 보냈습니다.")) } } data class PokeRequest( - @field:NotNull(message = "인증 날짜는 필수입니다.") @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE) - val date: LocalDate, + val date: LocalDate? = null, ) data class PokeResponse(