Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 0 additions & 32 deletions .env.example

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.back.koreaTravelGuide.domain.userChat.chatmessage.dto

import com.back.koreaTravelGuide.domain.userChat.chatmessage.entity.ChatMessage
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

data class ChatMessageResponse(
val id: Long?,
val roomId: Long,
val senderId: Long,
val content: String,
val createdAt: Instant,
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
) {
companion object {
fun from(message: ChatMessage): ChatMessageResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Index
import jakarta.persistence.Table
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

@Entity
@Table(
Expand All @@ -24,5 +25,5 @@ data class ChatMessage(
@Column(nullable = false, columnDefinition = "text")
val content: String,
@Column(name = "created_at", nullable = false)
val createdAt: Instant = Instant.now(),
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ChatRoomController(
@RequestBody req: ChatRoomStartRequest,
): ResponseEntity<ApiResponse<ChatRoomResponse>> {
val authenticatedId = requesterId ?: throw AccessDeniedException("인증이 필요합니다.")
val room = roomService.checkOneToOneRoom(req.guideId, req.userId, authenticatedId)
val room = roomService.createOneToOneRoom(req.guideId, req.userId, authenticatedId)
return ResponseEntity.ok(ApiResponse(msg = "채팅방 시작", data = ChatRoomResponse.from(room)))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.back.koreaTravelGuide.domain.userChat.chatroom.dto

import com.back.koreaTravelGuide.domain.userChat.chatroom.entity.ChatRoom
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

data class ChatRoomResponse(
val id: Long?,
val title: String,
val guideId: Long,
val userId: Long,
val updatedAt: Instant,
val updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
val lastMessageId: Long?,
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Index
import jakarta.persistence.Table
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

@Entity
@Table(
Expand All @@ -24,7 +25,7 @@ data class ChatRoom(
@Column(name = "user_id", nullable = false)
val userId: Long,
@Column(name = "updated_at", nullable = false)
val updatedAt: Instant = Instant.now(),
val updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
@Column(name = "last_message_id")
val lastMessageId: Long? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import com.back.koreaTravelGuide.domain.userChat.chatroom.repository.ChatRoomRep
import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.NoSuchElementException

@Service
Expand All @@ -15,7 +16,7 @@ class ChatRoomService(
private val messageRepository: ChatMessageRepository,
) {
@Transactional
fun checkOneToOneRoom(
fun createOneToOneRoom(
guideId: Long,
userId: Long,
requesterId: Long,
Expand All @@ -27,7 +28,12 @@ class ChatRoomService(
// 2) 없으면 생성 (동시요청은 DB 유니크 인덱스로 가드)
val title = "Guide-$guideId · User-$userId"
return roomRepository.save(
ChatRoom(title = title, guideId = guideId, userId = userId, updatedAt = Instant.now()),
ChatRoom(
title = title,
guideId = guideId,
userId = userId,
updatedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
),
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ springdoc:
# Weather API 설정
weather:
api:
key: ${WEATHER__API__KEY}
key: ${WEATHER_API_KEY}
base-url: https://apihub.kma.go.kr/api/typ02/openApi/MidFcstInfoService

# Tour API 설정
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.back.koreaTravelGuide.domain.userChat.chatmessage.controller

import com.back.koreaTravelGuide.common.security.JwtTokenProvider
import com.back.koreaTravelGuide.domain.user.entity.User
import com.back.koreaTravelGuide.domain.user.enums.UserRole
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageSendRequest
import com.back.koreaTravelGuide.domain.userChat.chatmessage.entity.ChatMessage
import com.back.koreaTravelGuide.domain.userChat.chatmessage.repository.ChatMessageRepository
import com.back.koreaTravelGuide.domain.userChat.chatroom.entity.ChatRoom
import com.back.koreaTravelGuide.domain.userChat.chatroom.repository.ChatRoomRepository
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.annotation.Transactional

@ActiveProfiles("test")
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class ChatMessageControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc

@Autowired
private lateinit var chatMessageRepository: ChatMessageRepository

@Autowired
private lateinit var chatRoomRepository: ChatRoomRepository

@Autowired
private lateinit var userRepository: UserRepository

@Autowired
private lateinit var jwtTokenProvider: JwtTokenProvider

@Autowired
private lateinit var objectMapper: ObjectMapper

private lateinit var guide: User
private lateinit var guest: User
private lateinit var guideToken: String
private lateinit var guestToken: String
private lateinit var room: ChatRoom
private lateinit var firstMessage: ChatMessage

@BeforeEach
fun setUp() {
chatMessageRepository.deleteAll()
chatRoomRepository.deleteAll()
userRepository.deleteAll()
guide =
userRepository.save(
User(
email = "[email protected]",
nickname = "guideTwo",
role = UserRole.GUIDE,
oauthProvider = "test",
oauthId = "guide456",
),
)
guest =
userRepository.save(
User(
email = "[email protected]",
nickname = "guestTwo",
role = UserRole.USER,
oauthProvider = "test",
oauthId = "guest456",
),
)
guideToken = jwtTokenProvider.createAccessToken(guide.id!!, guide.role)
guestToken = jwtTokenProvider.createAccessToken(guest.id!!, guest.role)
room =
chatRoomRepository.save(
ChatRoom(
title = "Guide-${guide.id} · User-${guest.id}",
guideId = guide.id!!,
userId = guest.id!!,
),
)
firstMessage =
chatMessageRepository.save(
ChatMessage(
roomId = room.id!!,
senderId = guide.id!!,
content = "첫 메세지",
),
)
}

@Test
@DisplayName("listMessages returns latest batch when after is missing")
fun listMessagesReturnsLatest() {
mockMvc.perform(
get("/api/userchat/rooms/${room.id}/messages")
.header("Authorization", "Bearer $guestToken"),
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data[0].id").value(firstMessage.id!!.toInt()))
.andExpect(jsonPath("$.data[0].content").value(firstMessage.content))
}

@Test
@DisplayName("listMessages filters newer messages when after provided")
fun listMessagesFiltersNewer() {
val newer =
chatMessageRepository.save(
ChatMessage(
roomId = room.id!!,
senderId = guest.id!!,
content = "두 번째",
),
)

mockMvc.perform(
get("/api/userchat/rooms/${room.id}/messages")
.param("after", firstMessage.id!!.toString())
.header("Authorization", "Bearer $guestToken"),
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.length()").value(1))
.andExpect(jsonPath("$.data[0].id").value(newer.id!!.toInt()))
.andExpect(jsonPath("$.data[0].content").value(newer.content))
}

@Test
@DisplayName("sendMessage stores message and updates room summary")
fun sendMessageStoresMessage() {
val before = chatMessageRepository.count()
val body = objectMapper.writeValueAsString(ChatMessageSendRequest("새 메세지"))

mockMvc.perform(
post("/api/userchat/rooms/${room.id}/messages")
.header("Authorization", "Bearer $guestToken")
.contentType(MediaType.APPLICATION_JSON)
.content(body),
)
.andExpect(status().isCreated)
.andExpect(jsonPath("$.data.content").value("새 메세지"))

val messages = chatMessageRepository.findAll()
assertEquals(before + 1, messages.size.toLong())
val latest = messages.maxByOrNull { it.id ?: 0L }!!
assertEquals(latest.id, chatRoomRepository.findById(room.id!!).get().lastMessageId)
}
}
Loading