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

[Chat] 채팅방에 초대할 때 사용할 코드 생성 API 구현 #149

Merged
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7d7cc9b
chore: db setting과 properties application.yml에 추가
minisundev Jun 3, 2024
0f52c3d
chore: build.gradle에 redis 추가
minisundev Jun 3, 2024
9a93d03
feat: getChatRoomInvitation Controller Layer 구현
minisundev Jun 3, 2024
edd40ed
feat: getChatRoomInvitation Service Layer 구현
minisundev Jun 3, 2024
66ffbc6
feat: ErrorCode 추가
minisundev Jun 3, 2024
b0b114c
feat: UserChatRoomInvitationRepository 생성
minisundev Jun 3, 2024
5b70b4b
feat: UserChatRoomInvitationRepository에 setInvitation, getInvitation …
minisundev Jun 3, 2024
9826a24
feat: InvitationChatRoomRepository 생성
minisundev Jun 3, 2024
0a934ba
feat: InvitationChatRoomRepository에 setInvitationLink 메서드 구현
minisundev Jun 3, 2024
7dfae34
feat: InvitationResponse DTO 생성
minisundev Jun 3, 2024
a5a16e1
feat: PropertyConfig 생성
minisundev Jun 3, 2024
d67ff43
feat: RedisConfig 생성
minisundev Jun 3, 2024
18b7f84
chore: Redis docker image 설정
minisundev Jun 3, 2024
47038d4
feat: ChatRoomControllerTest에 getChatRoomInvitation api testcode 작성
minisundev Jun 3, 2024
a9c1453
refac: 추가된 repository 테스트코드에 반영
minisundev Jun 3, 2024
406d11d
refac: 메서드,필드 이름 link,key 에서 code로 통일
minisundev Jun 3, 2024
a797346
refac: 불필요한 메서드 삭제
minisundev Jun 3, 2024
7ecfec9
refac: 불필요한 ErrorCode 삭제
minisundev Jun 3, 2024
46e80af
refac: 서비스 로직 repository에서 service로 옮기고 return type이 ResponseDTO가 아니게 변경
minisundev Jun 4, 2024
18b7cb6
rename: userChatRoomInvitationRepository를 InvitationRepository로 변경
minisundev Jun 5, 2024
ac747fb
remove: InvitationChatRoomRepository 삭제
minisundev Jun 5, 2024
18c44ef
feat: invitation 정보들을 인코딩해서 코드 생성하는 method 구현
minisundev Jun 5, 2024
1854afd
fix: getExpire를 하기 위한 key 수정
minisundev Jun 5, 2024
6020df3
refac: InvitationResponse에서 expiration 삭제
minisundev Jun 5, 2024
2383b40
rename: ChatRoomProperty로 이름 변경
minisundev Jun 5, 2024
be2f961
refac: 테스트코드에 변경사항 반영
minisundev Jun 5, 2024
640d3ac
style: 코드 정렬
minisundev Jun 5, 2024
7804e42
feat: InvitationService 생성
minisundev Jun 6, 2024
0449e6f
refac: InvitationRepository의 재사용 불가능한 로직 InvitationService로 분리
minisundev Jun 6, 2024
df2ecfd
refac: InvitationRepository대신 InvitationService 사용
minisundev Jun 6, 2024
c583e10
chore: 변경된 dependency ChatRoomServiceTest에 반영
minisundev Jun 6, 2024
dde725d
refac: getOrCreateInvitation 메서드 삭제하고 getChatRoomInvitation으로 로직 이동
minisundev Jun 10, 2024
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
3 changes: 3 additions & 0 deletions chat/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")

// non-blocking redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")

// test
testImplementation(project(":test"))
testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand Down
17 changes: 13 additions & 4 deletions chat/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ services:
image: mongo:latest
container_name: mongo
ports:
- "27017:27017"
- "27018:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: 58155815
MONGO_INITDB_DATABASE: mongodb
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: testpassword1234
MONGO_INITDB_DATABASE: mongodb

redis:
image: redis:alpine
container_name: redis_link
ports:
- "6379:6379"
environment:
- REDIS_PASSWORD = "testpassword1234"
command: [ "redis-server","--requirepass","${REDIS_PASSWORD}" ]
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kpring.chat.chatroom.api.v1
import kpring.chat.chatroom.service.ChatRoomService
import kpring.core.auth.client.AuthClient
import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest
import kpring.core.global.dto.response.ApiResponse
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
Expand Down Expand Up @@ -34,4 +35,14 @@ class ChatRoomController(
val result = chatRoomService.exitChatRoom(chatRoomId, userId)
return ResponseEntity.ok().body(result)
}

@GetMapping("/chatroom/{chatRoomId}/invite")
fun getChatRoomInvitation(
@PathVariable("chatRoomId") chatRoomId: String,
@RequestHeader("Authorization") token: String,
): ResponseEntity<*> {
val userId = authClient.getTokenInfo(token).data!!.userId
val result = chatRoomService.getChatRoomInvitation(chatRoomId, userId)
return ResponseEntity.ok().body(ApiResponse(data = result))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package kpring.chat.chatroom.repository

import kpring.chat.global.config.ChatRoomProperty
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.core.ValueOperations
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*

@Component
class InvitationRepository(
Copy link
Contributor

Choose a reason for hiding this comment

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

초대 저장소가 하는 역할이 무엇인가요? 서비스 layer에 더 어울리는 역할을 수행하는 것 같은 느낌?

Copy link
Contributor

Choose a reason for hiding this comment

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

재사용할 수 있는 컴포넌트인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

findbyId 나 save등만 있는게 아니라서 재사용이 불가능할 것 같아요
chatService랑은 좀 동떨어진 일을 하는 것 같아서 저 repository로 초대/참여 관련 로직을 분리했었는데 생각해보면 다른 Service로 분리하는게 더 나아보이네요

private val redisTemplate: RedisTemplate<String, String>,
private val chatRoomProperty: ChatRoomProperty,
) {
fun getInvitationCode(
userId: String,
chatRoomId: String,
): String {
val key = generateKey(userId, chatRoomId)
var value = redisTemplate.opsForValue().get(key)
if (value == null) {
value = setInvitation(key, chatRoomId)
}
return generateCode(key, value)
}

fun setInvitation(
key: String,
chatRoomId: String,
): String {
val value = generateValue()
val ops: ValueOperations<String, String> = redisTemplate.opsForValue()
ops.set(key, value, chatRoomProperty.getExpiration())
return value
}

fun getExpiration(
userId: String,
chatRoomId: String,
): Long {
val key = generateKey(userId, chatRoomId)
val expiration = redisTemplate.getExpire(key)
return expiration
}

fun generateCode(
key: String,
value: String,
): String {
val combine = "$key,$value"
Copy link
Contributor

Choose a reason for hiding this comment

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

combine 동사이기도 하고 무슨 의미인지 직관적으로 파악이 힘든 것 같아요!, key, value의 의미가 무엇인지 파악이 힘들어서 주석으로 무슨 의미인지를 좀 더 설명을 해주면 좋을 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

네~! 변수명이 가독성이 떨어지는거 같긴 하네요

val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(combine.toByteArray(StandardCharsets.UTF_8))
return Base64.getEncoder().encodeToString(hash)
}

private fun generateKey(
userId: String,
chatRoomId: String,
): String {
return "$userId:$chatRoomId"
}

private fun generateValue(): String {
return UUID.randomUUID().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package kpring.chat.chatroom.service

import kpring.chat.chatroom.model.ChatRoom
import kpring.chat.chatroom.repository.ChatRoomRepository
import kpring.chat.chatroom.repository.InvitationRepository
import kpring.chat.global.exception.ErrorCode
import kpring.chat.global.exception.GlobalException
import kpring.core.chat.chat.dto.response.InvitationResponse
import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest
import org.springframework.stereotype.Service

@Service
class ChatRoomService(
private val chatRoomRepository: ChatRoomRepository,
private val invitationRepository: InvitationRepository,
) {
fun createChatRoom(
request: CreateChatRoomRequest,
Expand All @@ -30,6 +33,15 @@ class ChatRoomService(
chatRoomRepository.save(chatRoom)
}

fun getChatRoomInvitation(
chatRoomId: String,
userId: String,
): InvitationResponse {
verifyChatRoomAccess(chatRoomId, userId)
val code = invitationRepository.getInvitationCode(userId, chatRoomId)
return InvitationResponse(code)
}

fun verifyChatRoomAccess(
chatRoomId: String,
userId: String,
Expand Down
19 changes: 19 additions & 0 deletions chat/src/main/kotlin/kpring/chat/global/config/ChatRoomProperty.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kpring.chat.global.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@ConfigurationProperties(prefix = "chatroom")
class ChatRoomProperty {
private lateinit var expiration: Duration

fun getExpiration(): Duration {
return expiration
}

fun setExpiration(expiration: Duration) {
this.expiration = expiration
}
}
16 changes: 16 additions & 0 deletions chat/src/main/kotlin/kpring/chat/global/config/RedisConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kpring.chat.global.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate

@Configuration
class RedisConfig {
@Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<*, *> {
val template = RedisTemplate<ByteArray, ByteArray>()
template.connectionFactory = connectionFactory
return template
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ enum class ErrorCode(val httpStatus: Int, val message: String) {

// 404
CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"),

// 500
INVITATION_LINK_SAVE_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invitation Code가 저장되지 않았습니다"),
Copy link
Contributor

Choose a reason for hiding this comment

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

client로 노출할 메시지라서 에러 메시지가 너무 상세하지 않은가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

좋은 지적이네요 다시 생각해볼게요ㅕ

}
13 changes: 11 additions & 2 deletions chat/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ spring:
authentication-database: admin
authSource: admin

redis:
host: localhost
port: 6379
password: testpassword1234

server:
port: 8081

auth:
url: "http://localhost:30000/"
url: "http://localhost:30001/"
url:
server: "http://localhost:8080/"
server: "http://localhost:8080/"

page:
size: 100
chatroom:
expiration: 1d
baseurl: "http://localhost:8081/"
Copy link
Contributor

Choose a reason for hiding this comment

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

baseurl을 사용한 곳이 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

지금은 사용하지 않고 있어요 빼겠습니다!

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.mockk.mockk
import io.mockk.verify
import kpring.chat.chatroom.model.ChatRoom
import kpring.chat.chatroom.repository.ChatRoomRepository
import kpring.chat.chatroom.repository.InvitationRepository
import kpring.chat.chatroom.service.ChatRoomService
import kpring.chat.global.ChatRoomTest
import kpring.chat.global.CommonTest
Expand All @@ -16,7 +17,8 @@ import java.util.*
class ChatRoomServiceTest : FunSpec({

val chatRoomRepository = mockk<ChatRoomRepository>()
val chatRoomService = ChatRoomService(chatRoomRepository)
val invitationRepository = mockk<InvitationRepository>()
val chatRoomService = ChatRoomService(chatRoomRepository, invitationRepository)

test("createChatRoom 는 새 ChatRoom을 저장해야 한다") {
// Given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package kpring.chat.chat.api.v1

import com.fasterxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.DescribeSpec
import io.mockk.every
import io.mockk.junit5.MockKExtension
import kpring.chat.chatroom.api.v1.ChatRoomController
import kpring.chat.chatroom.service.ChatRoomService
import kpring.chat.global.ChatRoomTest
import kpring.chat.global.CommonTest
import kpring.core.auth.client.AuthClient
import kpring.core.auth.dto.response.TokenInfo
import kpring.core.auth.enums.TokenType
import kpring.core.chat.chat.dto.response.InvitationResponse
import kpring.core.global.dto.response.ApiResponse
import kpring.test.restdoc.dsl.restDoc
import kpring.test.restdoc.json.JsonDataType
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.restdocs.ManualRestDocumentation
import org.springframework.restdocs.RestDocumentationExtension
import org.springframework.restdocs.operation.preprocess.Preprocessors
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.servlet.client.MockMvcWebTestClient
import org.springframework.web.context.WebApplicationContext

@WebMvcTest(controllers = [ChatRoomController::class])
@ExtendWith(RestDocumentationExtension::class)
@ExtendWith(SpringExtension::class)
@ExtendWith(MockKExtension::class)
class ChatRoomControllerTest(
private val om: ObjectMapper,
webContext: WebApplicationContext,
@MockkBean val chatRoomService: ChatRoomService,
@MockkBean val authClient: AuthClient,
) : DescribeSpec({

val restDocument = ManualRestDocumentation()
val webTestClient: WebTestClient =
MockMvcWebTestClient.bindToApplicationContext(webContext).configureClient().baseUrl("http://localhost:8081").filter(
WebTestClientRestDocumentation.documentationConfiguration(restDocument).operationPreprocessors()
.withRequestDefaults(Preprocessors.prettyPrint()).withResponseDefaults(Preprocessors.prettyPrint()),
).build()

beforeSpec { restDocument.beforeTest(this.javaClass, "chat controller") }

afterSpec { restDocument.afterTest() }

describe("GET /api/v1/chatroom/{chatRoomId}/invite : getChatRoomInvitation api test") {

val url = "/api/v1/chatroom/{chatRoomId}/invite"
it("getChatRoomInvitation api test") {

// Given
val chatRoomId = ChatRoomTest.TEST_ROOM_ID
val userId = CommonTest.TEST_USER_ID
val key = "62e9df6b-13cb-4673-a6fe-8566451b7f15"
val data = InvitationResponse(key)

every { authClient.getTokenInfo(any()) } returns
ApiResponse(
data =
TokenInfo(
type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID,
),
)

every {
chatRoomService.getChatRoomInvitation(
chatRoomId,
userId,
)
} returns data

// When
val result = webTestClient.get().uri(url, chatRoomId).header("Authorization", "Bearer mock_token").exchange()

val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(data = data)))

// Then
docs.restDoc(
identifier = "getChatRoomInvitation_200",
description = "채팅방 참여코드를 위한 key값을 반환하는 api",
) {
request {
path {
"chatRoomId" mean "채팅방 참여코드를 발급할 채팅방 Id"
}
}

response {
body {
"data.code" type JsonDataType.Strings mean "참여 코드"
}
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package kpring.core.chat.chat.dto.request

enum class ChatType(val type: String) {
Room("Room"),
Server("Server");
Server("Server"),
;

override fun toString(): String{
return type;
override fun toString(): String {
return type
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kpring.core.chat.chat.dto.response

data class InvitationResponse(
val code: String,
)