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 @@ -105,7 +105,7 @@ spring:
- Path=/chats/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/users/(?<segment>.*), /$\{segment}
- RewritePath=/chats/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter

default-filters:
Expand Down
2 changes: 2 additions & 0 deletions src/backend/chat-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")

implementation("org.springframework.boot:spring-boot-starter-aop")

implementation("org.springframework.cloud:spring-cloud-starter")
implementation("org.springframework.cloud:spring-cloud-starter-config")
implementation("org.springframework.cloud:spring-cloud-starter-bootstrap")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class DirectController(
}

@ResponseBody
@GetMapping("chat/direct")
@GetMapping("/chat/direct")
override fun readPaging(
@RequestParam("page", defaultValue = "0") page: Int,
@RequestParam("size", defaultValue = "10") size: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import java.io.Serializable
@JsonSerialize
@JsonDeserialize
data class DirectMessageCreate(
@Schema(description = "채널 ID, 다이렉트 채팅방 생성 후 id. DirectId와 같음", example = "channel-12345")
@Schema(description = "채널 ID", example = "channel-12345")
val channelId: String,

@Schema(description = "프로필 이미지 URL", example = "http://example.com/image.png")
Expand All @@ -24,20 +24,21 @@ data class DirectMessageCreate(
@Schema(description = "메시지 내용", example = "Hello!")
val content: String,

@Schema(description = "썸네일 URL", example = "http://example.com/thumbnail.png", nullable = true)
@Schema(description = "썸네일 URL", nullable = true)
val thumbnail: String? = null,

@Schema(description = "부모 메시지 ID", example = "msg-12345", nullable = true)
@Schema(description = "부모 메시지 ID", nullable = true)
val parentId: String? = null,

@Schema(description = "부모 메시지 작성자 이름", example = "Jane Doe", nullable = true)
@Schema(description = "부모 메시지 작성자 이름", nullable = true)
val parentName: String? = null,

@Schema(description = "부모 메시지 내용", example = "Previous message content", nullable = true)
@Schema(description = "부모 메시지 내용", nullable = true)
val parentContent: String? = null,
) : Serializable {

fun toDomain(userId: String): DirectMessage {
return DirectMessage(
return DirectMessage.create(
channelId = channelId,
userId = userId,
type = DirectMessageType.CREATE,
Expand Down Expand Up @@ -70,6 +71,7 @@ data class DirectMessageEditRequest(
) : Serializable {
fun toDomain(userId: String): DirectMessage {
return DirectMessage(
id = id,
channelId = channelId,
userId = userId,
type = DirectMessageType.EDIT,
Expand Down Expand Up @@ -113,7 +115,7 @@ data class DirectMessageTypingRequest(
val name: String,
) : Serializable {
fun toDomain(userId: String): DirectMessage {
return DirectMessage(
return DirectMessage.create(
channelId = channelId,
userId = userId,
name = name,
Expand Down Expand Up @@ -151,7 +153,7 @@ data class FileRequest(
val fileType: DirectMessageType,
) : Serializable {
fun toDomain(userId: String, type: DirectMessageType): DirectMessage {
return DirectMessage(
return DirectMessage.create(
userId = userId,
name = name,
profileImage = profileImage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,99 @@
package com.asyncgate.chat_server.domain

import com.asyncgate.chat_server.support.utility.IdGenerator
import java.time.LocalDateTime

class DirectMessage(
val id: String? = null,

val id: String,
val channelId: String,
val userId: String,
val type: DirectMessageType,

val profileImage: String? = null,

val read: Map<Long, Boolean>? = null,

val name: String? = null,
val content: String? = null,
val thumbnail: String? = null,

val parentId: String? = null,
val parentName: String? = null,
val parentContent: String? = null,

val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
var updatedAt: LocalDateTime? = null,
var isDeleted: Boolean = false,
) {
companion object {
fun create(
channelId: String,
userId: String,
type: DirectMessageType,
profileImage: String? = null,
name: String? = null,
content: String? = null,
thumbnail: String? = null,
parentId: String? = null,
parentName: String? = null,
parentContent: String? = null,
): DirectMessage {
return DirectMessage(
id = IdGenerator.generate(),
channelId = channelId,
userId = userId,
type = type,
profileImage = profileImage,
name = name,
content = content,
thumbnail = thumbnail,
parentId = parentId,
parentName = parentName,
parentContent = parentContent,
isDeleted = false
)
}
}

/**
* 현재 메시지를 "삭제" 상태로 변경한 새로운 도메인 객체 반환.
* (실제 삭제 플래그는 엔티티 변환 시 toEntity()에서 처리)
*/
fun markDeleted(): DirectMessage {
return DirectMessage(
id = this.id,
channelId = this.channelId,
userId = this.userId,
type = this.type,
profileImage = this.profileImage,
read = this.read,
name = this.name,
content = this.content,
thumbnail = this.thumbnail,
parentId = this.parentId,
parentName = this.parentName,
parentContent = this.parentContent,
isDeleted = true
)
}

/**
* 현재 메시지를 수정하여, 새로운 메시지(수정본)를 생성.
* 새로운 메시지 type은 EDIT로 변경됨.
*/
fun withEdit(newName: String, newContent: String): DirectMessage {
return DirectMessage(
id = this.id,
channelId = this.channelId,
userId = this.userId,
type = DirectMessageType.EDIT,
profileImage = this.profileImage,
read = this.read,
name = newName,
content = newContent,
thumbnail = this.thumbnail,
parentId = this.parentId,
parentName = this.parentName,
parentContent = this.parentContent,
isDeleted = false
)
}

override fun toString(): String {
return "DirectMessage(" +
"id=$id, " +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.asyncgate.chat_server.entity

import com.asyncgate.chat_server.domain.DirectMessageType
import com.asyncgate.chat_server.support.utility.IdGenerator
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document

@Document(collection = "directMessage")
data class DirectMessageEntity(
@get:Id
val id: String = IdGenerator.generate(),
@Id
val id: String,

val channelId: String,
val userId: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.asyncgate.chat_server.entity

import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.LocalDateTime

abstract class MongoBaseTimeEntity {

@CreatedDate
@LastModifiedDate
var createdAt: LocalDateTime? = null

@LastModifiedDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.asyncgate.chat_server.support.response.FailResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MissingServletRequestParameterException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.servlet.resource.NoResourceFoundException
Expand All @@ -23,12 +24,32 @@ class ChatServerErrorHandler {
return ResponseEntity.status(errorType.status).body(response)
}

@ExceptionHandler(NoResourceFoundException::class)
fun handleResourceException(exception: NoResourceFoundException): ResponseEntity<FailResponse> {
log.error("🚨 [Global Error] Resource not found: ${exception.message}", exception)
val errorType = FailType.RESOURCE_NOT_FOUND
val response: FailResponse = FailResponse.of(
errorType.errorCode,
errorType.message,
errorType.status.value()
)
return ResponseEntity.status(errorType.status).body(response)
}

@ExceptionHandler(MissingServletRequestParameterException::class)
fun handleMissingRequestParamException(e: MissingServletRequestParameterException): ResponseEntity<FailResponse> {
log.error("🚨 [Global Error] Missing request parameter: ${e.parameterName}", e)
val errorType = FailType.REQUEST_PARAMETER_MISSING
val response: FailResponse = FailResponse.of(
errorType.errorCode,
errorType.message,
errorType.status.value()
)
return ResponseEntity.status(errorType.status).body(response)
}

@ExceptionHandler(Exception::class)
fun handleException(exception: Exception) {
log.error("🚨 [Global Error] ${exception.message}", exception)
}

@ExceptionHandler(NoResourceFoundException::class)
fun handleResourceException(exception: NoResourceFoundException) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ enum class FailType(

DIRECT_MESSAGE_BAD_REQUEST(HttpStatus.BAD_REQUEST, "DirectMessage_4002", "IMAGE가 필수입니다."),
DIRECT_MESSAGE_CONTENT_NULL(HttpStatus.BAD_REQUEST, "DirectMessage_4002", "CODE, SNIPPET 타입은 content가 필수입니다."),
REQUEST_PARAMETER_MISSING(HttpStatus.BAD_REQUEST, "Request_4001", "필수 요청 파라미터가 누락되었습니다."),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource_4040", "요청한 리소스를 찾을 수 없습니다."),
;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.asyncgate.chat_server.service
import com.asyncgate.chat_server.domain.DirectMessage
import com.asyncgate.chat_server.domain.DirectMessageType
import com.asyncgate.chat_server.domain.ReadStatus
import com.asyncgate.chat_server.exception.ChatServerException
import com.asyncgate.chat_server.exception.FailType
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -32,13 +30,13 @@ class DirectListener(
log.info("directMessage = $directMessage")

val msg = HashMap<String, String>()
msg["type"] = DirectMessageType.CREATE.toString().lowercase()
msg["type"] = DirectMessageType.CREATE.toString()
msg["userId"] = java.lang.String.valueOf(directMessage.userId)
msg["name"] = directMessage.name ?: ""
msg["profileImage"] = directMessage.profileImage ?: ""
msg["message"] = directMessage.content ?: ""
msg["time"] = java.lang.String.valueOf(directMessage.createdAt)
msg["id"] = directMessage.id ?: throw ChatServerException(FailType.X_DIRECT_INTERNAL_ERROR)
msg["id"] = directMessage.id

val serializable = objectMapper.writeValueAsString(msg)
template.convertAndSend("/topic/direct-message/" + directMessage.channelId, serializable)
Expand Down Expand Up @@ -85,13 +83,14 @@ class DirectListener(

when (directMessage.type) {
DirectMessageType.EDIT -> {
msg["id"] = directMessage.id
msg["type"] = DirectMessageType.EDIT.toString()
msg["userId"] = directMessage.userId
msg["channelId"] = directMessage.channelId
msg["content"] = directMessage.content ?: ""
}
DirectMessageType.DELETE -> {
msg["id"] = directMessage.id ?: throw ChatServerException(FailType.X_DIRECT_INTERNAL_ERROR)
msg["id"] = directMessage.id
msg["type"] = DirectMessageType.DELETE.toString()
msg["userId"] = directMessage.userId
msg["channelId"] = directMessage.channelId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.asyncgate.chat_server.exception.FailType
import com.asyncgate.chat_server.kafka.KafkaProperties
import com.asyncgate.chat_server.repository.DirectMessageRepository
import com.asyncgate.chat_server.repository.ReadStatusRepository
import com.asyncgate.chat_server.support.utility.IdGenerator
import com.asyncgate.chat_server.support.utility.S3Util
import com.asyncgate.chat_server.support.utility.toDomain
import com.asyncgate.chat_server.support.utility.toEntity
Expand Down Expand Up @@ -95,28 +94,24 @@ class DirectServiceImpl(

@Transactional
override fun edit(directMessage: DirectMessage) {
checkNotNull(directMessage.id) {
"Logic error: 수정시에는 id가 필수이므로 존재하지 않을 수 없음"
}

checkNotNull(directMessage.id) { "Logic error: 수정시에는 id가 필수이므로 존재하지 않을 수 없음" }
val pastMessage = directMessageRepository.findById(directMessage.id)

checkNotNull(pastMessage) {
"Logic error: 이미 Null Check 완료"
}
?: throw IllegalStateException("Logic error: 이미 Null Check 완료")

validPermission(directMessage, pastMessage)

val deletedEntity = pastMessage.toEntity().copy(isDeleted = true)
directMessageRepository.save(deletedEntity.toDomain())
// 도메인 멤버 메서드를 활용하여 수정 결과를 생성
val deletedMessage = pastMessage.markDeleted()

val updatedMessage = pastMessage.toEntity().copy(
id = IdGenerator.generate(),
name = directMessage.name,
content = directMessage.content,
type = DirectMessageType.EDIT
)
directMessageRepository.save(updatedMessage.toDomain())
checkNotNull(directMessage.name) { "Logic error: 수정시 name이 필수로 들어옴" }
checkNotNull(directMessage.content) { "Logic error: 수정시 content가 필수로 들어옴" }

val editedMessage = pastMessage.withEdit(directMessage.name, directMessage.content)

// 기존 메시지를 삭제 상태로 업데이트 (엔티티 변환 후, isDeleted를 true로 설정)
directMessageRepository.save(deletedMessage.toEntity().copy(isDeleted = true).toDomain())
// 새로 생성된 수정본 저장
directMessageRepository.save(editedMessage)

val key = directMessage.channelId
kafkaTemplateForDirectMessage.send(kafkaProperties.topic.directAction, key, directMessage)
Expand Down
Loading
Loading