diff --git a/src/main/kotlin/busanVibe/busan/domain/chat/controller/ChatRestController.kt b/src/main/kotlin/busanVibe/busan/domain/chat/controller/ChatRestController.kt
index 8ad7433..b8151b9 100644
--- a/src/main/kotlin/busanVibe/busan/domain/chat/controller/ChatRestController.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/chat/controller/ChatRestController.kt
@@ -6,8 +6,7 @@ import busanVibe.busan.domain.chat.service.ChatMongoService
import busanVibe.busan.global.apiPayload.exception.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
+import jakarta.validation.Valid
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
@@ -18,15 +17,20 @@ import org.springframework.web.bind.annotation.RestController
@Tag(name = "채팅 관련 API")
@RestController
@RequestMapping("/api/chat")
-class ChatRestController(
+class ChatRestController(
private val chatMongoService: ChatMongoService,
) {
- val log: Logger = LoggerFactory.getLogger(ChatRestController::class.java)
-
@PostMapping("/send")
- fun sendMessage(@RequestBody chatMessage: ChatMessageSendDTO) {
- log.info("POST /chat/send - 메시지 수신: $chatMessage")
+ @Operation(summary = "채팅 전송 API",
+ description = """
+ 채팅 전송 API 입니다. 해당 API 호출 시, 메시지가 전송되고, 채팅 웹소켓에 연결된 유저들에게 메시지가 전송됩니다.
+ 메시지의 길이는 최대 200글자입니다.
+ type 유형 : ['CHAT', 'BOT']
+ - CHAT (구현O): 일반 채팅
+ - BOT (구현X): 챗봇 기능입니다. 본인에게만 웹소켓 메시지가 전송되고, 채팅방을 나갈 시 다시 볼 수 없습니다.
+ """)
+ fun sendMessage(@Valid @RequestBody chatMessage: ChatMessageSendDTO) {
chatMongoService.saveAndPublish(chatMessage)
}
diff --git a/src/main/kotlin/busanVibe/busan/domain/chat/dto/websocket/ChatMessageSendDTO.kt b/src/main/kotlin/busanVibe/busan/domain/chat/dto/websocket/ChatMessageSendDTO.kt
index ed0f39d..ee4ad4f 100644
--- a/src/main/kotlin/busanVibe/busan/domain/chat/dto/websocket/ChatMessageSendDTO.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/chat/dto/websocket/ChatMessageSendDTO.kt
@@ -3,12 +3,15 @@ package busanVibe.busan.domain.chat.dto.websocket
import busanVibe.busan.domain.chat.enums.MessageType
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
-import java.time.LocalDateTime
+import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.Size
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class ChatMessageSendDTO(
// var userId: Long? = null,
var type: MessageType? = null, // 메시지 타입
+ @field:NotBlank(message = "빈 문자열을 전송할 수 없습니다.")
+ @field:Size(min = 1, max = 200, message = "메세지의 길이는 1이상 200이하입니다.")
var message: String? = null, // 메시지
// var time: LocalDateTime? = null, // 전송 시간
){
diff --git a/src/main/kotlin/busanVibe/busan/domain/chat/service/ChatMongoService.kt b/src/main/kotlin/busanVibe/busan/domain/chat/service/ChatMongoService.kt
index 8fb015a..ee8b171 100644
--- a/src/main/kotlin/busanVibe/busan/domain/chat/service/ChatMongoService.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/chat/service/ChatMongoService.kt
@@ -9,13 +9,12 @@ import busanVibe.busan.domain.chat.repository.ChatMongoRepository
import busanVibe.busan.domain.user.data.User
import busanVibe.busan.domain.user.repository.UserRepository
import busanVibe.busan.domain.user.service.login.AuthService
-import busanVibe.busan.global.apiPayload.code.ErrorReasonDTO
import busanVibe.busan.global.apiPayload.code.status.ErrorStatus
import busanVibe.busan.global.apiPayload.exception.GeneralException
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
import org.springframework.data.domain.Pageable
import org.springframework.data.redis.listener.ChannelTopic
-import org.springframework.http.HttpStatus
-import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Service
import java.time.LocalDateTime
@@ -25,29 +24,15 @@ class ChatMongoService(
private val redisPublisher: RedisPublisher,
private val topic: ChannelTopic,
private val userRepository: UserRepository,
- private val messagingTemplate: SimpMessagingTemplate
) {
+ val log: Logger = LoggerFactory.getLogger(ChatMongoService::class.java)
+
fun saveAndPublish(chatMessage: ChatMessageSendDTO) {
// 현재 유저 조회
val currentUser = AuthService().getCurrentUser()
- // 값 없으면 "" 저장
- val message = chatMessage.message ?: ""
-
- // 200자 넘어갈 시 예외처리
- if (message.length > 200) {
- val errorDTO = ErrorReasonDTO(
- httpStatus = HttpStatus.BAD_REQUEST,
- code = ErrorStatus.CHAT_INVALID_LENGTH.code,
- message = ErrorStatus.CHAT_INVALID_LENGTH.message,
- isSuccess = false
- )
- messagingTemplate.convertAndSend("/sub/chat/error", errorDTO)
- return // 또는 throw 후 처리
- }
-
// 채팅 객체 생성
val document = ChatMessage(
type = chatMessage.type?: MessageType.CHAT,
@@ -68,6 +53,7 @@ class ChatMongoService(
userId = currentUser.id
)
+ log.info("[CHAT] /POST/chat/send, 메시지 전송: $chatMessage")
redisPublisher.publish(topic, receiveDto)
}
diff --git a/src/main/kotlin/busanVibe/busan/domain/common/dto/InfoType.kt b/src/main/kotlin/busanVibe/busan/domain/common/dto/InfoType.kt
index a394f2b..c8c72e9 100644
--- a/src/main/kotlin/busanVibe/busan/domain/common/dto/InfoType.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/common/dto/InfoType.kt
@@ -11,7 +11,7 @@ enum class InfoType(
// 명소
SIGHT("명소", "SIGHT", PlaceType.SIGHT),
RESTAURANT("식당", "RESTAURANT", PlaceType.RESTAURANT),
- CAFE("카페", "CAFE", PlaceType.CAFE),
+// CAFE("카페", "CAFE", PlaceType.CAFE),
CULTURE("문화시설", "CULTURE", PlaceType.CULTURE),
// 축제
FESTIVAL("축제", "FESTIVAL"),
diff --git a/src/main/kotlin/busanVibe/busan/domain/festival/controller/FestivalController.kt b/src/main/kotlin/busanVibe/busan/domain/festival/controller/FestivalController.kt
index 6e770c9..0fe8974 100644
--- a/src/main/kotlin/busanVibe/busan/domain/festival/controller/FestivalController.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/festival/controller/FestivalController.kt
@@ -22,6 +22,7 @@ class FestivalController(
) {
@GetMapping
+ @Operation(summary = "지역축제 목록 조회")
fun festivalList(
@RequestParam("sort", required = false) sort: FestivalSortType,
@RequestParam("status", required = false)status: FestivalStatus
diff --git a/src/main/kotlin/busanVibe/busan/domain/place/enums/PlaceType.kt b/src/main/kotlin/busanVibe/busan/domain/place/enums/PlaceType.kt
index 67d7083..eb9145d 100644
--- a/src/main/kotlin/busanVibe/busan/domain/place/enums/PlaceType.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/place/enums/PlaceType.kt
@@ -14,7 +14,7 @@ enum class PlaceType(
ALL("전체", "ALL", "", "", ""),
SIGHT("관광지", "SIGHT", "12", "useTime", "restDate"),
RESTAURANT("식당", "RESTAURANT", "39", "openTimeFood", "restDateFood"),
- CAFE("카페", "CAFE", "00", "openTimeFood", "restDateFood"),
+// CAFE("카페", "CAFE", "00", "openTimeFood", "restDateFood"),
CULTURE("문화시설", "CULTURE", "14", "useTimeCulture", "restDateCulture")
;
diff --git a/src/main/kotlin/busanVibe/busan/domain/search/controller/SearchController.kt b/src/main/kotlin/busanVibe/busan/domain/search/controller/SearchController.kt
index f4fcabc..78b1b47 100644
--- a/src/main/kotlin/busanVibe/busan/domain/search/controller/SearchController.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/search/controller/SearchController.kt
@@ -5,6 +5,7 @@ import busanVibe.busan.domain.search.dto.SearchResultDTO
import busanVibe.busan.domain.search.enums.GeneralSortType
import busanVibe.busan.domain.search.service.SearchQueryService
import busanVibe.busan.global.apiPayload.exception.ApiResponse
+import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@@ -19,10 +20,12 @@ class SearchController(
) {
@GetMapping("/search")
+ @Operation(summary = "검색 API", description = "통합 검색 API 입니다.
[option: Festival] + [sort: CONGESTION] 조합은 안됩니다.")
fun searchResult(@RequestParam("option", defaultValue = "ALL") infoType: InfoType,
- @RequestParam("sort", defaultValue = "DEFAULT") sort: GeneralSortType): ApiResponse{
+ @RequestParam("sort", defaultValue = "DEFAULT") sort: GeneralSortType,
+ @RequestParam("keyword", required = false, defaultValue = "") keyword: String): ApiResponse{
- val searchResult = searchQueryService.getSearchResult(infoType, sort)
+ val searchResult = searchQueryService.getSearchResult(infoType, sort, keyword)
return ApiResponse.onSuccess(searchResult)
}
diff --git a/src/main/kotlin/busanVibe/busan/domain/search/service/SearchQueryService.kt b/src/main/kotlin/busanVibe/busan/domain/search/service/SearchQueryService.kt
index 45bea9a..4a099f6 100644
--- a/src/main/kotlin/busanVibe/busan/domain/search/service/SearchQueryService.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/search/service/SearchQueryService.kt
@@ -25,7 +25,7 @@ class SearchQueryService(
) {
@Transactional(readOnly = true)
- fun getSearchResult(infoType: InfoType, sort: GeneralSortType): SearchResultDTO.ListDto {
+ fun getSearchResult(infoType: InfoType, sort: GeneralSortType, keyword: String): SearchResultDTO.ListDto {
val currentUser = AuthService().getCurrentUser()
// 축제 + 혼잡도 검색 예외 처리
@@ -36,7 +36,7 @@ class SearchQueryService(
// 명소 타입별 조회
val places = when (infoType) {
InfoType.ALL -> placeRepository.findAllWithLikesAndOpenTime()
- InfoType.CAFE -> placeRepository.findAllWithLikesAndOpenTimeByType(PlaceType.CAFE)
+// InfoType.CAFE -> placeRepository.findAllWithLikesAndOpenTimeByType(PlaceType.CAFE)
InfoType.RESTAURANT -> placeRepository.findAllWithLikesAndOpenTimeByType(PlaceType.RESTAURANT)
InfoType.SIGHT -> placeRepository.findAllWithLikesAndOpenTimeByType(PlaceType.SIGHT)
else -> emptyList()
@@ -48,13 +48,29 @@ class SearchQueryService(
else -> emptyList()
}
+ // 좋아요 정보 조회
val placeLikeList = placeLikeRepository.findLikeByPlace(places)
val festivalLikeList = festivalLikeRepository.findLikeByFestival(festivals)
- // 엔티티 List로 DTO List 반환
- val resultList = searchUtil.listToSearchDTO(places, festivals, sort, currentUser, placeLikeList, festivalLikeList)
+ // 엔티티 List로 DTO List 생성
+ val infoDtoList = searchUtil.listToSearchDTO(places, festivals, sort, currentUser, placeLikeList, festivalLikeList)
+ // 키워드 필터링 후 List 생성
+ val resultList: MutableList =
+ if (keyword.isNotEmpty()) { // 키워드가 존재하면, 이름에 키워드가 존재하지 않는 항목들을 제거
+ infoDtoList.toMutableList().apply { removeIf { !containKeyword(it, keyword) } }
+ } else {
+ infoDtoList.toMutableList()
+ }
+
+ // 반환
return SearchResultDTO.ListDto(sort.name, resultList)
}
+ private fun containKeyword(infoDto: SearchResultDTO.InfoDto, keyword: String): Boolean {
+ val name = infoDto.name.replace(" ", "").lowercase()
+ val key = keyword.replace(" ", "").lowercase()
+ return name.contains(key)
+ }
+
}
\ No newline at end of file
diff --git a/src/main/kotlin/busanVibe/busan/domain/tourApi/service/TourCommandService.kt b/src/main/kotlin/busanVibe/busan/domain/tourApi/service/TourCommandService.kt
index 5d8a74b..88c4ba5 100644
--- a/src/main/kotlin/busanVibe/busan/domain/tourApi/service/TourCommandService.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/tourApi/service/TourCommandService.kt
@@ -90,7 +90,7 @@ class TourCommandService(
private fun getUseTime(placeType: PlaceType, introItem: PlaceIntroductionItem?): String =
when (placeType) {
PlaceType.ALL -> introItem?.useTime
- PlaceType.CAFE -> introItem?.openTimeFood
+// PlaceType.CAFE -> introItem?.openTimeFood
PlaceType.RESTAURANT -> introItem?.openTimeFood
PlaceType.SIGHT -> introItem?.useTime
PlaceType.CULTURE -> introItem?.useTimeCulture
@@ -99,7 +99,7 @@ class TourCommandService(
private fun getRest(placeType: PlaceType, introItem: PlaceIntroductionItem?): String =
when (placeType) {
PlaceType.ALL -> introItem?.restDate
- PlaceType.CAFE -> introItem?.restDateFood
+// PlaceType.CAFE -> introItem?.restDateFood
PlaceType.RESTAURANT -> introItem?.restDateFood
PlaceType.SIGHT -> introItem?.restDate
PlaceType.CULTURE -> introItem?.restDateCulture
@@ -108,7 +108,7 @@ class TourCommandService(
private fun getCenter(placeType: PlaceType, introItem: PlaceIntroductionItem?): String =
when (placeType) {
PlaceType.ALL -> introItem?.infoCenter
- PlaceType.CAFE -> introItem?.infoCenterFood
+// PlaceType.CAFE -> introItem?.infoCenterFood
PlaceType.RESTAURANT -> introItem?.infoCenterFood
PlaceType.SIGHT -> introItem?.infoCenter
PlaceType.CULTURE -> introItem?.infoCenterCulture
diff --git a/src/main/kotlin/busanVibe/busan/domain/user/controller/UserAuthController.kt b/src/main/kotlin/busanVibe/busan/domain/user/controller/UserAuthController.kt
index 8aa514e..ea9d895 100644
--- a/src/main/kotlin/busanVibe/busan/domain/user/controller/UserAuthController.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/user/controller/UserAuthController.kt
@@ -27,6 +27,7 @@ class UserAuthController (
){
@GetMapping("/oauth/kakao")
+ @Operation(hidden = true)
fun callBack(@RequestParam("code") code: String): ResponseEntity {
val userResponse: UserLoginResponseDTO.LoginDto = userCommandService.loginOrRegisterByKakao(code)
val redirectHeader = loginRedirectUtil.getRedirectHeader(userResponse)
diff --git a/src/main/kotlin/busanVibe/busan/domain/user/service/UserQueryService.kt b/src/main/kotlin/busanVibe/busan/domain/user/service/UserQueryService.kt
index 20d5a12..52dea66 100644
--- a/src/main/kotlin/busanVibe/busan/domain/user/service/UserQueryService.kt
+++ b/src/main/kotlin/busanVibe/busan/domain/user/service/UserQueryService.kt
@@ -49,7 +49,7 @@ class UserQueryService(
// 명소 조회
val places = when(infoType){
InfoType.ALL -> placeRepository.findLikePlace(user)
- InfoType.RESTAURANT, InfoType.SIGHT, InfoType.CAFE -> placeRepository.findLikePlaceByType(
+ InfoType.RESTAURANT, InfoType.SIGHT -> placeRepository.findLikePlaceByType(
infoType.placeType?: throw GeneralException(ErrorStatus.SEARCH_INVALID_CONDITION),
user
)
diff --git a/src/main/kotlin/busanVibe/busan/global/apiPayload/exception/ExceptionAdvice.kt b/src/main/kotlin/busanVibe/busan/global/apiPayload/exception/ExceptionAdvice.kt
index 23c17b6..63453fe 100644
--- a/src/main/kotlin/busanVibe/busan/global/apiPayload/exception/ExceptionAdvice.kt
+++ b/src/main/kotlin/busanVibe/busan/global/apiPayload/exception/ExceptionAdvice.kt
@@ -9,6 +9,7 @@ import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
+import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.stereotype.Controller
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
@@ -51,6 +52,29 @@ class ExceptionAdvice : ResponseEntityExceptionHandler() {
return handleExceptionInternalArgs(ex, HttpHeaders.EMPTY, ErrorStatus.BAD_REQUEST, request, errors)
}
+ // 잘못된 body 요청 처리
+ override fun handleHttpMessageNotReadable(
+ e: HttpMessageNotReadableException,
+ headers: HttpHeaders,
+ status: HttpStatusCode,
+ request: WebRequest
+ ): ResponseEntity? {
+ val errorMessage = when {
+ e.message?.contains("Enum") == true -> "요청 값이 올바르지 않습니다. Enum 타입을 확인해주세요."
+ e.message?.contains("JSON parse error") == true -> "요청 Body의 JSON 형식이 올바르지 않습니다."
+ e.message?.contains("Required request body is missing") == true -> "요청 Body가 비어있습니다."
+ else -> "잘못된 요청 본문입니다. 요청 형식을 확인해주세요."
+ }
+
+ return handleExceptionInternalArgs(
+ e,
+ headers,
+ ErrorStatus.BAD_REQUEST,
+ request,
+ mapOf("body" to errorMessage)
+ )
+ }
+
@ExceptionHandler(MethodArgumentTypeMismatchException::class)
fun handleMethodArgumentTypeMismatch(
e: MethodArgumentTypeMismatchException,
@@ -132,6 +156,9 @@ class ExceptionAdvice : ResponseEntityExceptionHandler() {
return super.handleExceptionInternal(e, body, headers, status, request)
}
+
+
+
private fun handleExceptionInternalArgs(
e: Exception,
headers: HttpHeaders,