Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit 3b6c60b

Browse files
authored
Merge pull request #109 from ASAP-Lettering/ASAP-325
ASAP-325 feat: 편지 공유 상태 확인할 수 있는 api 추가
2 parents b444edb + 2e64df3 commit 3b6c60b

11 files changed

Lines changed: 282 additions & 3 deletions

File tree

Application-Module/src/main/kotlin/com/asap/application/letter/port/in/LetterLogUsecase.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ interface LetterLogUsecase {
66

77
fun log(request: LogRequest)
88

9+
fun finLatestLogByLetterCode(letterCode: String): LogResponse?
10+
911
data class LogRequest(
1012
val letterCode: String,
1113
val logType: LetterLogType,
1214
val logContent: String,
1315
)
16+
17+
data class LogResponse(
18+
val letterId: String,
19+
val logType: LetterLogType,
20+
val logContent: String,
21+
)
1422
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.asap.application.letter.port.out
22

3+
import com.asap.domain.common.DomainId
34
import com.asap.domain.letter.entity.LetterLog
45

56
interface LetterLogManagementPort {
67
fun save(log: LetterLog): LetterLog
78

89
fun findAll(): List<LetterLog>
10+
11+
fun findLatestByLetterId(letterId: DomainId): LetterLog?
912
}

Application-Module/src/main/kotlin/com/asap/application/letter/service/LetterLogService.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,17 @@ class LetterLogService(
2525
letterLogManagementPort.save(this)
2626
}
2727
}
28+
29+
override fun finLatestLogByLetterCode(letterCode: String): LetterLogUsecase.LogResponse? {
30+
val letter = sendLetterManagementPort.getLetterByCodeNotNull(letterCode)
31+
val latestLog = letterLogManagementPort.findLatestByLetterId(letter.id)
32+
33+
return latestLog?.let {
34+
LetterLogUsecase.LogResponse(
35+
letterId = it.targetLetterId.value,
36+
logType = it.logType,
37+
logContent = it.content
38+
)
39+
}
40+
}
2841
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.asap.bootstrap.web.letter.api
2+
3+
import com.asap.bootstrap.common.security.annotation.AccessUser
4+
import com.asap.bootstrap.web.letter.dto.LetterShareStatusResponse
5+
import io.swagger.v3.oas.annotations.Operation
6+
import io.swagger.v3.oas.annotations.media.Content
7+
import io.swagger.v3.oas.annotations.media.Schema
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse
9+
import io.swagger.v3.oas.annotations.responses.ApiResponses
10+
import io.swagger.v3.oas.annotations.tags.Tag
11+
import org.springframework.web.bind.annotation.GetMapping
12+
import org.springframework.web.bind.annotation.RequestMapping
13+
import org.springframework.web.bind.annotation.RequestParam
14+
15+
@Tag(name = "Letter", description = "Letter API")
16+
@RequestMapping("/api/v1/letters/logs")
17+
interface LetterLogApi {
18+
19+
@Operation(summary = "편지 공유 상태 조회")
20+
@GetMapping("/share/status")
21+
@ApiResponses(
22+
value = [
23+
ApiResponse(
24+
responseCode = "200",
25+
description = """
26+
편지 공유 상태 조회 성공
27+
- isShared: 공유 여부
28+
- letterId: 편지 ID
29+
- shareTarget: 공유 대상
30+
31+
isShared가 true인 경우, letterId와 shareTarget은 null이 아님 즉 공유 성공
32+
isShared가 false인 경우, letterId와 shareTarget은 null임 즉 공유 실패
33+
34+
shareTarget은 카카오 챗 타입
35+
* MEMO_CHAT: 개인 채팅
36+
* DIRECT_CHAT: 1:1 채팅
37+
* MULTI_CHAT: 그룹 채팅
38+
* OPEN_DIRECT_CHAT: 오픈 채팅
39+
* OPEN_MULTI_CHAT: 오픈 그룹 채팅
40+
""",
41+
content = [
42+
Content(
43+
mediaType = "application/json",
44+
schema = Schema(implementation = LetterShareStatusResponse::class)
45+
)
46+
]
47+
)
48+
]
49+
)
50+
fun getLetterShareStatus(
51+
@RequestParam("letterCode") letterCode: String,
52+
@AccessUser userId: String
53+
): LetterShareStatusResponse
54+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.asap.bootstrap.web.letter.controller
2+
3+
import com.asap.application.letter.port.`in`.LetterLogUsecase
4+
import com.asap.bootstrap.web.letter.api.LetterLogApi
5+
import com.asap.bootstrap.web.letter.dto.LetterShareStatusResponse
6+
import com.asap.bootstrap.webhook.dto.KakaoWebHookRequest
7+
import com.fasterxml.jackson.databind.ObjectMapper
8+
import org.springframework.web.bind.annotation.RestController
9+
10+
@RestController
11+
class LetterLogController(
12+
private val letterLogUsecase: LetterLogUsecase,
13+
private val objectMapper: ObjectMapper
14+
) : LetterLogApi {
15+
override fun getLetterShareStatus(letterCode: String, userId: String): LetterShareStatusResponse {
16+
return letterLogUsecase.finLatestLogByLetterCode(letterCode)?.let {
17+
val kakaoWebHookRequest =
18+
objectMapper.readValue(it.logContent, KakaoWebHookRequest::class.java)
19+
LetterShareStatusResponse.success(
20+
letterId = it.letterId,
21+
shareTarget = kakaoWebHookRequest.chatType
22+
)
23+
} ?: run {
24+
LetterShareStatusResponse.fail()
25+
}
26+
}
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.asap.bootstrap.web.letter.dto
2+
3+
import com.asap.bootstrap.webhook.dto.KakaoChatType
4+
5+
data class LetterShareStatusResponse(
6+
val isShared: Boolean,
7+
val letterId: String?,
8+
val shareTarget: KakaoChatType?,
9+
) {
10+
companion object{
11+
fun success(
12+
letterId: String,
13+
shareTarget: KakaoChatType
14+
): LetterShareStatusResponse {
15+
return LetterShareStatusResponse(
16+
isShared = true,
17+
letterId = letterId,
18+
shareTarget = shareTarget
19+
)
20+
}
21+
22+
fun fail(): LetterShareStatusResponse {
23+
return LetterShareStatusResponse(
24+
isShared = false,
25+
letterId = null,
26+
shareTarget = null
27+
)
28+
}
29+
}
30+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.asap.bootstrap.acceptance.letter.controller
2+
3+
import com.asap.application.letter.port.`in`.LetterLogUsecase
4+
import com.asap.bootstrap.acceptance.letter.LetterAcceptanceSupporter
5+
import com.asap.bootstrap.webhook.dto.KakaoChatType
6+
import com.asap.bootstrap.webhook.dto.KakaoWebHookRequest
7+
import com.asap.domain.letter.entity.LetterLogType
8+
import org.junit.jupiter.api.Test
9+
import org.mockito.BDDMockito
10+
import org.springframework.boot.test.mock.mockito.MockBean
11+
import org.springframework.test.web.servlet.get
12+
13+
class LetterLogControllerTest: LetterAcceptanceSupporter() {
14+
15+
@MockBean
16+
private lateinit var letterLogUsecase: LetterLogUsecase
17+
18+
@Test
19+
fun getLetterShareStatus(){
20+
// given
21+
val letterCode = "letterCode"
22+
val accessToken = jwtMockManager.generateAccessToken("userId")
23+
24+
BDDMockito.given(letterLogUsecase.finLatestLogByLetterCode(letterCode)).willReturn(
25+
LetterLogUsecase.LogResponse(
26+
letterId = "letterId",
27+
logType = LetterLogType.SHARE,
28+
logContent = KakaoWebHookRequest(
29+
chatType = KakaoChatType.MEMO_CHAT,
30+
hashChatId = "hashChatId",
31+
templateId = "templateId",
32+
requestType = LetterLogType.SHARE,
33+
requestId = "requestId",
34+
).let{
35+
objectMapper.writeValueAsString(it)
36+
}
37+
)
38+
)
39+
40+
// when then
41+
mockMvc.get("/api/v1/letters/logs/share/status?letterCode=$letterCode"){
42+
header("Authorization", "Bearer $accessToken")
43+
}
44+
.andExpect {
45+
status { isOk() }
46+
jsonPath("$.isShared") { isBoolean() }
47+
jsonPath("$.letterId") { isString() }
48+
jsonPath("$.shareTarget") { isString() }
49+
}
50+
}
51+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.asap.bootstrap.integration.letter
2+
3+
import com.asap.application.letter.LetterMockManager
4+
import com.asap.bootstrap.IntegrationSupporter
5+
import com.asap.bootstrap.webhook.dto.KakaoChatType
6+
import com.asap.bootstrap.webhook.dto.KakaoWebHookRequest
7+
import com.asap.domain.letter.entity.LetterLogType
8+
import org.junit.jupiter.api.Test
9+
import org.springframework.http.MediaType
10+
import org.springframework.test.web.servlet.get
11+
import org.springframework.test.web.servlet.post
12+
13+
class LetterLogApiIntegrationTest(
14+
private val letterMockManager: LetterMockManager
15+
) : IntegrationSupporter() {
16+
17+
@Test
18+
fun getLetterShareStatus_success() {
19+
//given
20+
val senderId = userMockManager.settingUser(username = "senderUsername")
21+
val userId = userMockManager.settingUser(username = "username")
22+
val accessToken = jwtMockManager.generateAccessToken(userId)
23+
val letter =
24+
letterMockManager.generateMockSendLetter("username", senderId = senderId)
25+
val letterCode = letter.letterCode!!
26+
27+
28+
val request = KakaoWebHookRequest(
29+
requestId = letterCode,
30+
requestType = LetterLogType.SHARE,
31+
chatType = KakaoChatType.MEMO_CHAT,
32+
hashChatId = "1234567890",
33+
templateId = "1234567890"
34+
)
35+
36+
mockMvc.post("/webhook/kakao") {
37+
headers {
38+
set("Authorization", "KakaoAP 1234567890")
39+
set("X-Kakao-Resource-ID", "1234567890")
40+
set("User-Agent", "KakaoAgent")
41+
}
42+
contentType = MediaType.APPLICATION_JSON
43+
content = objectMapper.writeValueAsString(request)
44+
}
45+
//when then
46+
mockMvc.get("/api/v1/letters/logs/share/status?letterCode=$letterCode") {
47+
header("Authorization", "Bearer $accessToken")
48+
}
49+
.andExpect {
50+
status { isOk() }
51+
jsonPath("$.isShared") { value(true) }
52+
jsonPath("$.letterId") { value(letter.id.value) }
53+
jsonPath("$.shareTarget") { value(request.chatType.name) }
54+
}
55+
}
56+
57+
@Test
58+
fun getLetterShareStatus_fail() {
59+
//given
60+
val senderId = userMockManager.settingUser(username = "senderUsername")
61+
val userId = userMockManager.settingUser(username = "username")
62+
val accessToken = jwtMockManager.generateAccessToken(userId)
63+
val letter =
64+
letterMockManager.generateMockSendLetter("username", senderId = senderId)
65+
val letterCode = letter.letterCode!!
66+
67+
//when then
68+
mockMvc.get("/api/v1/letters/logs/share/status?letterCode=$letterCode") {
69+
header("Authorization", "Bearer $accessToken")
70+
}
71+
.andExpect {
72+
status { isOk() }
73+
jsonPath("$.isShared") { value(false) }
74+
jsonPath("$.letterId") { value(null) }
75+
jsonPath("$.shareTarget") { value(null) }
76+
}
77+
}
78+
}

Bootstrap-Module/src/test/kotlin/com/asap/bootstrap/integration/webhook/WebHookApiIntegrationTest.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.asap.bootstrap.webhook.dto.KakaoChatType
77
import com.asap.bootstrap.webhook.dto.KakaoWebHookRequest
88
import com.asap.domain.letter.entity.LetterLogType
99
import com.fasterxml.jackson.module.kotlin.readValue
10+
import io.kotest.matchers.nulls.shouldNotBeNull
1011
import io.kotest.matchers.shouldBe
1112
import org.junit.jupiter.api.Test
1213
import org.springframework.http.MediaType
@@ -51,9 +52,10 @@ class WebHookApiIntegrationTest(
5152
response.andExpect {
5253
status { isOk() }
5354
}
54-
with(letterLogManagementPort.findAll().first()) {
55-
this.targetLetterId shouldBe letter.id
56-
this.logType shouldBe LetterLogType.SHARE
55+
with(letterLogManagementPort.findLatestByLetterId(letter.id)) {
56+
this.shouldNotBeNull()
57+
this!!.targetLetterId shouldBe letter.id
58+
this!!.logType shouldBe LetterLogType.SHARE
5759
objectMapper.readValue<KakaoWebHookRequest>(this.content) shouldBe request
5860
}
5961
}

Infrastructure-Module/Persistence/src/main/kotlin/com/asap/persistence/jpa/letter/adapter/LetterLogManagementJpaAdapter.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.asap.persistence.jpa.letter.adapter
22

33
import com.asap.application.letter.port.out.LetterLogManagementPort
4+
import com.asap.domain.common.DomainId
45
import com.asap.domain.letter.entity.LetterLog
56
import com.asap.persistence.jpa.letter.LetterLogMapper
67
import com.asap.persistence.jpa.letter.repository.LetterLogJpaRepository
@@ -17,4 +18,8 @@ class LetterLogManagementJpaAdapter(
1718
override fun findAll(): List<LetterLog> {
1819
return letterLogJpaRepository.findAll().map { LetterLogMapper.toDomain(it) }
1920
}
21+
22+
override fun findLatestByLetterId(letterId: DomainId): LetterLog? {
23+
return letterLogJpaRepository.findLatestBy(letterId.value)?.let { LetterLogMapper.toDomain(it) }
24+
}
2025
}

0 commit comments

Comments
 (0)