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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 호출 시, 메시지가 전송되고, 채팅 웹소켓에 연결된 유저들에게 메시지가 전송됩니다.
메시지의 길이는 <b>최대 200글자</b>입니다.
type 유형 : ['CHAT', 'BOT']
- CHAT (구현O): 일반 채팅
- BOT (구현X): 챗봇 기능입니다. 본인에게만 웹소켓 메시지가 전송되고, 채팅방을 나갈 시 다시 볼 수 없습니다.
""")
fun sendMessage(@Valid @RequestBody chatMessage: ChatMessageSendDTO) {
chatMongoService.saveAndPublish(chatMessage)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, // 전송 시간
){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -68,6 +53,7 @@ class ChatMongoService(
userId = currentUser.id
)

log.info("[CHAT] /POST/chat/send, 메시지 전송: $chatMessage")
redisPublisher.publish(topic, receiveDto)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class FestivalController(
) {

@GetMapping
@Operation(summary = "지역축제 목록 조회")
fun festivalList(
@RequestParam("sort", required = false) sort: FestivalSortType,
@RequestParam("status", required = false)status: FestivalStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,10 +20,12 @@ class SearchController(
) {

@GetMapping("/search")
@Operation(summary = "검색 API", description = "통합 검색 API 입니다.<br><b>[option: Festival] + [sort: CONGESTION]</b> 조합은 안됩니다.")
fun searchResult(@RequestParam("option", defaultValue = "ALL") infoType: InfoType,
@RequestParam("sort", defaultValue = "DEFAULT") sort: GeneralSortType): ApiResponse<SearchResultDTO.ListDto>{
@RequestParam("sort", defaultValue = "DEFAULT") sort: GeneralSortType,
@RequestParam("keyword", required = false, defaultValue = "") keyword: String): ApiResponse<SearchResultDTO.ListDto>{

val searchResult = searchQueryService.getSearchResult(infoType, sort)
val searchResult = searchQueryService.getSearchResult(infoType, sort, keyword)
return ApiResponse.onSuccess(searchResult)

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

// 축제 + 혼잡도 검색 예외 처리
Expand All @@ -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()
Expand All @@ -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<SearchResultDTO.InfoDto> =
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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class UserAuthController (
){

@GetMapping("/oauth/kakao")
@Operation(hidden = true)
fun callBack(@RequestParam("code") code: String): ResponseEntity<Void> {
val userResponse: UserLoginResponseDTO.LoginDto = userCommandService.loginOrRegisterByKakao(code)
val redirectHeader = loginRedirectUtil.getRedirectHeader(userResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Any>? {
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,
Expand Down Expand Up @@ -132,6 +156,9 @@ class ExceptionAdvice : ResponseEntityExceptionHandler() {
return super.handleExceptionInternal(e, body, headers, status, request)
}




private fun handleExceptionInternalArgs(
e: Exception,
headers: HttpHeaders,
Expand Down