diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..dad3f7a Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..667b6b4 --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: + - BVFH-170-develop + + +env: + AWS_REGION: ap-northeast-2 + ECR_REPOSITORY: cabbage/chat-service + SERVICE_NAME: chat-service + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build Docker image + run: | + ./gradlew bootJar + docker build -t ${{ env.ECR_REPOSITORY }}:latest -f Dockerfile . + + - name: Tag & Push to ECR + run: | + REPO_URI=$(aws ecr describe-repositories --repository-names ${{ env.ECR_REPOSITORY }} --region ${{ env.AWS_REGION }} --query "repositories[0].repositoryUri" --output text) + docker tag ${{ env.ECR_REPOSITORY }}:latest $REPO_URI:latest + docker push $REPO_URI:latest + + - name: Get image digest + id: digest + run: | + IMAGE_DIGEST=$(aws ecr describe-images --repository-name ${{ env.ECR_REPOSITORY }} --image-ids imageTag=latest --query 'imageDetails[0].imageDigest' --output text) + echo "Image digest: $IMAGE_DIGEST" + echo "digest=$IMAGE_DIGEST" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index df197ec..33505c5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ bin/ !**/src/test/**/bin/ ### IntelliJ IDEA ### +ChatMessageKafkaSendTestController.java .idea *.iws *.iml @@ -27,7 +28,6 @@ out/ !**/src/test/**/out/ ### Spring ### -application*.yml .env logs/ tmp/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a8477b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM eclipse-temurin:17-jdk-alpine +WORKDIR /app +COPY build/libs/chatservice-0.0.1-SNAPSHOT.jar app.jar + +# :흰색_확인_표시: ENTRYPOINT로 java만 실행 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 70d5957..0fbb7e7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.0' + id 'org.springframework.boot' version '3.4.1' id 'io.spring.dependency-management' version '1.1.7' } @@ -24,10 +24,25 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + //jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + //mongoDB implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' - implementation 'org.springframework.boot:spring-boot-starter-web' + //websocket implementation 'org.springframework.boot:spring-boot-starter-websocket' + //kafka + implementation 'org.springframework.kafka:spring-kafka' + //swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + //feign client + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + // S3 + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.281' + //actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' @@ -38,3 +53,9 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:2024.0.1" + } +} diff --git a/src.zip b/src.zip new file mode 100644 index 0000000..745a4d4 Binary files /dev/null and b/src.zip differ diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..0787fb7 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000..63b728b Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/com/.DS_Store b/src/main/java/com/.DS_Store new file mode 100644 index 0000000..c1257c0 Binary files /dev/null and b/src/main/java/com/.DS_Store differ diff --git a/src/main/java/com/chalnakchalnak/.DS_Store b/src/main/java/com/chalnakchalnak/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/main/java/com/chalnakchalnak/.DS_Store differ diff --git a/src/main/java/com/chalnakchalnak/chatservice/.DS_Store b/src/main/java/com/chalnakchalnak/chatservice/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/main/java/com/chalnakchalnak/chatservice/.DS_Store differ diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageQueryVoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageQueryVoMapper.java new file mode 100644 index 0000000..8fc18bd --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageQueryVoMapper.java @@ -0,0 +1,57 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.ReplyPreviewVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.GetMessagesRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.GetReadCheckPointRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.GetMessagesResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.GetReadCheckPointResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetMessagesRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetReadCheckPointRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatMessageQueryVoMapper { + + public GetMessagesRequestDto toGetMessagesRequestDto(GetMessagesRequestVo getMessagesRequestVo, String memberUuid) { + return GetMessagesRequestDto.builder() + .chatRoomUuid(getMessagesRequestVo.getChatRoomUuid()) + .memberUuid(memberUuid) + .lastMessageId(getMessagesRequestVo.getLastMessageId()) + .limit(getMessagesRequestVo.getLimit() != null ? getMessagesRequestVo.getLimit() : 20) + .build(); + } + + public GetMessagesResponseVo toGetMessagesResponseVo(GetMessagesResponseDto getMessagesResponseDto) { + return GetMessagesResponseVo.builder() + .messageUuid(getMessagesResponseDto.getMessageUuid()) + .chatRoomUuid(getMessagesResponseDto.getChatRoomUuid()) + .senderUuid(getMessagesResponseDto.getSenderUuid()) + .message(getMessagesResponseDto.getMessage()) + .messageType(getMessagesResponseDto.getMessageType()) + .sentAt(getMessagesResponseDto.getSentAt().toString()) + .replyToMessageUuid(getMessagesResponseDto.getReplyToMessageUuid()) + .replyPreview(getMessagesResponseDto.getReplyPreview() != null ? + ReplyPreviewVo.builder() + .senderUuid(getMessagesResponseDto.getReplyPreview().getSenderUuid()) + .message(getMessagesResponseDto.getReplyPreview().getMessage()) + .messageType(getMessagesResponseDto.getReplyPreview().getMessageType()) + .build() + : null) + .build(); + } + + public GetReadCheckPointRequestDto toGetReadCheckPointRequestDto(GetReadCheckPointRequestVo getReadCheckPointRequestVo) { + return GetReadCheckPointRequestDto.builder() + .chatRoomUuid(getReadCheckPointRequestVo.getChatRoomUuid()) + .memberUuid(getReadCheckPointRequestVo.getMemberUuid()) + .build(); + } + + public GetReadCheckPointResponseVo toGetReadCheckPointResponseVo(GetReadCheckPointResponseDto getReadCheckPointResponseDto) { + return GetReadCheckPointResponseVo.builder() + .lastReadMessageSentAt(getReadCheckPointResponseDto.getLastReadMessageSentAt().toString()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageVoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageVoMapper.java new file mode 100644 index 0000000..dea83af --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/ChatMessageVoMapper.java @@ -0,0 +1,33 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.ReadMessageRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.SendMessageRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Component +public class ChatMessageVoMapper { + + public SendMessageRequestDto toSendMessageRequestDto(SendMessageRequestVo sendMessageRequestVo) { + return SendMessageRequestDto.builder() + .chatRoomUuid(sendMessageRequestVo.getChatRoomUuid()) + .senderUuid(sendMessageRequestVo.getSenderUuid()) + .message(sendMessageRequestVo.getMessage()) + .messageType(sendMessageRequestVo.getMessageType()) + .sentAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))) + .replyToMessageUuid(sendMessageRequestVo.getReplyToMessageUuid()) + .build(); + } + + public ReadMessageRequestDto toReadMessageRequestDto(ReadMessageRequestVo readMessageRequestVo) { + return ReadMessageRequestDto.builder() + .chatRoomUuid(readMessageRequestVo.getChatRoomUuid()) + .memberUuid(readMessageRequestVo.getMemberUuid()) + .lastReadMessageSentAt(readMessageRequestVo.getLastReadMessageSentAt()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/PreSignedUrlVoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/PreSignedUrlVoMapper.java new file mode 100644 index 0000000..ad4bddc --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/mapper/PreSignedUrlVoMapper.java @@ -0,0 +1,25 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.PreSignedUrlRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.PreSignedUrlResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.PreSignedUrlRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class PreSignedUrlVoMapper { + + public PreSignedUrlRequestDto toPreSignedUrlRequestDto(PreSignedUrlRequestVo preSignedUrlRequestVo, String memberUuid) { + return PreSignedUrlRequestDto.builder() + .memberUuid(memberUuid) + .contentType(preSignedUrlRequestVo.getContentType()) + .build(); + } + + public PreSignedUrlResponseVo toPreSignedUrlResponseVo(PreSignedUrlResponseDto preSignedUrlResponseDto) { + return PreSignedUrlResponseVo.builder() + .url(preSignedUrlResponseDto.getUrl()) + .fields(preSignedUrlResponseDto.getFields()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/ReplyPreviewVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/ReplyPreviewVo.java new file mode 100644 index 0000000..56ccb8e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/ReplyPreviewVo.java @@ -0,0 +1,20 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ReplyPreviewVo { + private String senderUuid; + private String message; + private String messageType; + + @Builder + public ReplyPreviewVo(String senderUuid, String message, String messageType) { + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetMessagesRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetMessagesRequestVo.java new file mode 100644 index 0000000..777dda2 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetMessagesRequestVo.java @@ -0,0 +1,18 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GetMessagesRequestVo { + + @NotBlank(message = "채팅방 UUID는 필수입니다.") + private String chatRoomUuid; + + private String lastMessageId; + + private Integer limit; + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetReadCheckPointRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetReadCheckPointRequestVo.java new file mode 100644 index 0000000..b868367 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/GetReadCheckPointRequestVo.java @@ -0,0 +1,16 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GetReadCheckPointRequestVo { + + @NotBlank(message = "chatRoomUuid는 필수 값입니다.") + private String chatRoomUuid; + + @NotBlank(message = "memberUuid는 필수 값입니다.") + private String memberUuid; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/PreSignedUrlRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/PreSignedUrlRequestVo.java new file mode 100644 index 0000000..0209453 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/PreSignedUrlRequestVo.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PreSignedUrlRequestVo { + + @NotBlank(message = "파일 형식은 필수입니다.") + @Pattern( + regexp = "image/(png|jpeg|webp|bmp)", + message = "지원하지 않는 이미지 형식입니다." + ) + private String contentType; +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/ReadMessageRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/ReadMessageRequestVo.java new file mode 100644 index 0000000..17880ea --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/ReadMessageRequestVo.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReadMessageRequestVo { + + @NotBlank(message = "chatRoomUuid 는 필수 값입니다.") + private String chatRoomUuid; + + @NotBlank(message = "memberUuid 는 필수 값입니다.") + private String memberUuid; + + private LocalDateTime lastReadMessageSentAt; + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/SendMessageRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/SendMessageRequestVo.java new file mode 100644 index 0000000..fc1a6b2 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/in/SendMessageRequestVo.java @@ -0,0 +1,37 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in; + +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class SendMessageRequestVo { + + @NotBlank(message = "ChatRoomUuid는 필수 입력값입니다.") + private String chatRoomUuid; + + @NotBlank(message = "SenderUuid는 필수 입력값입니다.") + private String senderUuid; + + @NotBlank(message = "Message는 필수 입력값입니다.") + @Size(max = 5000, message = "메시지는 최대 5000자까지 입력 가능합니다.") + private String message; + + @NotNull(message = "MessageType은 필수 입력값입니다.") + private MessageType messageType; + + private String replyToMessageUuid; + + @AssertTrue(message = "REPLY 타입일 경우 replyToMessageId는 필수입니다.") + private boolean isValidReplyCondition() { + if (messageType == null) return true; + if (messageType == MessageType.REPLY) { + return replyToMessageUuid != null && !replyToMessageUuid.isBlank(); + } else { + return replyToMessageUuid == null || replyToMessageUuid.isBlank(); + } + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetMessagesResponseVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetMessagesResponseVo.java new file mode 100644 index 0000000..7e243f2 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetMessagesResponseVo.java @@ -0,0 +1,36 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.ReplyPreviewVo; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GetMessagesResponseVo { + + private String messageUuid; + private String chatRoomUuid; + private String senderUuid; + private String message; + private String messageType; + private String sentAt; + private String replyToMessageUuid; + private ReplyPreviewVo replyPreview; + + @Builder + public GetMessagesResponseVo( + String messageUuid, String chatRoomUuid, String senderUuid, + String message, String messageType, String sentAt, + String replyToMessageUuid, ReplyPreviewVo replyPreview + ) { + this.messageUuid = messageUuid; + this.chatRoomUuid = chatRoomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + this.replyToMessageUuid = replyToMessageUuid; + this.replyPreview = replyPreview; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetReadCheckPointResponseVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetReadCheckPointResponseVo.java new file mode 100644 index 0000000..d5233f7 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/GetReadCheckPointResponseVo.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetReadCheckPointResponseVo { + + private String lastReadMessageSentAt; + + @Builder + public GetReadCheckPointResponseVo(String lastReadMessageSentAt) { + this.lastReadMessageSentAt = lastReadMessageSentAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/PreSignedUrlResponseVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/PreSignedUrlResponseVo.java new file mode 100644 index 0000000..d5490b1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/vo/out/PreSignedUrlResponseVo.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Map; + +@Getter +public class PreSignedUrlResponseVo { + + private String url; + private Map fields; + + @Builder + public PreSignedUrlResponseVo(String url, Map fields) { + this.url = url; + this.fields = fields; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/config/WebConfig.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/config/WebConfig.java new file mode 100644 index 0000000..203fd3f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/config/WebConfig.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.web.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(false); + } +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageController.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageController.java new file mode 100644 index 0000000..27c021c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageController.java @@ -0,0 +1,39 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.web.presentation; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper.ChatMessageVoMapper; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.ReadMessageRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.SendMessageRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.ChatMessageUseCase; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +@RequiredArgsConstructor +@Slf4j +public class ChatMessageController { + + private final ChatMessageUseCase chatMessageUseCase; + private final ChatMessageVoMapper chatMessageVoMapper; + + @MessageMapping("/chat/send") + public void sendMessage(@Payload @Valid SendMessageRequestVo sendMessageRequestVo) { + log.info("Received message: {}", sendMessageRequestVo.getReplyToMessageUuid()); + chatMessageUseCase.sendMessage( + chatMessageVoMapper.toSendMessageRequestDto(sendMessageRequestVo) + ); + } + + @MessageMapping("/chat/read") + public void updateReadCheckPoint(@Payload ReadMessageRequestVo readMessageRequestVo) { + chatMessageUseCase.updateReadCheckPoint( + chatMessageVoMapper.toReadMessageRequestDto(readMessageRequestVo) + ); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageQueryController.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageQueryController.java new file mode 100644 index 0000000..ff8aff8 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/ChatMessageQueryController.java @@ -0,0 +1,59 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.web.presentation; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper.ChatMessageQueryVoMapper; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.GetMessagesRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.GetReadCheckPointRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.GetMessagesResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.GetReadCheckPointResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.ChatMessageQueryUseCase; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/chat/messages") +@RequiredArgsConstructor +public class ChatMessageQueryController { + + private final ChatMessageQueryUseCase chatMessageQueryUseCase; + private final ChatMessageQueryVoMapper chatMessageQueryVoMapper; + + @Operation( + summary = "Get Messages History API", + description = + "채팅방 메시지 이력 조회
lastMessageId : nullable(첫 페이지 조회 시 param에서 삭제)
limit : nullable(default = 20)
" + + "메시지 타입이 \"REPLY\"인 경우에만 replyToMessageUuid와 replyPreview가 함께 반환됩니다", + tags = {"chat-message"} + ) + @GetMapping("/history") + public List getMessagesHistory( + @RequestHeader("X-Member-Uuid") String memberUuid, + @ModelAttribute @Valid GetMessagesRequestVo getMessagesRequestVo + ) { + return chatMessageQueryUseCase.getMessages( + chatMessageQueryVoMapper.toGetMessagesRequestDto(getMessagesRequestVo, memberUuid) + ).stream() + .map(chatMessageQueryVoMapper::toGetMessagesResponseVo) + .toList(); + } + + @Operation( + summary = "Get Read Check Point API", + description = "채팅방 읽음 체크포인트 조회
채팅방 메시지 이력 조회와 함꼐 사용하여 상대방의 메시지 조회 여부를 나타내주세요.", + tags = {"chat-message"} + ) + @GetMapping("read-check-point") + public GetReadCheckPointResponseVo getReadCheckPoint( + @ModelAttribute @Valid GetReadCheckPointRequestVo getReadCheckPointRequestVo + ) { + return chatMessageQueryVoMapper.toGetReadCheckPointResponseVo( + chatMessageQueryUseCase.getReadCheckPoint( + chatMessageQueryVoMapper.toGetReadCheckPointRequestDto(getReadCheckPointRequestVo) + ) + ); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/PreSignedUrlController.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/PreSignedUrlController.java new file mode 100644 index 0000000..6724565 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/web/presentation/PreSignedUrlController.java @@ -0,0 +1,39 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.web.presentation; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.mapper.PreSignedUrlVoMapper; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.in.PreSignedUrlRequestVo; +import com.chalnakchalnak.chatservice.chatmessage.adpater.in.vo.out.PreSignedUrlResponseVo; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.PreSignedUrlUseCase; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/pre-signed-url") +public class PreSignedUrlController { + + private final PreSignedUrlUseCase preSignedUrlUseCase; + private final PreSignedUrlVoMapper preSignedUrlVoMapper; + + @Operation( + summary = "Get AWS S3 Pre-signed URL for image upload", + description = "이미지 업로드를 위한 AWS S3 Pre-signed URL을 요청합니다.
지원하는 이미지 형식: png, jpeg, webp, bmp", + tags = {"chat-message"} + ) + @GetMapping + public PreSignedUrlResponseVo getPreSignedUrl( + @RequestHeader("X-Member-Uuid") String memberUuid, + @ModelAttribute @Valid PreSignedUrlRequestVo preSignedUrlRequestVo + ) { + return preSignedUrlVoMapper.toPreSignedUrlResponseVo( + preSignedUrlUseCase.generatePreSignedUrl( + preSignedUrlVoMapper.toPreSignedUrlRequestDto(preSignedUrlRequestVo, memberUuid) + ) + ); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/ChatHandshakeInterceptor.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/ChatHandshakeInterceptor.java new file mode 100644 index 0000000..6aad2ab --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/ChatHandshakeInterceptor.java @@ -0,0 +1,58 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.websocket; + +import com.chalnakchalnak.chatservice.chatroom.application.port.out.validator.ChatRoomValidatorPort; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ChatHandshakeInterceptor implements HandshakeInterceptor { + + private final ChatRoomValidatorPort chatRoomValidator; + + @Override + public boolean beforeHandshake(ServerHttpRequest request, + ServerHttpResponse response, + WebSocketHandler webSocketHandler, + Map attributes) { + + if (!(request instanceof ServletServerHttpRequest)) return false; + + ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; + HttpServletRequest httpRequest = servletRequest.getServletRequest(); + + String memberUuid = httpRequest.getParameter("memberUuid"); + String chatRoomUuid = httpRequest.getParameter("chatRoomUuid"); + + if (memberUuid == null || chatRoomUuid == null) { + log.warn("WebSocket 연결 거부: 파라미터 없음"); + return false; + } + + chatRoomValidator.memberAccessed(chatRoomUuid, memberUuid); + + // 검증 통과 + attributes.put("memberUuid", memberUuid); + attributes.put("chatRoomUuid", chatRoomUuid); + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, + ServerHttpResponse response, + WebSocketHandler webSocketHandler, + Exception exception) { + log.info("WebSocket 연결"); + } +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/HandshakeHandler.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/HandshakeHandler.java new file mode 100644 index 0000000..ed83eca --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/HandshakeHandler.java @@ -0,0 +1,24 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.websocket; + +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.support.DefaultHandshakeHandler; + +import java.security.Principal; +import java.util.Map; + +@Component +public class HandshakeHandler extends DefaultHandshakeHandler { + + @Override + protected Principal determineUser(ServerHttpRequest request, + WebSocketHandler webSocketHandler, + Map attributes) { + + String memberUuid = ((ServletServerHttpRequest) request).getServletRequest().getParameter("memberUuid"); + + return () -> memberUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/WebSocketConfig.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/WebSocketConfig.java new file mode 100644 index 0000000..14beb50 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/WebSocketConfig.java @@ -0,0 +1,47 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.websocket; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +@RequiredArgsConstructor +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ChatHandshakeInterceptor chatHandshakeInterceptor; + private final HandshakeHandler handshakeHandler; + + @Override + public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { + stompEndpointRegistry + .addEndpoint("/ws/chat") + .addInterceptors(chatHandshakeInterceptor) + .setHandshakeHandler(handshakeHandler) + .setAllowedOriginPatterns("*"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { + messageBrokerRegistry.enableSimpleBroker("/topic", "/queue") + .setHeartbeatValue(new long[]{10000L, 10000L}) + .setTaskScheduler(messageBrokerTaskScheduler()); + messageBrokerRegistry.setApplicationDestinationPrefixes("/pub"); + messageBrokerRegistry.setUserDestinationPrefix("/user"); + } + + @Bean(name = "customMessageBrokerTaskScheduler") + public ThreadPoolTaskScheduler messageBrokerTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("wss-heartbeat-"); + scheduler.setPoolSize(1); + scheduler.initialize(); + return scheduler; + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketErrorMessage.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketErrorMessage.java new file mode 100644 index 0000000..90276d1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketErrorMessage.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.websocket.exception; + +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class WebSocketErrorMessage { + private final int code; + private final String message; + + public WebSocketErrorMessage(BaseResponseStatus status) { + this.code = status.getCode(); + this.message = status.getMessage(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketExceptionHandler.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketExceptionHandler.java new file mode 100644 index 0000000..ff0e50e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/in/websocket/exception/WebSocketExceptionHandler.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.in.websocket.exception; + +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.simp.annotation.SendToUser; +import org.springframework.web.bind.annotation.ControllerAdvice; + +@Slf4j +@ControllerAdvice +public class WebSocketExceptionHandler { + + @MessageExceptionHandler(BaseException.class) + @SendToUser("/queue/errors") + public WebSocketErrorMessage handleWebSocketBaseException(BaseException ex) { + log.warn("WebSocket 처리 중 에러 발생: {}", ex.getMessage()); + + BaseResponseStatus status = ex.getStatus(); + return new WebSocketErrorMessage(status); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageConsumer.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageConsumer.java new file mode 100644 index 0000000..4491912 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageConsumer.java @@ -0,0 +1,42 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.kafka; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatMessageRepositoryPort; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.DuplicateKeyException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.support.Acknowledgment; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class KafkaChatMessageConsumer { + + private final SimpMessagingTemplate messagingTemplate; + private final ChatMessageRepositoryPort chatMessageRepositoryPort; + private final ObjectMapper objectMapper; + + @KafkaListener(topics = "chat.private.room") + public void consume(String payload, Acknowledgment ack) { + try { + ChatMessageDto message = objectMapper.readValue(payload, ChatMessageDto.class); + + log.info("kafka 메시지 수신 : " + message); + chatMessageRepositoryPort.processMessage(message); + + messagingTemplate.convertAndSend("/topic/chatroom/" + message.getChatRoomUuid(), message); + + ack.acknowledge(); + log.info("Kafka 메시지 처리 성공: {}", message.getChatRoomUuid()); + } catch (DuplicateKeyException e) { + ack.acknowledge(); + log.warn("중복된 메시지 수신, 전송 생략: {}", e.getMessage()); + } catch (Exception e) { + log.error("Kafka 메시지 소비 실패, 재시도 수행", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageProducer.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageProducer.java new file mode 100644 index 0000000..bcfe13b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/KafkaChatMessageProducer.java @@ -0,0 +1,76 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.kafka; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.kafka.mapper.KafkaEventDtoMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.PublishChatMessagePort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.GenerateUuidPort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.errors.TimeoutException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KafkaChatMessageProducer implements PublishChatMessagePort { + + private final KafkaTemplate kafkaTemplate; + private final ObjectMapper objectMapper; + private final KafkaEventDtoMapper kafkaEventDtoMapper; + private final GenerateUuidPort generateUuidPort; + + private final String TOPIC_NAME = "chat.private.room"; + + @Value("${spring.kafka.producer.properties.request.timeout.ms}") + private int TIMEOUT_MILLISECONDS; + + @Override + public Boolean publishChatMessage(SendMessageRequestDto sendMessageRequestDto) { + final ChatMessageDto chatMessageDto = kafkaEventDtoMapper.toChatMessageDto( + sendMessageRequestDto, generateUuidPort.generateUuid() + ); + + final String payload = toJson(chatMessageDto); + log.info("Kafka 전송 준비: chatRoomUuid={}, payload={}", chatMessageDto.getChatRoomUuid(), payload); + + try { + CompletableFuture> future = + kafkaTemplate.send(TOPIC_NAME, chatMessageDto.getChatRoomUuid(), payload); + + SendResult result = future.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); + + RecordMetadata meta = result.getRecordMetadata(); + log.info("Kafka 전송 성공: topic={}, partition={}, offset={}", + meta.topic(), meta.partition(), meta.offset()); + + return true; + + } catch (TimeoutException e) { + log.warn("Kafka 전송 타임아웃 (5초)", e); + return false; + } catch (Exception e) { + log.error("Kafka 전송 실패", e); + return false; + } + } + + private String toJson(ChatMessageDto chatMessageDto) { + try { + return objectMapper.writeValueAsString(chatMessageDto); + } catch (JsonProcessingException e) { + log.error("Kafka 메시지 직렬화 실패: {}", chatMessageDto, e); + throw new BaseException(BaseResponseStatus.FAILED_SERIALIZE_MESSAGE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/config/JacksonConfig.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/config/JacksonConfig.java new file mode 100644 index 0000000..2fcdfbf --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/config/JacksonConfig.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.kafka.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/mapper/KafkaEventDtoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/mapper/KafkaEventDtoMapper.java new file mode 100644 index 0000000..128f11b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/kafka/mapper/KafkaEventDtoMapper.java @@ -0,0 +1,45 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.kafka.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ReplyPreviewDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatMessageRepositoryPort; +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class KafkaEventDtoMapper { + + private final ChatMessageRepositoryPort chatMessageRepositoryPort; + + public ChatMessageDto toChatMessageDto(SendMessageRequestDto sendMessageRequestDto, String uuid) { + String replyToMessageUuid = null; + ReplyPreviewDto replyPreview = null; + + if (sendMessageRequestDto.getMessageType() == MessageType.REPLY && sendMessageRequestDto.getReplyToMessageUuid() != null) { + GetMessagesResponseDto origin = + chatMessageRepositoryPort.findByMessageUuid(sendMessageRequestDto.getReplyToMessageUuid()); + + replyToMessageUuid = sendMessageRequestDto.getReplyToMessageUuid(); + replyPreview = ReplyPreviewDto.builder() + .senderUuid(origin.getSenderUuid()) + .message(origin.getMessage()) + .messageType(origin.getMessageType().toString()) + .build(); + } + + return ChatMessageDto.builder() + .messageUuid(uuid) + .chatRoomUuid(sendMessageRequestDto.getChatRoomUuid()) + .senderUuid(sendMessageRequestDto.getSenderUuid()) + .message(sendMessageRequestDto.getMessage()) + .messageType(sendMessageRequestDto.getMessageType().toString()) + .sentAt(sendMessageRequestDto.getSentAt()) + .replyToMessageUuid(replyToMessageUuid) + .replyPreview(replyPreview) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatMessageDocument.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatMessageDocument.java new file mode 100644 index 0000000..0ec20f7 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatMessageDocument.java @@ -0,0 +1,48 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "chat_message") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChatMessageDocument { + + @Id + private ObjectId id; + + @Indexed(unique = true) + private String messageUuid; + + private String chatRoomUuid; + private String senderUuid; + private String message; + private String messageType; + private LocalDateTime sentAt; + private String replyToMessageUuid; + private ReplyPreview replyPreview; + + @Builder + public ChatMessageDocument(ObjectId id, String messageUuid, String chatRoomUuid, + String senderUuid, String message, String messageType, + LocalDateTime sentAt, String replyToMessageUuid, ReplyPreview replyPreview + ) { + this.id = id; + this.messageUuid = messageUuid; + this.chatRoomUuid = chatRoomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + this.replyToMessageUuid = replyToMessageUuid; + this.replyPreview = replyPreview; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatReadCheckPointDocument.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatReadCheckPointDocument.java new file mode 100644 index 0000000..177737c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ChatReadCheckPointDocument.java @@ -0,0 +1,36 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "chat_read_checkpoint") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChatReadCheckPointDocument { + @Id + private String id; + private String chatRoomUuid; + private String memberUuid; + private LocalDateTime lastReadMessageSentAt; + private LocalDateTime updatedAt; + + @Builder + public ChatReadCheckPointDocument(String id, String chatRoomUuid, String memberUuid, LocalDateTime lastReadMessageSentAt, LocalDateTime updatedAt) { + this.id = id; + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.lastReadMessageSentAt = lastReadMessageSentAt; + this.updatedAt = updatedAt; + } + + public void updateCheckpoint(LocalDateTime lastReadMessageSentAt, LocalDateTime updatedAt) { + this.lastReadMessageSentAt = lastReadMessageSentAt; + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ReplyPreview.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ReplyPreview.java new file mode 100644 index 0000000..aa0ee27 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/entity/ReplyPreview.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity; + +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReplyPreview { + + private String senderUuid; + private String message; + private MessageType messageType; + + @Builder + public ReplyPreview(String senderUuid, String message, MessageType messageType) { + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatMessageDocumentMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatMessageDocumentMapper.java new file mode 100644 index 0000000..56ee37b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatMessageDocumentMapper.java @@ -0,0 +1,49 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatMessageDocument; +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ReplyPreview; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ReplyPreviewDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import org.springframework.stereotype.Component; + +@Component +public class ChatMessageDocumentMapper { + + public ChatMessageDocument toChatMessageDocument(ChatMessageDto chatMessageDto) { + return ChatMessageDocument.builder() + .messageUuid(chatMessageDto.getMessageUuid()) + .chatRoomUuid(chatMessageDto.getChatRoomUuid()) + .senderUuid(chatMessageDto.getSenderUuid()) + .message(chatMessageDto.getMessage()) + .messageType(chatMessageDto.getMessageType()) + .sentAt(chatMessageDto.getSentAt()).replyToMessageUuid(chatMessageDto.getReplyToMessageUuid()) + .replyPreview(chatMessageDto.getReplyPreview() == null ? null : ReplyPreview.builder() + .senderUuid(chatMessageDto.getReplyPreview().getSenderUuid()) + .message(chatMessageDto.getReplyPreview().getMessage()) + .messageType(MessageType.valueOf(chatMessageDto.getReplyPreview().getMessageType())) + .build()) + .build(); + } + + public GetMessagesResponseDto toGetMessagesResponseDto(ChatMessageDocument chatMessageDocument) { + return GetMessagesResponseDto.builder() + .messageUuid(chatMessageDocument.getMessageUuid()) + .chatRoomUuid(chatMessageDocument.getChatRoomUuid()) + .senderUuid(chatMessageDocument.getSenderUuid()) + .message(chatMessageDocument.getMessage()) + .messageType(chatMessageDocument.getMessageType().toString()) + .sentAt(chatMessageDocument.getSentAt()) + .replyToMessageUuid(chatMessageDocument.getReplyToMessageUuid()) + .replyPreview( + chatMessageDocument.getReplyPreview() != null ? + ReplyPreviewDto.builder() + .senderUuid(chatMessageDocument.getReplyPreview().getSenderUuid()) + .message(chatMessageDocument.getReplyPreview().getMessage()) + .messageType(chatMessageDocument.getReplyPreview().getMessageType().toString()) + .build() : null + ) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatReadCheckPointDocumentMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatReadCheckPointDocumentMapper.java new file mode 100644 index 0000000..0573b42 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/mapper/ChatReadCheckPointDocumentMapper.java @@ -0,0 +1,13 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatReadCheckPointDocumentMapper { + public GetReadCheckPointResponseDto toGetReadCheckPointResponseDto(String lastReadMessageSentAt) { + return GetReadCheckPointResponseDto.builder() + .lastReadMessageSentAt(lastReadMessageSentAt) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageMongoRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageMongoRepository.java new file mode 100644 index 0000000..3b9a52b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageMongoRepository.java @@ -0,0 +1,18 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatMessageDocument; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface ChatMessageMongoRepository extends MongoRepository { + List findTopByChatRoomUuidOrderByIdDesc(String chatRoomUuid, Pageable pageable); + List findByChatRoomUuidAndIdLessThanOrderByIdDesc(String chatRoomUuid, ObjectId lastId, Pageable pageable); + List findByChatRoomUuidAndSentAtAfterOrderByIdDesc(String chatRoomUuid, LocalDateTime sentAt, Pageable pageable); + Optional findByMessageUuid(String messageUuid); + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageQueryRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageQueryRepository.java new file mode 100644 index 0000000..af09d24 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageQueryRepository.java @@ -0,0 +1,71 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatMessageDocument; +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatReadCheckPointDocument; +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.mapper.ChatMessageDocumentMapper; +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.mapper.ChatReadCheckPointDocumentMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetMessagesRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetReadCheckPointRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatMessageQueryRepositoryPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberExitRepositoryPort; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ChatMessageQueryRepository implements ChatMessageQueryRepositoryPort { + + private final ChatMessageMongoRepository chatMessageMongoRepository; + private final ChatMessageDocumentMapper chatMessageDocumentMapper; + private final ChatReadCheckPointMongoRepository chatReadCheckPointMongoRepository; + private final ChatRoomMemberExitRepositoryPort chatRoomMemberExitRepositoryPort; + private final ChatReadCheckPointDocumentMapper chatReadCheckPointDocumentMapper; + + @Override + public List getMessages(GetMessagesRequestDto getMessagesRequestDto) { + final PageRequest pageable = PageRequest.of(0, getMessagesRequestDto.getLimit()); + List messages; + + if (getMessagesRequestDto.getLastMessageId() == null) { + final LocalDateTime exitedAt = chatRoomMemberExitRepositoryPort + .findByChatRoomUuidAndMemberUuid(getMessagesRequestDto.getChatRoomUuid(), getMessagesRequestDto.getMemberUuid()) + .map(chatRoomMemberExitDto -> chatRoomMemberExitDto.getExitedAt()) + .orElse(LocalDateTime.of(1970, 1, 1, 0, 0)); + + messages = chatMessageMongoRepository.findByChatRoomUuidAndSentAtAfterOrderByIdDesc( + getMessagesRequestDto.getChatRoomUuid(), exitedAt, pageable); + } else { + final ObjectId objectId = new ObjectId(getMessagesRequestDto.getLastMessageId()); + messages = chatMessageMongoRepository.findByChatRoomUuidAndIdLessThanOrderByIdDesc( + getMessagesRequestDto.getChatRoomUuid(), objectId, pageable); + } + + return messages + .stream() + .map(chatMessageDocumentMapper::toGetMessagesResponseDto) + .toList(); + } + + @Override + public GetReadCheckPointResponseDto getReadCheckPoint(GetReadCheckPointRequestDto getReadCheckPointRequestDto) { + final String lastReadMessageSentAt = + chatReadCheckPointMongoRepository + .findByChatRoomUuidAndMemberUuid( + getReadCheckPointRequestDto.getChatRoomUuid(), + getReadCheckPointRequestDto.getMemberUuid() + ) + .map(ChatReadCheckPointDocument::getLastReadMessageSentAt) + .orElse(LocalDateTime.of(1970, 1, 1, 0, 0)) + .toString(); + + + return chatReadCheckPointDocumentMapper.toGetReadCheckPointResponseDto(lastReadMessageSentAt); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageRepository.java new file mode 100644 index 0000000..84e189e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatMessageRepository.java @@ -0,0 +1,52 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.mapper.ChatMessageDocumentMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.mapper.ChatMessageMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatMessageRepositoryPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberRepositoryPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomSummaryUpdaterPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.PublishChatRoomSummaryUpdatePort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ChatMessageRepository implements ChatMessageRepositoryPort { + + private final ChatMessageMongoRepository chatMessageMongoRepository; + private final ChatMessageDocumentMapper chatMessageDocumentMapper; + private final ChatRoomSummaryUpdaterPort chatRoomSummaryUpdaterPort; + private final ChatRoomMemberRepositoryPort chatRoomMemberRepositoryPort; + private final PublishChatRoomSummaryUpdatePort publishChatRoomSummaryUpdatePort; + private final ChatMessageMapper chatMessageMapper; + +// @Transactional + @Override + public void processMessage(ChatMessageDto chatMessageDto) { + chatMessageMongoRepository.save( + chatMessageDocumentMapper.toChatMessageDocument(chatMessageDto) + ); + + final String receiverUuid = chatRoomMemberRepositoryPort.findOpponentUuid( + chatMessageDto.getChatRoomUuid(), chatMessageDto.getSenderUuid() + ); + + chatRoomSummaryUpdaterPort.updateOnMessage(chatMessageDto, receiverUuid); + + publishChatRoomSummaryUpdatePort.publishChatRoomSummaryUpdate( + chatMessageMapper.toChatRoomSummaryUpdateEventByMessage(chatMessageDto, receiverUuid) + ); + } + + @Override + public GetMessagesResponseDto findByMessageUuid(String messageUuid) { + return chatMessageMongoRepository + .findByMessageUuid(messageUuid) + .map(chatMessageDocumentMapper::toGetMessagesResponseDto) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_MESSAGE_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointMongoRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointMongoRepository.java new file mode 100644 index 0000000..8b951fc --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointMongoRepository.java @@ -0,0 +1,11 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatReadCheckPointDocument; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +public interface ChatReadCheckPointMongoRepository extends MongoRepository { + + Optional findByChatRoomUuidAndMemberUuid(String chatRoomUuid, String memberUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointUpdater.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointUpdater.java new file mode 100644 index 0000000..8942b5d --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/mongo/repository/ChatReadCheckPointUpdater.java @@ -0,0 +1,56 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.mongo.entity.ChatReadCheckPointDocument; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatReadCheckPointUpdaterPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Repository +@RequiredArgsConstructor +public class ChatReadCheckPointUpdater implements ChatReadCheckPointUpdaterPort { + + private final MongoTemplate mongoTemplate; + + @Override + public Boolean updateReadCheckPoint(ReadMessageRequestDto readMessageRequestDto) { + final LocalDateTime sentAt = readMessageRequestDto.getLastReadMessageSentAt(); + final LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + + Query query = Query.query(Criteria.where("chatRoomUuid").is(readMessageRequestDto.getChatRoomUuid()) + .and("memberUuid").is(readMessageRequestDto.getMemberUuid())); + + var existing = mongoTemplate.findOne(query, ChatReadCheckPointDocument.class, "chat_read_checkpoint"); + + // 2. 없으면 insert + if (existing == null) { + ChatReadCheckPointDocument doc = ChatReadCheckPointDocument.builder() + .chatRoomUuid(readMessageRequestDto.getChatRoomUuid()) + .memberUuid(readMessageRequestDto.getMemberUuid()) + .lastReadMessageSentAt(sentAt) + .updatedAt(now) + .build(); + mongoTemplate.insert(doc, "chat_read_checkpoint"); + return true; + } + + // 3. sentAt이 이전 값보다 클 때만 update + if (sentAt.isAfter(existing.getLastReadMessageSentAt())) { + Update update = new Update() + .set("lastReadMessageSentAt", sentAt) + .set("updatedAt", now); + mongoTemplate.updateFirst(query, update, "chat_read_checkpoint"); + return true; + } + + // 변경할 필요 없음 + return false; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/PreSignedUrlGenerator.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/PreSignedUrlGenerator.java new file mode 100644 index 0000000..9beee6f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/PreSignedUrlGenerator.java @@ -0,0 +1,71 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.s3; + +import com.chalnakchalnak.chatservice.chatmessage.adpater.out.util.PreSignedUrlUtil; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.PreSignedUrlDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.mapper.PreSignedUrlMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.GeneratePreSignedUrlPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +@Component +@RequiredArgsConstructor +public class PreSignedUrlGenerator implements GeneratePreSignedUrlPort { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + private final String algorithm = "AWS4-HMAC-SHA256"; + private final String service = "s3"; + + private final PreSignedUrlUtil preSignedUrlUtil; + private final PreSignedUrlMapper preSignedUrlMapper; + + @Override + public PreSignedUrlResponseDto generatePreSignedUrl(PreSignedUrlDto preSignedUrlDto) { + Date now = new Date(); + + String date = formatDate(now, "yyyyMMdd"); + String dateTime = formatDate(now, "yyyyMMdd'T'HHmmss'Z'"); + String credential = accessKey + "/" + date + "/" + region + "/" + service + "/aws4_request"; + + String policyDoc = PreSignedUrlUtil.generateDoc(bucket, algorithm, credential, dateTime, preSignedUrlDto.getContentType()); + String policy = Base64.getEncoder().encodeToString(policyDoc.getBytes(StandardCharsets.UTF_8)); + byte[] signingKey = preSignedUrlUtil.getSignatureKey(secretKey, date, region, service); + String signature = preSignedUrlUtil.bytesToHex(preSignedUrlUtil.hmacSha256(signingKey, policy)); + + Map fields = new LinkedHashMap<>(); + fields.put("key", preSignedUrlDto.getKey()); + fields.put("Content-Type", preSignedUrlDto.getContentType()); + fields.put("bucket", bucket); + fields.put("X-Amz-Algorithm", algorithm); + fields.put("X-Amz-Credential", credential); + fields.put("X-Amz-Date", dateTime); + fields.put("Policy", policy); + fields.put("X-Amz-Signature", signature); + + String url = "https://" + bucket + ".s3." + region + ".amazonaws.com/"; + + return preSignedUrlMapper.toPreSignedUrlResponseDto(url, fields); + } + + private String formatDate(Date date, String pattern) { + SimpleDateFormat fmt = new SimpleDateFormat(pattern); + fmt.setTimeZone(TimeZone.getTimeZone("UTC")); + return fmt.format(date); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/S3ImageKeyValidator.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/S3ImageKeyValidator.java new file mode 100644 index 0000000..f1efaa8 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/s3/S3ImageKeyValidator.java @@ -0,0 +1,67 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.GetObjectMetadataRequest; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ImageKeyValidatorPort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class S3ImageKeyValidator implements ImageKeyValidatorPort { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + /** + * 이미지 타입 메시지일 경우, 키 형식 및 S3 존재 여부 검증 + */ + @Override + public void validateImageMessage(SendMessageRequestDto sendMessageRequestDto) { + final String key = sendMessageRequestDto.getMessage(); + + validateKeyFormat(key); + validateOwnership(sendMessageRequestDto.getSenderUuid(), key); + validateS3Existence(key); + } + + /** + * key 형식 검증: chat/{memberUuid}/images/{uuid}.확장자 + */ + private void validateKeyFormat(String key) { + if (!key.matches("^chat/[\\w-]+/images/[\\w-]+\\.[a-zA-Z0-9]+$")) { + throw new BaseException(BaseResponseStatus.INVALID_IMAGE_MESSAGE_KEY_FORMAT); + } + } + + /** + * senderUuid가 key 내 포함된 memberUuid와 일치하는지 검증 + */ + private void validateOwnership(String senderUuid, String key) { + String[] parts = key.split("/"); + if (parts.length < 3 || !parts[1].equals(senderUuid)) { + throw new BaseException(BaseResponseStatus.INVALID_IMAGE_MESSAGE_SENDER); + } + } + + /** + * 실제 S3 객체 존재 여부 확인 + */ + private void validateS3Existence(String key) { + try { + amazonS3.getObjectMetadata(new GetObjectMetadataRequest(bucket, key)); + } catch (AmazonS3Exception e) { + if (e.getStatusCode() == 404) { + throw new BaseException(BaseResponseStatus.IMAGE_FILE_NOT_FOUND_IN_S3); + } + throw new BaseException(BaseResponseStatus.FAILED_TO_ACCESS_S3); + } + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/util/PreSignedUrlUtil.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/util/PreSignedUrlUtil.java new file mode 100644 index 0000000..cd85341 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/util/PreSignedUrlUtil.java @@ -0,0 +1,72 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.util; + +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class PreSignedUrlUtil { + + private static final String algorithm = "HmacSHA256"; + private static final String data = "aws4_request"; + + public static String generateDoc(String bucket, + String algorithm, + String credential, + String dateTime, + String contentType) { + return """ + { + "expiration": "%s", + "conditions": [ + {"bucket": "%s"}, + ["starts-with", "$key", ""], + {"x-amz-algorithm": "%s"}, + {"x-amz-credential": "%s"}, + {"x-amz-date": "%s"}, + ["content-length-range", 0, 10485760], + {"Content-Type": "%s"} + ] + } + """.formatted( + new Date(System.currentTimeMillis() + 5 * 60 * 1000).toInstant().toString(), + bucket, + algorithm, + credential, + dateTime, + contentType + ); + } + + public static byte[] hmacSha256(byte[] key, String data) { + try { + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new BaseException(BaseResponseStatus.UNABLE_TO_CALCULATE_HMAC); + } + } + + public static byte[] getSignatureKey(String secretKey, String date, String region, String service) { + byte[] kDate = hmacSha256(("AWS4" + secretKey).getBytes(StandardCharsets.UTF_8), date); + byte[] kRegion = hmacSha256(kDate, region); + byte[] kService = hmacSha256(kRegion, service); + return hmacSha256(kService, data); + } + + public static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/websocket/message/SendMessageToClient.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/websocket/message/SendMessageToClient.java new file mode 100644 index 0000000..8da6635 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/adpater/out/websocket/message/SendMessageToClient.java @@ -0,0 +1,25 @@ +package com.chalnakchalnak.chatservice.chatmessage.adpater.out.websocket.message; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.SendMessageToClientPort; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SendMessageToClient implements SendMessageToClientPort { + + private final SimpMessagingTemplate messagingTemplate; + + @Override + public void sendMessage(ReadMessageRequestDto readMessageRequestDto, String opponentUuid) { + messagingTemplate.convertAndSendToUser( + opponentUuid, + "/queue/chatroom/read", + readMessageRequestDto + ); + } + + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ChatMessageDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ChatMessageDto.java new file mode 100644 index 0000000..361dd66 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ChatMessageDto.java @@ -0,0 +1,41 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class ChatMessageDto { + + private String messageUuid; + private String chatRoomUuid; + private String senderUuid; + private String message; + private String messageType; + @JsonFormat(shape = JsonFormat.Shape.STRING, timezone = "Asia/Seoul") + private LocalDateTime sentAt; + private String replyToMessageUuid; + private ReplyPreviewDto replyPreview; + + @Builder + public ChatMessageDto( + String messageUuid, String chatRoomUuid, String senderUuid, + String message, String messageType, LocalDateTime sentAt, + String replyToMessageUuid, ReplyPreviewDto replyPreview + ) { + this.messageUuid = messageUuid; + this.chatRoomUuid = chatRoomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + this.replyToMessageUuid = replyToMessageUuid; + this.replyPreview = replyPreview; + } + + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/PreSignedUrlDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/PreSignedUrlDto.java new file mode 100644 index 0000000..f0085ad --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/PreSignedUrlDto.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PreSignedUrlDto { + + private String key; + private String contentType; + + @Builder + public PreSignedUrlDto(String key, String contentType) { + this.key = key; + this.contentType = contentType; + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ReplyPreviewDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ReplyPreviewDto.java new file mode 100644 index 0000000..396047a --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/ReplyPreviewDto.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReplyPreviewDto { + + private String senderUuid; + private String message; + private String messageType; + + @Builder + public ReplyPreviewDto(String senderUuid, String message, String messageType) { + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetMessagesRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetMessagesRequestDto.java new file mode 100644 index 0000000..912c8b6 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetMessagesRequestDto.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetMessagesRequestDto { + + private String chatRoomUuid; + private String memberUuid; + private String lastMessageId; + private Integer limit; + + @Builder + public GetMessagesRequestDto(String chatRoomUuid, String memberUuid, String lastMessageId, Integer limit) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.lastMessageId = lastMessageId; + this.limit = limit; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetReadCheckPointRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetReadCheckPointRequestDto.java new file mode 100644 index 0000000..7483fb2 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/GetReadCheckPointRequestDto.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetReadCheckPointRequestDto { + + private String chatRoomUuid; + private String memberUuid; + + @Builder + public GetReadCheckPointRequestDto(String chatRoomUuid, String memberUuid) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/PreSignedUrlRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/PreSignedUrlRequestDto.java new file mode 100644 index 0000000..f08c895 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/PreSignedUrlRequestDto.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PreSignedUrlRequestDto { + + private String memberUuid; + private String contentType; + + @Builder + public PreSignedUrlRequestDto(String memberUuid, String contentType) { + this.memberUuid = memberUuid; + this.contentType = contentType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/ReadMessageRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/ReadMessageRequestDto.java new file mode 100644 index 0000000..887e898 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/ReadMessageRequestDto.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ReadMessageRequestDto { + + private String chatRoomUuid; + private String memberUuid; + private LocalDateTime lastReadMessageSentAt; + + @Builder + public ReadMessageRequestDto(String chatRoomUuid, String memberUuid, LocalDateTime lastReadMessageSentAt) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.lastReadMessageSentAt = lastReadMessageSentAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/SendMessageRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/SendMessageRequestDto.java new file mode 100644 index 0000000..74077ac --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/in/SendMessageRequestDto.java @@ -0,0 +1,29 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.in; + +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class SendMessageRequestDto { + + private String chatRoomUuid; + private String senderUuid; + private String message; + private MessageType messageType; + private LocalDateTime sentAt; + private String replyToMessageUuid; + + @Builder + public SendMessageRequestDto(String chatRoomUuid, String senderUuid, String message, + MessageType messageType, LocalDateTime sentAt, String replyToMessageUuid) { + this.chatRoomUuid = chatRoomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + this.replyToMessageUuid = replyToMessageUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetMessagesResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetMessagesResponseDto.java new file mode 100644 index 0000000..c3c80b6 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetMessagesResponseDto.java @@ -0,0 +1,36 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ReplyPreviewDto; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class GetMessagesResponseDto { + + private String messageUuid; + private String chatRoomUuid; + private String senderUuid; + private String message; + private String messageType; + private LocalDateTime sentAt; + private String replyToMessageUuid; + private ReplyPreviewDto replyPreview; + + @Builder + public GetMessagesResponseDto( + String messageUuid, String chatRoomUuid, String senderUuid, + String message, String messageType, LocalDateTime sentAt, + String replyToMessageUuid, ReplyPreviewDto replyPreview + ) { + this.messageUuid = messageUuid; + this.chatRoomUuid = chatRoomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + this.replyToMessageUuid = replyToMessageUuid; + this.replyPreview = replyPreview; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetReadCheckPointResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetReadCheckPointResponseDto.java new file mode 100644 index 0000000..0850b1b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/GetReadCheckPointResponseDto.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.out; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetReadCheckPointResponseDto { + + private String lastReadMessageSentAt; + + @Builder + public GetReadCheckPointResponseDto(String lastReadMessageSentAt) { + this.lastReadMessageSentAt = lastReadMessageSentAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/PreSignedUrlResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/PreSignedUrlResponseDto.java new file mode 100644 index 0000000..9c024c8 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/dto/out/PreSignedUrlResponseDto.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.dto.out; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Map; + +@Getter +public class PreSignedUrlResponseDto { + + private String url; + private Map fields; + + @Builder + public PreSignedUrlResponseDto(String url, Map fields) { + this.url = url; + this.fields = fields; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ChatMessageMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ChatMessageMapper.java new file mode 100644 index 0000000..24f6cf9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ChatMessageMapper.java @@ -0,0 +1,26 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.ChatRoomSummaryUpdateEvent; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.enums.ChatRoomSummaryUpdateEventType; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ChatMessageMapper { + + public ChatRoomSummaryUpdateEvent toChatRoomSummaryUpdateEventByMessage( + ChatMessageDto chatMessageDto, + String receiverUuid + ) { + return ChatRoomSummaryUpdateEvent.builder() + .chatRoomUuid(chatMessageDto.getChatRoomUuid()) + .targetMemberUuids(List.of( + chatMessageDto.getSenderUuid(), + receiverUuid + )) + .eventType(ChatRoomSummaryUpdateEventType.MESSAGE_UPDATE.toString()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/PreSignedUrlMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/PreSignedUrlMapper.java new file mode 100644 index 0000000..1aeebc1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/PreSignedUrlMapper.java @@ -0,0 +1,31 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.PreSignedUrlDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.PreSignedUrlRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class PreSignedUrlMapper { + + public PreSignedUrlDto toPreSignedUrlDto(PreSignedUrlRequestDto preSignedUrlRequestDto, String randomUuid) { + + final String ext = preSignedUrlRequestDto.getContentType() + .substring(preSignedUrlRequestDto.getContentType().indexOf("/") + 1); + final String key = "chat/" + preSignedUrlRequestDto.getMemberUuid() + "/images/" + randomUuid + "." + ext; + + return PreSignedUrlDto.builder() + .key(key) + .contentType(preSignedUrlRequestDto.getContentType()) + .build(); + } + + public PreSignedUrlResponseDto toPreSignedUrlResponseDto(String url, Map fields) { + return PreSignedUrlResponseDto.builder() + .url(url) + .fields(fields) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ReadMessageMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ReadMessageMapper.java new file mode 100644 index 0000000..eab2cd5 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/mapper/ReadMessageMapper.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.mapper; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.ChatRoomSummaryUpdateEvent; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.enums.ChatRoomSummaryUpdateEventType; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ReadMessageMapper { + + public ChatRoomSummaryUpdateEvent toChatRoomSummaryUpdateEventByRead(ReadMessageRequestDto readMessageRequestDto) { + return ChatRoomSummaryUpdateEvent.builder() + .chatRoomUuid(readMessageRequestDto.getChatRoomUuid()) + .targetMemberUuids(List.of( + readMessageRequestDto.getMemberUuid() + )) + .eventType(ChatRoomSummaryUpdateEventType.READ_UPDATE.toString()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageQueryUseCase.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageQueryUseCase.java new file mode 100644 index 0000000..d6a02cd --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageQueryUseCase.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.in; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetMessagesRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetReadCheckPointRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; + +import java.util.List; + +public interface ChatMessageQueryUseCase { + + List getMessages(GetMessagesRequestDto getMessagesRequestDto); + GetReadCheckPointResponseDto getReadCheckPoint(GetReadCheckPointRequestDto getReadCheckPointRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageUseCase.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageUseCase.java new file mode 100644 index 0000000..7861824 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/ChatMessageUseCase.java @@ -0,0 +1,10 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.in; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; + +public interface ChatMessageUseCase { + + void sendMessage(SendMessageRequestDto sendMessageRequestDto); + void updateReadCheckPoint(ReadMessageRequestDto readMessageRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/PreSignedUrlUseCase.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/PreSignedUrlUseCase.java new file mode 100644 index 0000000..f92f04a --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/in/PreSignedUrlUseCase.java @@ -0,0 +1,9 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.in; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.PreSignedUrlRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; + +public interface PreSignedUrlUseCase { + + PreSignedUrlResponseDto generatePreSignedUrl(PreSignedUrlRequestDto preSignedUrlRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageQueryRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageQueryRepositoryPort.java new file mode 100644 index 0000000..a0c169b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageQueryRepositoryPort.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetMessagesRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetReadCheckPointRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; + +import java.util.List; + +public interface ChatMessageQueryRepositoryPort { + + List getMessages(GetMessagesRequestDto getMessagesRequestDto); + GetReadCheckPointResponseDto getReadCheckPoint(GetReadCheckPointRequestDto getReadCheckPointRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageRepositoryPort.java new file mode 100644 index 0000000..5f68101 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatMessageRepositoryPort.java @@ -0,0 +1,12 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; + + +public interface ChatMessageRepositoryPort { + + void processMessage(ChatMessageDto chatMessageDto); + GetMessagesResponseDto findByMessageUuid(String messageUuid); + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatReadCheckPointUpdaterPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatReadCheckPointUpdaterPort.java new file mode 100644 index 0000000..507a38c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ChatReadCheckPointUpdaterPort.java @@ -0,0 +1,8 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; + +public interface ChatReadCheckPointUpdaterPort { + + Boolean updateReadCheckPoint(ReadMessageRequestDto readMessageRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/GeneratePreSignedUrlPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/GeneratePreSignedUrlPort.java new file mode 100644 index 0000000..4673d41 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/GeneratePreSignedUrlPort.java @@ -0,0 +1,9 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.PreSignedUrlDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; + +public interface GeneratePreSignedUrlPort { + + PreSignedUrlResponseDto generatePreSignedUrl(PreSignedUrlDto requestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ImageKeyValidatorPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ImageKeyValidatorPort.java new file mode 100644 index 0000000..26a99ac --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/ImageKeyValidatorPort.java @@ -0,0 +1,8 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; + +public interface ImageKeyValidatorPort { + + void validateImageMessage(SendMessageRequestDto sendMessageRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/PublishChatMessagePort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/PublishChatMessagePort.java new file mode 100644 index 0000000..058d199 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/PublishChatMessagePort.java @@ -0,0 +1,8 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; + +public interface PublishChatMessagePort { + + Boolean publishChatMessage(SendMessageRequestDto sendMessageRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/SendMessageToClientPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/SendMessageToClientPort.java new file mode 100644 index 0000000..7f0d041 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/port/out/SendMessageToClientPort.java @@ -0,0 +1,8 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; + +public interface SendMessageToClientPort { + + void sendMessage(ReadMessageRequestDto readMessageRequestDto, String opponentUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageQueryService.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageQueryService.java new file mode 100644 index 0000000..e8c96ea --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageQueryService.java @@ -0,0 +1,29 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.service; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetMessagesRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.GetReadCheckPointRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetMessagesResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.GetReadCheckPointResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.ChatMessageQueryUseCase; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatMessageQueryRepositoryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatMessageQueryService implements ChatMessageQueryUseCase { + + private final ChatMessageQueryRepositoryPort chatMessageQueryRepositoryPort; + + @Override + public List getMessages(GetMessagesRequestDto getMessagesRequestDto) { + return chatMessageQueryRepositoryPort.getMessages(getMessagesRequestDto); + } + + @Override + public GetReadCheckPointResponseDto getReadCheckPoint(GetReadCheckPointRequestDto getReadCheckPointRequestDto) { + return chatMessageQueryRepositoryPort.getReadCheckPoint(getReadCheckPointRequestDto); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageService.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageService.java new file mode 100644 index 0000000..1b7377f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/ChatMessageService.java @@ -0,0 +1,72 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.service; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.SendMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.mapper.ReadMessageMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.ChatMessageUseCase; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ChatReadCheckPointUpdaterPort; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.ImageKeyValidatorPort; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.PublishChatMessagePort; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.SendMessageToClientPort; +import com.chalnakchalnak.chatservice.chatmessage.domain.MessageType; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberRepositoryPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomSummaryUpdaterPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.PublishChatRoomSummaryUpdatePort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +public class ChatMessageService implements ChatMessageUseCase { + + private final PublishChatMessagePort publishChatMessagePort; + private final ChatReadCheckPointUpdaterPort chatReadCheckPointUpdaterPort; + private final SendMessageToClientPort sendMessageToClientPort; + private final ChatRoomMemberRepositoryPort chatRoomMemberRepositoryPort; + private final ChatRoomSummaryUpdaterPort chatRoomSummaryUpdaterPort; + private final PublishChatRoomSummaryUpdatePort publishChatRoomSummaryUpdatePort; + private final ReadMessageMapper readMessageMapper; + private final ImageKeyValidatorPort imageKeyValidatorPort; + + @Override + public void sendMessage(SendMessageRequestDto sendMessageRequestDto) { + if (sendMessageRequestDto.getMessageType().equals(MessageType.IMAGE)) { + imageKeyValidatorPort.validateImageMessage(sendMessageRequestDto); + } + + final Boolean result = publishChatMessagePort.publishChatMessage(sendMessageRequestDto); + + if (!result) { + throw new BaseException(BaseResponseStatus.FAILED_PUBLISH_MESSAGE); + } + } + + //@Transactional + @Override + public void updateReadCheckPoint(ReadMessageRequestDto readMessageRequestDto) { + try { + final Boolean updated = chatReadCheckPointUpdaterPort.updateReadCheckPoint(readMessageRequestDto); + if (!updated) return; + + final String opponentUuid = chatRoomMemberRepositoryPort.findOpponentUuid( + readMessageRequestDto.getChatRoomUuid(), readMessageRequestDto.getMemberUuid() + ); + + chatRoomSummaryUpdaterPort.updateOnRead(readMessageRequestDto); + + sendMessageToClientPort.sendMessage(readMessageRequestDto, opponentUuid); + + publishChatRoomSummaryUpdatePort.publishChatRoomSummaryUpdate( + readMessageMapper.toChatRoomSummaryUpdateEventByRead(readMessageRequestDto) + ); + } + catch (BaseException e) { + throw e; + } catch (Exception e) { + throw new BaseException(BaseResponseStatus.FAILED_UPDATE_READ_CHECK_POINT); + } + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/PreSignedUrlService.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/PreSignedUrlService.java new file mode 100644 index 0000000..2b7d128 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/application/service/PreSignedUrlService.java @@ -0,0 +1,28 @@ +package com.chalnakchalnak.chatservice.chatmessage.application.service; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.PreSignedUrlRequestDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.out.PreSignedUrlResponseDto; +import com.chalnakchalnak.chatservice.chatmessage.application.mapper.PreSignedUrlMapper; +import com.chalnakchalnak.chatservice.chatmessage.application.port.in.PreSignedUrlUseCase; +import com.chalnakchalnak.chatservice.chatmessage.application.port.out.GeneratePreSignedUrlPort; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.GenerateUuidPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PreSignedUrlService implements PreSignedUrlUseCase { + + private final GeneratePreSignedUrlPort generatePreSignedUrlPort; + private final PreSignedUrlMapper preSignedUrlMapper; + private final GenerateUuidPort generateUuidPort; + + public PreSignedUrlResponseDto generatePreSignedUrl(PreSignedUrlRequestDto preSignedUrlRequestDto) { + return generatePreSignedUrlPort.generatePreSignedUrl( + preSignedUrlMapper.toPreSignedUrlDto(preSignedUrlRequestDto, generateUuidPort.generateUuid()) + ); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/ChatMessage.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/ChatMessage.java new file mode 100644 index 0000000..04b3d62 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/ChatMessage.java @@ -0,0 +1,25 @@ +package com.chalnakchalnak.chatservice.chatmessage.domain; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ChatMessage { + + private String roomUuid; + private String senderUuid; + private String message; + private MessageType messageType; + private LocalDateTime sentAt; + + @Builder + public ChatMessage(String roomUuid, String senderUuid, String message, MessageType messageType, LocalDateTime sentAt) { + this.roomUuid = roomUuid; + this.senderUuid = senderUuid; + this.message = message; + this.messageType = messageType; + this.sentAt = sentAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/MessageType.java b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/MessageType.java new file mode 100644 index 0000000..cb0f5c4 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatmessage/domain/MessageType.java @@ -0,0 +1,33 @@ +package com.chalnakchalnak.chatservice.chatmessage.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MessageType { + + TEXT("TEXT", "텍스트 메시지"), + IMAGE("IMAGE", "이미지"), + REPLY("REPLY", "답장 메시지"),; + + private final String code; + private final String label; + + @JsonCreator + public static MessageType from(String value) { + for (MessageType type : values()) { + if (type.code.equalsIgnoreCase(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown message type: " + value); + } + + @JsonValue + public String getCode() { + return code; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomSummaryVoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomSummaryVoMapper.java new file mode 100644 index 0000000..acdc52d --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomSummaryVoMapper.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.mapper; + +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out.GetChatRoomSummaryResponseVo; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatRoomSummaryVoMapper { + + public GetChatRoomSummaryResponseVo toGetChatRoomSummaryResponseVo(GetChatRoomSummaryResponseDto chatRoomSummaryResponseDto) { + return GetChatRoomSummaryResponseVo.builder() + .chatRoomUuid(chatRoomSummaryResponseDto.getChatRoomUuid()) + .opponentUuid(chatRoomSummaryResponseDto.getOpponentUuid()) + .lastMessage(chatRoomSummaryResponseDto.getLastMessage()) + .lastMessageSentAt(chatRoomSummaryResponseDto.getLastMessageSentAt()) + .unreadCount(chatRoomSummaryResponseDto.getUnreadCount()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomVoMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomVoMapper.java new file mode 100644 index 0000000..c21d1a8 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/mapper/ChatRoomVoMapper.java @@ -0,0 +1,50 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.mapper; + +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.CreateChatRoomRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.GetChatRoomInfoRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.GetChatRoomListByPostRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out.GetChatRoomListByPostResponseVo; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.CreateChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomListByPostRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatRoomVoMapper { + + public CreateChatRoomRequestDto toCreateChatRoomDto(CreateChatRoomRequestVo createChatRoomRequestVo) { + return CreateChatRoomRequestDto.builder() + .postUuid(createChatRoomRequestVo.getPostUuid()) + .sellerUuid(createChatRoomRequestVo.getSellerUuid()) + .buyerUuid(createChatRoomRequestVo.getBuyerUuid()) + .chatRoomType(createChatRoomRequestVo.getChatRoomType()) + .build(); + } + + public ExitChatRoomRequestDto toExitChatRooRequestDto(String memberUuid, String chatRoomUuid) { + return ExitChatRoomRequestDto.builder() + .memberUuid(memberUuid) + .chatRoomUuid(chatRoomUuid) + .build(); + } + + public GetChatRoomInfoRequestDto toGetChatRoomInfoDto(GetChatRoomInfoRequestVo getChatRoomInfoRequestVo) { + return GetChatRoomInfoRequestDto.builder() + .chatRoomUuid(getChatRoomInfoRequestVo.getChatRoomUuid()) + .build(); + } + + public GetChatRoomListByPostRequestDto toGetChatRoomListByPostRequestDto(GetChatRoomListByPostRequestVo getChatRoomListByPostRequestVo) { + return GetChatRoomListByPostRequestDto.builder() + .postUuid(getChatRoomListByPostRequestVo.getPostUuid()) + .build(); + } + + public GetChatRoomListByPostResponseVo toGetChatRoomListByPostResponseVo(GetChatRoomListByPostResponseDto getChatRoomListByPostResponseDto) { + return GetChatRoomListByPostResponseVo.builder() + .chatRoomUuid(getChatRoomListByPostResponseDto.getChatRoomUuid()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomController.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomController.java new file mode 100644 index 0000000..aaf6934 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomController.java @@ -0,0 +1,89 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.presentation; + +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.mapper.ChatRoomVoMapper; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.CreateChatRoomRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.GetChatRoomInfoRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in.GetChatRoomListByPostRequestVo; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out.GetChatRoomListByPostResponseVo; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomInfoResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.in.ChatRoomUseCase; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/chatroom") +@RequiredArgsConstructor +public class ChatRoomController { + + private final ChatRoomUseCase chatRoomUseCase; + private final ChatRoomVoMapper chatRoomVoMapper; + + + @Operation( + summary = "Create Private ChatRoom API", + description = "1대1 채팅방 생성
chatRoomType(enum) : AUCTION_PRIVATE(경매 1대1 채팅방), NORMAL_PRIVATE(일반 1대1 채팅방)", + tags = {"chatroom"} + ) + @PostMapping("/private") + public String createPrivateChatRoom( + @RequestBody @Valid CreateChatRoomRequestVo createChatRoomRequestVo + ) { + + return chatRoomUseCase.createPrivateChatRoom( + chatRoomVoMapper.toCreateChatRoomDto(createChatRoomRequestVo) + ); + } + + @Operation( + summary = "Exit ChatRoom API", + description = "채팅방 나가기 API
채팅방에서 나가도 채팅방이 삭제되지는 않음
나간 멤버의 채팅방 리스트와 이전까지의 메시지 조회가 불가하도록 적용됩니다.", + tags = {"chatroom"} + ) + @PostMapping("/exit/{chatRoomUuid}") + public void exitChatRoom( + @RequestHeader("X-Member-Uuid") String memberUuid, + @PathVariable String chatRoomUuid + ) { + + chatRoomUseCase.exitChatRoom( + chatRoomVoMapper.toExitChatRooRequestDto(memberUuid, chatRoomUuid) + ); + } + + @GetMapping("/info/{chatRoomUuid}") + @Operation( + summary = "Get ChatRoom Info API", + description = "채팅방 정보 조회 API", + tags = {"chatroom"} + ) + public GetChatRoomInfoResponseDto getChatRoomInfo( + @ModelAttribute @Valid GetChatRoomInfoRequestVo getChatRoomInfoRequestVo + ) { + + return chatRoomUseCase.getChatRoomInfo( + chatRoomVoMapper.toGetChatRoomInfoDto(getChatRoomInfoRequestVo) + ); + } + + @GetMapping("/{postUuid}") + @Operation( + summary = "Get ChatRoom List By Post API", + description = "특정 게시글에 대한 채팅방 정보 조회 API
해당 게시글에 대한 1대1 채팅방이 존재하지 않는 경우, 빈 값 반환", + tags = {"chatroom"} + ) + public List getChatRoomListByPost( + @ModelAttribute @Valid GetChatRoomListByPostRequestVo getChatRoomListByPostRequestVo + ) { + + return chatRoomUseCase.getChatRoomListByPost( + chatRoomVoMapper.toGetChatRoomListByPostRequestDto(getChatRoomListByPostRequestVo)) + .stream() + .map(chatRoomVoMapper::toGetChatRoomListByPostResponseVo) + .toList(); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomSummaryController.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomSummaryController.java new file mode 100644 index 0000000..dc31423 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/presentation/ChatRoomSummaryController.java @@ -0,0 +1,36 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.presentation; + +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.mapper.ChatRoomSummaryVoMapper; +import com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out.GetChatRoomSummaryResponseVo; +import com.chalnakchalnak.chatservice.chatroom.application.port.in.ChatRoomSummaryUseCase; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/chatroom-summary") +@RequiredArgsConstructor +public class ChatRoomSummaryController { + + private final ChatRoomSummaryUseCase chatRoomSummaryUseCase; + private final ChatRoomSummaryVoMapper chatRoomSummaryVoMapper; + + @Operation(summary = "Get My ChatRoomSummaryList", + description = "내가 참여하고 있는 채팅방의 요약 정보를 조회합니다.", tags = {"chatroom-summary"}) + @GetMapping + public List getMyChatRoomList( + @RequestHeader("X-Member-Uuid") String memberUuid + ) { + return chatRoomSummaryUseCase.getMyChatRoomList(memberUuid) + .stream() + .map(chatRoomSummaryVoMapper::toGetChatRoomSummaryResponseVo) + .toList(); + } + + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/CreateChatRoomRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/CreateChatRoomRequestVo.java new file mode 100644 index 0000000..1ef610e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/CreateChatRoomRequestVo.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in; + +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class CreateChatRoomRequestVo { + + @NotBlank(message = "Post UUID는 필수 값입니다") + private String postUuid; + + @NotBlank(message = "Seller UUID는 필수 값입니다") + private String sellerUuid; + + @NotBlank(message = "Buyer UUID는 필수 값입니다") + private String buyerUuid; + + @NotNull(message = "ChatRoomType은 필수 값입니다") + private ChatRoomType chatRoomType; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomInfoRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomInfoRequestVo.java new file mode 100644 index 0000000..1f9c2d4 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomInfoRequestVo.java @@ -0,0 +1,13 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GetChatRoomInfoRequestVo { + + @NotBlank(message = "chatRoomUuid는 필수 값 입니다.") + private String chatRoomUuid; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomListByPostRequestVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomListByPostRequestVo.java new file mode 100644 index 0000000..957d4a1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/in/GetChatRoomListByPostRequestVo.java @@ -0,0 +1,13 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.in; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GetChatRoomListByPostRequestVo { + + @NotBlank(message = "postUuid는 필수 값입니다.") + private String postUuid; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomListByPostResponseVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomListByPostResponseVo.java new file mode 100644 index 0000000..75824aa --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomListByPostResponseVo.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetChatRoomListByPostResponseVo { + + private String chatRoomUuid; + + @Builder + public GetChatRoomListByPostResponseVo(String chatRoomUuid) { + this.chatRoomUuid = chatRoomUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomSummaryResponseVo.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomSummaryResponseVo.java new file mode 100644 index 0000000..88cc0a9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/in/web/vo/out/GetChatRoomSummaryResponseVo.java @@ -0,0 +1,31 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.in.web.vo.out; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class GetChatRoomSummaryResponseVo { + + private String chatRoomUuid; + private String opponentUuid; + private String lastMessage; + private LocalDateTime lastMessageSentAt; + private int unreadCount; + + @Builder + public GetChatRoomSummaryResponseVo( + String chatRoomUuid, + String opponentUuid, + String lastMessage, + LocalDateTime lastMessageSentAt, + int unreadCount + ) { + this.chatRoomUuid = chatRoomUuid; + this.opponentUuid = opponentUuid; + this.lastMessage = lastMessage; + this.lastMessageSentAt = lastMessageSentAt; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/KafkaChatRoomSummaryUpdateProducer.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/KafkaChatRoomSummaryUpdateProducer.java new file mode 100644 index 0000000..7b089a2 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/KafkaChatRoomSummaryUpdateProducer.java @@ -0,0 +1,44 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.ChatRoomSummaryUpdateEvent; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.PublishChatRoomSummaryUpdatePort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KafkaChatRoomSummaryUpdateProducer implements PublishChatRoomSummaryUpdatePort { + + private final KafkaTemplate kafkaTemplate; + private final ObjectMapper objectMapper; + private static final String TOPIC = "chat-service.chatroom-summary.update"; + + @Override + public void publishChatRoomSummaryUpdate(ChatRoomSummaryUpdateEvent chatRoomSummaryUpdateEvent) { + try { + final String payload = toJson(chatRoomSummaryUpdateEvent); + + kafkaTemplate.send(TOPIC, chatRoomSummaryUpdateEvent.getChatRoomUuid(), payload); + log.info("Kafka 채팅방 요약 업데이트 이벤트 발행: {}", payload); + + } catch (Exception e) { + log.error("Kafka 채팅방 요약 업데이트 이벤트 발행 실패", e); + throw new BaseException(BaseResponseStatus.FAILED_PUBLISH_CHAT_ROOM_SUMMARY_UPDATE); + } + } + + private String toJson(ChatRoomSummaryUpdateEvent chatRoomSummaryUpdateEvent) { + try { + return objectMapper.writeValueAsString(chatRoomSummaryUpdateEvent); + } catch (JsonProcessingException e) { + throw new BaseException(BaseResponseStatus.FAILED_SERIALIZE_MESSAGE); + } + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/ChatRoomSummaryUpdateEvent.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/ChatRoomSummaryUpdateEvent.java new file mode 100644 index 0000000..a9b6bb3 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/ChatRoomSummaryUpdateEvent.java @@ -0,0 +1,25 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ChatRoomSummaryUpdateEvent { + + private String chatRoomUuid; + private List targetMemberUuids; + private String eventType; + + @Builder + public ChatRoomSummaryUpdateEvent( + String chatRoomUuid, + List targetMemberUuids, + String eventType + ) { + this.chatRoomUuid = chatRoomUuid; + this.targetMemberUuids = targetMemberUuids; + this.eventType = eventType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/enums/ChatRoomSummaryUpdateEventType.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/enums/ChatRoomSummaryUpdateEventType.java new file mode 100644 index 0000000..9d8ff3c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/kafka/payload/enums/ChatRoomSummaryUpdateEventType.java @@ -0,0 +1,16 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ChatRoomSummaryUpdateEventType { + MESSAGE_UPDATE("메시지 업데이트"), + READ_UPDATE("읽음 업데이트"), + ROOM_EXIT("채팅방 퇴장 업데이트"); + + @JsonValue + private final String label; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomMemberExitDocument.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomMemberExitDocument.java new file mode 100644 index 0000000..9e172cc --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomMemberExitDocument.java @@ -0,0 +1,30 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "chat_room_exit") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatRoomMemberExitDocument { + + @Id + private String id; + + private String chatRoomUuid; + private String memberUuid; + private LocalDateTime exitedAt; + + @Builder + public ChatRoomMemberExitDocument(String chatRoomUuid, String memberUuid, LocalDateTime exitedAt) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.exitedAt = exitedAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomSummaryDocument.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomSummaryDocument.java new file mode 100644 index 0000000..6167f81 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/document/ChatRoomSummaryDocument.java @@ -0,0 +1,49 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Document(collection = "chat_room_summary") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatRoomSummaryDocument { + + @Id + private String id; + + private String chatRoomUuid; + private String memberUuid; + private String opponentUuid; + private String lastMessage; + private LocalDateTime lastMessageSentAt; + private String messageType; + private int unreadCount; + private LocalDateTime updatedAt; + + @Builder + public ChatRoomSummaryDocument( + String chatRoomUuid, + String memberUuid, + String opponentUuid, + String lastMessage, + LocalDateTime lastMessageSentAt, + String messageType, + int unreadCount, + LocalDateTime updatedAt + ) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.opponentUuid = opponentUuid; + this.lastMessage = lastMessage; + this.lastMessageSentAt = lastMessageSentAt; + this.messageType = messageType; + this.unreadCount = unreadCount; + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomMemberExitDocumentMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomMemberExitDocumentMapper.java new file mode 100644 index 0000000..67211a6 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomMemberExitDocumentMapper.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.mapper; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document.ChatRoomMemberExitDocument; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberExitDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatRoomMemberExitDocumentMapper { + + public ChatRoomMemberExitDto toChatRoomMemberExitDto(ChatRoomMemberExitDocument chatRoomMemberExitDocument) { + return ChatRoomMemberExitDto.builder() + .chatRoomUuid(chatRoomMemberExitDocument.getChatRoomUuid()) + .memberUuid(chatRoomMemberExitDocument.getMemberUuid()) + .exitedAt(chatRoomMemberExitDocument.getExitedAt()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomSummaryDocumentMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomSummaryDocumentMapper.java new file mode 100644 index 0000000..7a4aefb --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/mapper/ChatRoomSummaryDocumentMapper.java @@ -0,0 +1,20 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.mapper; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document.ChatRoomSummaryDocument; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; +import org.springframework.stereotype.Component; + +@Component +public class ChatRoomSummaryDocumentMapper { + + public GetChatRoomSummaryResponseDto toGetChatRoomSummaryResponseDto(ChatRoomSummaryDocument chatRoomSummaryDocument) { + return GetChatRoomSummaryResponseDto.builder() + .chatRoomUuid(chatRoomSummaryDocument.getChatRoomUuid()) + .opponentUuid(chatRoomSummaryDocument.getOpponentUuid()) + .lastMessage(chatRoomSummaryDocument.getLastMessage()) + .lastMessageSentAt(chatRoomSummaryDocument.getLastMessageSentAt()) + .messageType(chatRoomSummaryDocument.getMessageType()) + .unreadCount(chatRoomSummaryDocument.getUnreadCount()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitMongoRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitMongoRepository.java new file mode 100644 index 0000000..773c859 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitMongoRepository.java @@ -0,0 +1,11 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document.ChatRoomMemberExitDocument; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +public interface ChatRoomMemberExitMongoRepository extends MongoRepository { + + Optional findByChatRoomUuidAndMemberUuid(String chatRoomUuid, String memberUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitRepository.java new file mode 100644 index 0000000..f6ee68f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitRepository.java @@ -0,0 +1,24 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.mapper.ChatRoomMemberExitDocumentMapper; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberExitDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberExitRepositoryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class ChatRoomMemberExitRepository implements ChatRoomMemberExitRepositoryPort { + + private final ChatRoomMemberExitMongoRepository chatRoomMemberExitMongoRepository; + private final ChatRoomMemberExitDocumentMapper chatRoomMemberExitDocumentMapper; + + @Override + public Optional findByChatRoomUuidAndMemberUuid(String chatRoomUuid, String memberUuid) { + return chatRoomMemberExitMongoRepository.findByChatRoomUuidAndMemberUuid(chatRoomUuid, memberUuid) + .map(chatRoomMemberExitDocumentMapper::toChatRoomMemberExitDto); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitUpdater.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitUpdater.java new file mode 100644 index 0000000..ab6ea9a --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomMemberExitUpdater.java @@ -0,0 +1,35 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberExitUpdaterPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Repository +@RequiredArgsConstructor +public class ChatRoomMemberExitUpdater implements ChatRoomMemberExitUpdaterPort { + + private final MongoTemplate mongoTemplate; + + @Override + public void updateExitedAt(ExitChatRoomRequestDto exitChatRoomRequestDto) { + Query query = new Query( + Criteria.where("chatRoomUuid").is(exitChatRoomRequestDto.getChatRoomUuid()) + .and("memberUuid").is(exitChatRoomRequestDto.getMemberUuid()) + ); + + Update update = new Update() + .set("chatRoomUuid", exitChatRoomRequestDto.getChatRoomUuid()) + .set("memberUuid", exitChatRoomRequestDto.getMemberUuid()) + .set("exitedAt", LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + + mongoTemplate.upsert(query, update, "chat_room_exit"); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryMongoRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryMongoRepository.java new file mode 100644 index 0000000..43fb27e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryMongoRepository.java @@ -0,0 +1,13 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.document.ChatRoomSummaryDocument; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomSummaryMongoRepository extends MongoRepository { + + Optional> findAllByMemberUuidOrderByLastMessageSentAtDesc (String memberUuid); + void deleteByChatRoomUuidAndMemberUuid (String chatRoomUuid, String memberUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryRepository.java new file mode 100644 index 0000000..05045ca --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryRepository.java @@ -0,0 +1,37 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.mapper.ChatRoomSummaryDocumentMapper; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomSummaryRepositoryPort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ChatRoomSummaryRepository implements ChatRoomSummaryRepositoryPort { + + private final ChatRoomSummaryMongoRepository chatRoomSummaryMongoRepository; + private final ChatRoomSummaryDocumentMapper chatRoomSummaryDocumentMapper; + + @Override + public List getMyChatRoomList(String memberUuid) { + return chatRoomSummaryMongoRepository.findAllByMemberUuidOrderByLastMessageSentAtDesc(memberUuid) + .orElseThrow(() -> new BaseException(BaseResponseStatus.MEMBER_CHAT_ROOM_NOT_FOUND)) + .stream() + .map(chatRoomSummaryDocumentMapper::toGetChatRoomSummaryResponseDto) + .toList(); + } + + @Override + public void deleteChatRoomSummary(ExitChatRoomRequestDto exitChatRoomRequestDto) { + chatRoomSummaryMongoRepository.deleteByChatRoomUuidAndMemberUuid( + exitChatRoomRequestDto.getChatRoomUuid(), + exitChatRoomRequestDto.getMemberUuid() + ); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryUpdater.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryUpdater.java new file mode 100644 index 0000000..66edf24 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mongo/repository/ChatRoomSummaryUpdater.java @@ -0,0 +1,75 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mongo.repository; + +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomSummaryUpdaterPort; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Service +@RequiredArgsConstructor +public class ChatRoomSummaryUpdater implements ChatRoomSummaryUpdaterPort { + + private final MongoTemplate mongoTemplate; + + /** + * 메시지 수신 시 summary update + */ + @Override + public void updateOnMessage(ChatMessageDto chatMessageDto, String receiverUuid) { + // 1. 수신자 summary: unreadCount 증가 + Query receiverQuery = Query.query(Criteria.where("chatRoomUuid").is(chatMessageDto.getChatRoomUuid()) + .and("memberUuid").is(receiverUuid)); + + Update receiverUpdate = new Update() + .set("chatRoomUuid", chatMessageDto.getChatRoomUuid()) + .set("memberUuid", receiverUuid) + .set("opponentUuid", chatMessageDto.getSenderUuid()) + .set("lastMessage", chatMessageDto.getMessage()) + .set("lastMessageSentAt", chatMessageDto.getSentAt()) + .set("messageType", chatMessageDto.getMessageType()) + .set("updatedAt", LocalDateTime.now(ZoneId.of("Asia/Seoul"))) + .inc("unreadCount", 1); + + mongoTemplate.upsert(receiverQuery, receiverUpdate, "chat_room_summary"); + + // 발신자 summary upsert + Query senderQuery = Query.query(Criteria.where("chatRoomUuid").is(chatMessageDto.getChatRoomUuid()) + .and("memberUuid").is(chatMessageDto.getSenderUuid())); + + Update senderUpdate = new Update() + .set("chatRoomUuid", chatMessageDto.getChatRoomUuid()) + .set("memberUuid", chatMessageDto.getSenderUuid()) + .set("opponentUuid", receiverUuid) + .set("lastMessage", chatMessageDto.getMessage()) + .set("lastMessageSentAt", chatMessageDto.getSentAt()) + .set("messageType", chatMessageDto.getMessageType()) + .set("updatedAt", LocalDateTime.now(ZoneId.of("Asia/Seoul"))) + .set("unreadCount", 0); + + mongoTemplate.upsert(senderQuery, senderUpdate, "chat_room_summary"); + } + + /** + * 특정 메시지 읽음 처리 시 이떄까지 발송된 모든 메시지를 읽었음으로 판단 + */ + @Override + public void updateOnRead(ReadMessageRequestDto readMessageRequestDto) { + Query query = Query.query(Criteria.where("chatRoomUuid").is(readMessageRequestDto.getChatRoomUuid()) + .and("memberUuid").is(readMessageRequestDto.getMemberUuid())); + + Update update = new Update() + .set("unreadCount", 0) + .set("updatedAt", LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + + mongoTemplate.updateFirst(query, update, "chat_room_summary"); + } +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/common/BaseEntity.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/common/BaseEntity.java new file mode 100644 index 0000000..c298d5c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/common/BaseEntity.java @@ -0,0 +1,26 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) +@Getter +@MappedSuperclass +public abstract class BaseEntity { + + @CreatedDate + @Column(name="created_at", updatable = false, columnDefinition = "DATETIME(0)") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name="updated_at", columnDefinition = "DATETIME(0)") + private LocalDateTime updatedAt; + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomEntity.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomEntity.java new file mode 100644 index 0000000..5914462 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomEntity.java @@ -0,0 +1,40 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.common.BaseEntity; +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomType; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "chat_room") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatRoomEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + private Long id; + + @Column(name = "chat_room_uuid", nullable = false, unique = true, length = 50) + private String chatRoomUuid; + + @Column(name = "post_uuid", nullable = false, length = 50) + private String postUuid; + + @Enumerated(EnumType.STRING) + @Column(name = "chat_room_type", nullable = false, length = 20) + private ChatRoomType chatRoomType; + + @Builder + public ChatRoomEntity(Long id, String chatRoomUuid, String postUuid, ChatRoomType chatRoomType) { + this.id = id; + this.chatRoomUuid = chatRoomUuid; + this.postUuid = postUuid; + this.chatRoomType = chatRoomType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomMemberEntity.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomMemberEntity.java new file mode 100644 index 0000000..10ed78d --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/entity/ChatRoomMemberEntity.java @@ -0,0 +1,40 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.common.BaseEntity; +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomMemberRole; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "chat_room_member") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ChatRoomMemberEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + private Long id; + + @Column(name = "chat_room_uuid", nullable = false, length = 50) + private String chatRoomUuid; + + @Column(name = "member_uuid", nullable = false, length = 50) + private String memberUuid; + + @Enumerated(EnumType.STRING) + @Column(name = "role", nullable = false, length = 20) + private ChatRoomMemberRole role; + + @Builder + public ChatRoomMemberEntity(Long id, String chatRoomUuid, String memberUuid, ChatRoomMemberRole role) { + this.id = id; + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.role = role; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/mapper/ChatRoomEntityMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/mapper/ChatRoomEntityMapper.java new file mode 100644 index 0000000..34a155a --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/mapper/ChatRoomEntityMapper.java @@ -0,0 +1,61 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.mapper; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomMemberEntity; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomEntity; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomMemberDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomMemberRole; +import org.springframework.stereotype.Component; + +@Component +public class ChatRoomEntityMapper { + + public ChatRoomEntity toChatRoomEntity(CreateChatRoomDto createChatRoomDto) { + return ChatRoomEntity.builder() + .chatRoomUuid(createChatRoomDto.getChatRoomUuid()) + .postUuid(createChatRoomDto.getPostUuid()) + .chatRoomType(createChatRoomDto.getChatRoomType()) + .build(); + } + + public ChatRoomMemberEntity toChatRoomMemberEntityBySeller (CreateChatRoomMemberDto createChatRoomMemberDto) { + return ChatRoomMemberEntity.builder() + .chatRoomUuid(createChatRoomMemberDto.getChatRoomUuid()) + .memberUuid(createChatRoomMemberDto.getSellerUuid()) + .role(ChatRoomMemberRole.SELLER) + + .build(); + } + + public ChatRoomMemberEntity toChatRoomMemberEntityByBuyer (CreateChatRoomMemberDto createChatRoomMemberDto) { + return ChatRoomMemberEntity.builder() + .chatRoomUuid(createChatRoomMemberDto.getChatRoomUuid()) + .memberUuid(createChatRoomMemberDto.getBuyerUuid()) + .role(ChatRoomMemberRole.BUYER) + .build(); + } + + public ChatRoomInfoDto toChatRoomInfoDto(ChatRoomEntity chatRoomEntity) { + return ChatRoomInfoDto.builder() + .chatRoomUuid(chatRoomEntity.getChatRoomUuid()) + .postUuid(chatRoomEntity.getPostUuid()) + .chatRoomType(chatRoomEntity.getChatRoomType().toString()) + .build(); + } + + public ChatRoomMemberInfoDto toChatRoomMemberInfoDto(ChatRoomMemberEntity chatRoomMemberEntity) { + return ChatRoomMemberInfoDto.builder() + .memberUuid(chatRoomMemberEntity.getMemberUuid()) + .role(chatRoomMemberEntity.getRole().toString()) + .build(); + } + + public GetChatRoomListByPostResponseDto toGetChatRoomListByPostResponseDto(ChatRoomEntity chatRoomEntity) { + return GetChatRoomListByPostResponseDto.builder() + .chatRoomUuid(chatRoomEntity.getChatRoomUuid()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomJpaRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomJpaRepository.java new file mode 100644 index 0000000..657d5e9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomJpaRepository.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomJpaRepository extends JpaRepository { + Optional findByChatRoomUuid(String chatRoomUuid); + Boolean existsByChatRoomUuid(String chatRoomUuid); + Optional> findByPostUuid(String postUuid); +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberJpaRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberJpaRepository.java new file mode 100644 index 0000000..892770b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberJpaRepository.java @@ -0,0 +1,28 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomMemberEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomMemberJpaRepository extends JpaRepository { + + @Query(""" + SELECT r.id + FROM ChatRoomEntity r + JOIN ChatRoomMemberEntity m ON r.chatRoomUuid = m.chatRoomUuid + WHERE r.postUuid = :postUuid + AND m.memberUuid = :buyerUuid + AND m.role = 'BUYER' + """) + Optional findPrivateRoomId(@Param("postUuid") String postUuid, + @Param("buyerUuid") String buyerUuid); + + Boolean existsByChatRoomUuidAndMemberUuid(String chatRoomUuid, String memberUuid); + + Optional> findByChatRoomUuid(String chatRoomUuid); +} + diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberRepository.java new file mode 100644 index 0000000..fbe2a1b --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomMemberRepository.java @@ -0,0 +1,56 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomMemberEntity; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.mapper.ChatRoomEntityMapper; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomMemberDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomMemberRepositoryPort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ChatRoomMemberRepository implements ChatRoomMemberRepositoryPort { + + private final ChatRoomMemberJpaRepository chatRoomMemberJpaRepository; + private final ChatRoomEntityMapper chatRoomEntityMapper; + + @Override + public void saveChatRoomMembers(CreateChatRoomMemberDto createChatRoomMemberDto) { + final ChatRoomMemberEntity seller = chatRoomEntityMapper.toChatRoomMemberEntityBySeller(createChatRoomMemberDto); + final ChatRoomMemberEntity buyer = chatRoomEntityMapper.toChatRoomMemberEntityByBuyer(createChatRoomMemberDto); + + chatRoomMemberJpaRepository.saveAll(List.of(seller, buyer)); + } + + @Override + public Optional findPrivateChatRoomUuid(String postUuid, String buyerUuid) { + return chatRoomMemberJpaRepository.findPrivateRoomId(postUuid, buyerUuid); + } + + @Override + public String findOpponentUuid(String chatRoomUuid, String myMemberUuid) { + List members = chatRoomMemberJpaRepository.findByChatRoomUuid(chatRoomUuid) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_NOT_FOUND)); + + return members.stream() + .filter(member -> !member.getMemberUuid().equals(myMemberUuid)) + .findFirst() + .map(ChatRoomMemberEntity::getMemberUuid) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_MEMBER_NOT_FOUND)); + } + + @Override + public Optional> getChatRoomMembers(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto) { + return chatRoomMemberJpaRepository.findByChatRoomUuid(getChatRoomInfoRequestDto.getChatRoomUuid()) + .map(members -> members.stream() + .map(chatRoomEntityMapper::toChatRoomMemberInfoDto) + .toList()); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomRepository.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomRepository.java new file mode 100644 index 0000000..7476c06 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/repository/ChatRoomRepository.java @@ -0,0 +1,53 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.entity.ChatRoomEntity; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.mapper.ChatRoomEntityMapper; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomListByPostRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomRepositoryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ChatRoomRepository implements ChatRoomRepositoryPort { + + private final ChatRoomJpaRepository chatRoomJpaRepository; + private final ChatRoomEntityMapper chatRoomEntityMapper; + + @Override + public String createChatRoom(CreateChatRoomDto createChatRoomDto) { + final ChatRoomEntity chatRoom = chatRoomEntityMapper.toChatRoomEntity(createChatRoomDto); + final ChatRoomEntity savedRoom = chatRoomJpaRepository.save(chatRoom); + + return savedRoom.getChatRoomUuid(); + } + + @Override + public Optional findRoomUuidById(Long roomId) { + return chatRoomJpaRepository.findById(roomId).map(ChatRoomEntity::getChatRoomUuid); + } + + @Override + public Optional getChatRoomInfo(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto) { + return chatRoomJpaRepository.findByChatRoomUuid(getChatRoomInfoRequestDto.getChatRoomUuid()) + .map(chatRoomEntityMapper::toChatRoomInfoDto); + } + + @Override + public Optional> getChatRoomListByPost(GetChatRoomListByPostRequestDto getChatRoomListByPostRequestDto) { + return chatRoomJpaRepository.findByPostUuid(getChatRoomListByPostRequestDto.getPostUuid()) + .map(entity -> + entity.stream() + .map(chatRoomEntityMapper::toGetChatRoomListByPostResponseDto) + .toList() + ); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/validator/ChatRoomValidator.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/validator/ChatRoomValidator.java new file mode 100644 index 0000000..062b97c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/adpater/out/mysql/validator/ChatRoomValidator.java @@ -0,0 +1,27 @@ +package com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.validator; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository.ChatRoomJpaRepository; +import com.chalnakchalnak.chatservice.chatroom.adpater.out.mysql.repository.ChatRoomMemberJpaRepository; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.validator.ChatRoomValidatorPort; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ChatRoomValidator implements ChatRoomValidatorPort { + + private final ChatRoomJpaRepository chatRoomJpaRepository; + private final ChatRoomMemberJpaRepository chatRoomMemberJpaRepository; + + @Override + public void memberAccessed(String chatRoomUuid, String memberUuid) { + if (!chatRoomJpaRepository.existsByChatRoomUuid(chatRoomUuid)) { + throw new BaseException(BaseResponseStatus.CHAT_ROOM_NOT_FOUND); + } + if (!chatRoomMemberJpaRepository.existsByChatRoomUuidAndMemberUuid(chatRoomUuid, memberUuid)) { + throw new BaseException(BaseResponseStatus.CHAT_ROOM_MEMBER_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomInfoDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomInfoDto.java new file mode 100644 index 0000000..f7fbbdb --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomInfoDto.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ChatRoomInfoDto { + + private String chatRoomUuid; + private String postUuid; + private String chatRoomType; + + @Builder + public ChatRoomInfoDto(String chatRoomUuid, String postUuid, String chatRoomType) { + this.chatRoomUuid = chatRoomUuid; + this.postUuid = postUuid; + this.chatRoomType = chatRoomType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberExitDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberExitDto.java new file mode 100644 index 0000000..42926bc --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberExitDto.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ChatRoomMemberExitDto { + + private String chatRoomUuid; + private String memberUuid; + private LocalDateTime exitedAt; + + @Builder + public ChatRoomMemberExitDto(String chatRoomUuid, String memberUuid, LocalDateTime exitedAt) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.exitedAt = exitedAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberInfoDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberInfoDto.java new file mode 100644 index 0000000..d425d22 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/ChatRoomMemberInfoDto.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ChatRoomMemberInfoDto { + + private String memberUuid; + private String role; + + @Builder + public ChatRoomMemberInfoDto(String memberUuid, String role) { + this.memberUuid = memberUuid; + this.role = role; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomDto.java new file mode 100644 index 0000000..f2b9765 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomDto.java @@ -0,0 +1,20 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto; + +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomType; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CreateChatRoomDto { + + private String chatRoomUuid; + private String postUuid; + private ChatRoomType chatRoomType; + + @Builder + public CreateChatRoomDto(String chatRoomUuid, String postUuid, ChatRoomType chatRoomType) { + this.chatRoomUuid = chatRoomUuid; + this.postUuid = postUuid; + this.chatRoomType = chatRoomType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomMemberDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomMemberDto.java new file mode 100644 index 0000000..fdcdf7f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/CreateChatRoomMemberDto.java @@ -0,0 +1,19 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CreateChatRoomMemberDto { + + private String chatRoomUuid; + private String sellerUuid; + private String buyerUuid; + + @Builder + public CreateChatRoomMemberDto(String chatRoomUuid, String sellerUuid, String buyerUuid) { + this.chatRoomUuid = chatRoomUuid; + this.sellerUuid = sellerUuid; + this.buyerUuid = buyerUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/CreateChatRoomRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/CreateChatRoomRequestDto.java new file mode 100644 index 0000000..a9e24b5 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/CreateChatRoomRequestDto.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.in; + +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomType; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CreateChatRoomRequestDto { + + private String postUuid; + private String sellerUuid; + private String buyerUuid; + private ChatRoomType chatRoomType; + + @Builder + public CreateChatRoomRequestDto(String postUuid, String sellerUuid, String buyerUuid, ChatRoomType chatRoomType) { + this.postUuid = postUuid; + this.sellerUuid = sellerUuid; + this.buyerUuid = buyerUuid; + this.chatRoomType = chatRoomType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/ExitChatRoomRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/ExitChatRoomRequestDto.java new file mode 100644 index 0000000..53a2565 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/ExitChatRoomRequestDto.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ExitChatRoomRequestDto { + + private String memberUuid; + private String chatRoomUuid; + + @Builder + public ExitChatRoomRequestDto(String memberUuid, String chatRoomUuid) { + this.memberUuid = memberUuid; + this.chatRoomUuid = chatRoomUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomInfoRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomInfoRequestDto.java new file mode 100644 index 0000000..dd07dee --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomInfoRequestDto.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetChatRoomInfoRequestDto { + + private String chatRoomUuid; + + @Builder + public GetChatRoomInfoRequestDto(String chatRoomUuid) { + this.chatRoomUuid = chatRoomUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomListByPostRequestDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomListByPostRequestDto.java new file mode 100644 index 0000000..e08632e --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/in/GetChatRoomListByPostRequestDto.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.in; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetChatRoomListByPostRequestDto { + + private String postUuid; + + @Builder + public GetChatRoomListByPostRequestDto(String postUuid) { + this.postUuid = postUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/CreateChatRoomResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/CreateChatRoomResponseDto.java new file mode 100644 index 0000000..42ceff4 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/CreateChatRoomResponseDto.java @@ -0,0 +1,10 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.out; + +import lombok.Getter; + +@Getter +public class CreateChatRoomResponseDto { + + private String chatRoomUuid; + private String postUuid; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomInfoResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomInfoResponseDto.java new file mode 100644 index 0000000..672a306 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomInfoResponseDto.java @@ -0,0 +1,25 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class GetChatRoomInfoResponseDto { + + private String chatRoomUuid; + private String postUuid; + private String chatRoomType; + private List members; + + @Builder + public GetChatRoomInfoResponseDto(String chatRoomUuid, String postUuid, String chatRoomType, List members) { + this.chatRoomUuid = chatRoomUuid; + this.postUuid = postUuid; + this.chatRoomType = chatRoomType; + this.members = members; + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomListByPostResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomListByPostResponseDto.java new file mode 100644 index 0000000..0b57108 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomListByPostResponseDto.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.out; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class GetChatRoomListByPostResponseDto { + + private String chatRoomUuid; + + @Builder + public GetChatRoomListByPostResponseDto(String chatRoomUuid) { + this.chatRoomUuid = chatRoomUuid; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomSummaryResponseDto.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomSummaryResponseDto.java new file mode 100644 index 0000000..02dba24 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/dto/out/GetChatRoomSummaryResponseDto.java @@ -0,0 +1,34 @@ +package com.chalnakchalnak.chatservice.chatroom.application.dto.out; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class GetChatRoomSummaryResponseDto { + + private String chatRoomUuid; + private String opponentUuid; + private String lastMessage; + private LocalDateTime lastMessageSentAt; + private String messageType; + private int unreadCount; + + @Builder + public GetChatRoomSummaryResponseDto( + String chatRoomUuid, + String opponentUuid, + String lastMessage, + LocalDateTime lastMessageSentAt, + String messageType, + int unreadCount + ) { + this.chatRoomUuid = chatRoomUuid; + this.opponentUuid = opponentUuid; + this.lastMessage = lastMessage; + this.lastMessageSentAt = lastMessageSentAt; + this.messageType = messageType; + this.unreadCount = unreadCount; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/mapper/ChatRoomMapper.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/mapper/ChatRoomMapper.java new file mode 100644 index 0000000..1c8b962 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/mapper/ChatRoomMapper.java @@ -0,0 +1,43 @@ +package com.chalnakchalnak.chatservice.chatroom.application.mapper; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomMemberDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.CreateChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomInfoResponseDto; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ChatRoomMapper { + + public CreateChatRoomDto toCreateChatRoomDto(CreateChatRoomRequestDto createChatRoomRequestDto, String chatRoomUuid) { + return CreateChatRoomDto.builder() + .chatRoomUuid(chatRoomUuid) + .postUuid(createChatRoomRequestDto.getPostUuid()) + .chatRoomType(createChatRoomRequestDto.getChatRoomType()) + .build(); + } + + public CreateChatRoomMemberDto toCreateChatRoomMemberDto(CreateChatRoomRequestDto createChatRoomRequestDto, String chatRoomUuid) { + return CreateChatRoomMemberDto.builder() + .chatRoomUuid(chatRoomUuid) + .sellerUuid(createChatRoomRequestDto.getSellerUuid()) + .buyerUuid(createChatRoomRequestDto.getBuyerUuid()) + .build(); + } + + public GetChatRoomInfoResponseDto toGetChatRoomInfoResponseDto( + ChatRoomInfoDto chatRoomInfoDto, + List chatRoomMemberInfoDto + ) { + return GetChatRoomInfoResponseDto.builder() + .chatRoomUuid(chatRoomInfoDto.getChatRoomUuid()) + .postUuid(chatRoomInfoDto.getPostUuid()) + .chatRoomType(chatRoomInfoDto.getChatRoomType().toString()) + .members(chatRoomMemberInfoDto) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomSummaryUseCase.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomSummaryUseCase.java new file mode 100644 index 0000000..8ab8ce9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomSummaryUseCase.java @@ -0,0 +1,10 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.in; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; + +import java.util.List; + +public interface ChatRoomSummaryUseCase { + + List getMyChatRoomList(String memberUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomUseCase.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomUseCase.java new file mode 100644 index 0000000..4f8fed1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/in/ChatRoomUseCase.java @@ -0,0 +1,18 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.in; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.CreateChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomListByPostRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomInfoResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; + +import java.util.List; + +public interface ChatRoomUseCase { + + String createPrivateChatRoom(CreateChatRoomRequestDto createChatRoomRequestDto); + void exitChatRoom(ExitChatRoomRequestDto exitChatRoomRequestDto); + GetChatRoomInfoResponseDto getChatRoomInfo(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto); + List getChatRoomListByPost(GetChatRoomListByPostRequestDto getChatRoomListByPostRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitRepositoryPort.java new file mode 100644 index 0000000..191648f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitRepositoryPort.java @@ -0,0 +1,10 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberExitDto; + +import java.util.Optional; + +public interface ChatRoomMemberExitRepositoryPort { + + Optional findByChatRoomUuidAndMemberUuid(String chatRoomUuid, String memberUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitUpdaterPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitUpdaterPort.java new file mode 100644 index 0000000..8b282e9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberExitUpdaterPort.java @@ -0,0 +1,8 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; + +public interface ChatRoomMemberExitUpdaterPort { + + void updateExitedAt(ExitChatRoomRequestDto exitChatRoomRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberRepositoryPort.java new file mode 100644 index 0000000..1490c99 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomMemberRepositoryPort.java @@ -0,0 +1,16 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomMemberDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomMemberRepositoryPort { + + void saveChatRoomMembers(CreateChatRoomMemberDto createChatRoomMemberDto); + Optional findPrivateChatRoomUuid(String postUuid, String buyerUuid); + String findOpponentUuid(String chatRoomUuid, String myMemberUuid); + Optional> getChatRoomMembers(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomRepositoryPort.java new file mode 100644 index 0000000..597fb72 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomRepositoryPort.java @@ -0,0 +1,18 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.CreateChatRoomDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomListByPostRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomRepositoryPort { + + String createChatRoom(CreateChatRoomDto createChatRoomDto); + Optional findRoomUuidById(Long roomId); + Optional getChatRoomInfo(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto); + Optional> getChatRoomListByPost(GetChatRoomListByPostRequestDto getChatRoomListByPostRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryRepositoryPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryRepositoryPort.java new file mode 100644 index 0000000..87ca75d --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryRepositoryPort.java @@ -0,0 +1,13 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; + +import java.util.List; + +public interface ChatRoomSummaryRepositoryPort { + + List getMyChatRoomList(String memberUuid); + void deleteChatRoomSummary(ExitChatRoomRequestDto exitChatRoomRequestDto); + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryUpdaterPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryUpdaterPort.java new file mode 100644 index 0000000..858e33c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/ChatRoomSummaryUpdaterPort.java @@ -0,0 +1,10 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatmessage.application.dto.ChatMessageDto; +import com.chalnakchalnak.chatservice.chatmessage.application.dto.in.ReadMessageRequestDto; + +public interface ChatRoomSummaryUpdaterPort { + + void updateOnMessage(ChatMessageDto chatMessageDto, String receiverUuid); + void updateOnRead(ReadMessageRequestDto readMessageRequestDto); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/GenerateUuidPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/GenerateUuidPort.java new file mode 100644 index 0000000..762d583 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/GenerateUuidPort.java @@ -0,0 +1,6 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +public interface GenerateUuidPort { + + String generateUuid(); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/PublishChatRoomSummaryUpdatePort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/PublishChatRoomSummaryUpdatePort.java new file mode 100644 index 0000000..f114ac9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/PublishChatRoomSummaryUpdatePort.java @@ -0,0 +1,9 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out; + +import com.chalnakchalnak.chatservice.chatroom.adpater.out.kafka.payload.ChatRoomSummaryUpdateEvent; + +public interface PublishChatRoomSummaryUpdatePort { + + void publishChatRoomSummaryUpdate(ChatRoomSummaryUpdateEvent chatRoomSummaryUpdateEvent); + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/validator/ChatRoomValidatorPort.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/validator/ChatRoomValidatorPort.java new file mode 100644 index 0000000..0124a90 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/port/out/validator/ChatRoomValidatorPort.java @@ -0,0 +1,6 @@ +package com.chalnakchalnak.chatservice.chatroom.application.port.out.validator; + +public interface ChatRoomValidatorPort { + + void memberAccessed(String roomUUid, String userUuid); +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomService.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomService.java new file mode 100644 index 0000000..b0407d0 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomService.java @@ -0,0 +1,86 @@ +package com.chalnakchalnak.chatservice.chatroom.application.service; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.ChatRoomMemberInfoDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.CreateChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.ExitChatRoomRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomInfoRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.in.GetChatRoomListByPostRequestDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomInfoResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomListByPostResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.mapper.ChatRoomMapper; +import com.chalnakchalnak.chatservice.chatroom.application.port.in.ChatRoomUseCase; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.*; +import com.chalnakchalnak.chatservice.common.exception.BaseException; +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ChatRoomService implements ChatRoomUseCase { + + private final ChatRoomRepositoryPort chatRoomRepositoryPort; + private final ChatRoomMemberRepositoryPort chatRoomMemberRepositoryPort; + private final ChatRoomSummaryRepositoryPort chatRoomSummaryRepositoryPort; + private final ChatRoomMemberExitUpdaterPort chatRoomMemberExitUpdaterPort; + private final ChatRoomMapper chatRoomMapper; + private final GenerateUuidPort generateUuidPort; + + @Override + @Transactional + public String createPrivateChatRoom(CreateChatRoomRequestDto createChatRoomRequestDto) { + Optional existingRoomId = chatRoomMemberRepositoryPort.findPrivateChatRoomUuid( + createChatRoomRequestDto.getPostUuid(), createChatRoomRequestDto.getBuyerUuid() + ); + + if (existingRoomId.isPresent()) { + return chatRoomRepositoryPort.findRoomUuidById(existingRoomId.get()) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_NOT_FOUND)); + } + + final String chatRoomUuid = generateUuidPort.generateUuid(); + + chatRoomRepositoryPort.createChatRoom( + chatRoomMapper.toCreateChatRoomDto(createChatRoomRequestDto, chatRoomUuid) + ); + chatRoomMemberRepositoryPort.saveChatRoomMembers( + chatRoomMapper.toCreateChatRoomMemberDto(createChatRoomRequestDto, chatRoomUuid) + ); + + return chatRoomUuid; + } + + //@Transactional + @Override + public void exitChatRoom(ExitChatRoomRequestDto exitChatRoomRequestDto) { + + chatRoomMemberExitUpdaterPort.updateExitedAt(exitChatRoomRequestDto); + + chatRoomSummaryRepositoryPort.deleteChatRoomSummary(exitChatRoomRequestDto); + } + + @Override + public GetChatRoomInfoResponseDto getChatRoomInfo(GetChatRoomInfoRequestDto getChatRoomInfoRequestDto) { + ChatRoomInfoDto chatRoomInfoDto = + chatRoomRepositoryPort.getChatRoomInfo(getChatRoomInfoRequestDto) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_NOT_FOUND)); + + List chatRoomMemberInfoDto = + chatRoomMemberRepositoryPort.getChatRoomMembers(getChatRoomInfoRequestDto) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_MEMBER_NOT_FOUND)); + + return chatRoomMapper.toGetChatRoomInfoResponseDto(chatRoomInfoDto, chatRoomMemberInfoDto); + } + + @Override + public List getChatRoomListByPost(GetChatRoomListByPostRequestDto getChatRoomListByPostRequestDto) { + return chatRoomRepositoryPort.getChatRoomListByPost(getChatRoomListByPostRequestDto) + .orElseThrow(() -> new BaseException(BaseResponseStatus.CHAT_ROOM_NOT_FOUND)); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomSummaryService.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomSummaryService.java new file mode 100644 index 0000000..3df41a6 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/application/service/ChatRoomSummaryService.java @@ -0,0 +1,22 @@ +package com.chalnakchalnak.chatservice.chatroom.application.service; + +import com.chalnakchalnak.chatservice.chatroom.application.dto.out.GetChatRoomSummaryResponseDto; +import com.chalnakchalnak.chatservice.chatroom.application.port.in.ChatRoomSummaryUseCase; +import com.chalnakchalnak.chatservice.chatroom.application.port.out.ChatRoomSummaryRepositoryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatRoomSummaryService implements ChatRoomSummaryUseCase { + + private final ChatRoomSummaryRepositoryPort chatRoomSummaryRepositoryPort; + + @Override + public List getMyChatRoomList(String memberUuid){ + + return chatRoomSummaryRepositoryPort.getMyChatRoomList(memberUuid); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomDomain.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomDomain.java new file mode 100644 index 0000000..5ffd4ab --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomDomain.java @@ -0,0 +1,20 @@ +package com.chalnakchalnak.chatservice.chatroom.domain; + +import com.chalnakchalnak.chatservice.chatroom.domain.enums.ChatRoomType; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ChatRoomDomain { + + private String postUuid; + private String chatRoomUuid; + private ChatRoomType chatRoomType; + + @Builder + public ChatRoomDomain(String postUuid, String chatRoomUuid, ChatRoomType chatRoomType) { + this.postUuid = postUuid; + this.chatRoomUuid = chatRoomUuid; + this.chatRoomType = chatRoomType; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomMemberDomain.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomMemberDomain.java new file mode 100644 index 0000000..09a51f9 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/ChatRoomMemberDomain.java @@ -0,0 +1,21 @@ +package com.chalnakchalnak.chatservice.chatroom.domain; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ChatRoomMemberDomain { + + private String chatRoomUuid; + private String memberUuid; + private LocalDateTime joinedAt; + + @Builder + public ChatRoomMemberDomain(String chatRoomUuid, String memberUuid, LocalDateTime joinedAt) { + this.chatRoomUuid = chatRoomUuid; + this.memberUuid = memberUuid; + this.joinedAt = joinedAt; + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomMemberRole.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomMemberRole.java new file mode 100644 index 0000000..f29ebd5 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomMemberRole.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.chatroom.domain.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ChatRoomMemberRole { + + SELLER("판매자"), + BUYER("구매자"); + + private final String label; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomType.java b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomType.java new file mode 100644 index 0000000..edaa1e1 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/chatroom/domain/enums/ChatRoomType.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.chatroom.domain.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ChatRoomType { + + AUCTION_PRIVATE("경매 1대1 채팅방"), + NORMAL_PRIVATE("일반 1대1 채팅방"); + + private final String label; +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/config/MongoConfig.java b/src/main/java/com/chalnakchalnak/chatservice/common/config/MongoConfig.java new file mode 100644 index 0000000..90ad96c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/config/MongoConfig.java @@ -0,0 +1,49 @@ +package com.chalnakchalnak.chatservice.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class MongoConfig { + + private final MongoDatabaseFactory mongoDatabaseFactory; + + @Bean + public MongoCustomConversions customConversions() { + List> converters = new ArrayList<>(); + converters.add(new LocalDateTimeToDateConverter()); + converters.add(new DateToLocalDateTimeConverter()); + return new MongoCustomConversions(converters); + } + + static class LocalDateTimeToDateConverter implements Converter { + @Override + public Date convert(LocalDateTime source) { + return Date.from(source.atZone(ZoneId.of("Asia/Seoul")).toInstant()); + } + } + + static class DateToLocalDateTimeConverter implements Converter { + @Override + public LocalDateTime convert(Date source) { + return LocalDateTime.ofInstant(source.toInstant(), ZoneId.of("Asia/Seoul")); + } + } + + @Bean + public MongoTransactionManager transactionManager() { + return new MongoTransactionManager(mongoDatabaseFactory); + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/config/S3Config.java b/src/main/java/com/chalnakchalnak/chatservice/common/config/S3Config.java new file mode 100644 index 0000000..5d38d9f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/config/S3Config.java @@ -0,0 +1,29 @@ +package com.chalnakchalnak.chatservice.common.config; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.region.static}") + private String region; + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(DefaultAWSCredentialsProviderChain.getInstance()) + .build(); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/config/SwaggerConfig.java b/src/main/java/com/chalnakchalnak/chatservice/common/config/SwaggerConfig.java new file mode 100644 index 0000000..cb9386c --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/config/SwaggerConfig.java @@ -0,0 +1,43 @@ +package com.chalnakchalnak.chatservice.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private static final String BEARER_TOKEN_PREFIX = "Bearer"; + + @Bean + public OpenAPI openAPI() { + + String securityJwtName = "JWT"; + SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityJwtName); + Components components = new Components() + .addSecuritySchemes(securityJwtName, new SecurityScheme() + .name(securityJwtName) + .type(SecurityScheme.Type.HTTP) + .scheme(BEARER_TOKEN_PREFIX) + .bearerFormat(securityJwtName)); + + return new OpenAPI() + .addSecurityItem(securityRequirement) + .components(components) + .addServersItem(new Server().url("/chat-service")) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("MSA - CHAT SERVICE 문서") + .description("00 API 테스트를 위한 Swagger UI") + .version("1.0.0"); + } + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/exception/AsyncExceptionHandler.java b/src/main/java/com/chalnakchalnak/chatservice/common/exception/AsyncExceptionHandler.java new file mode 100644 index 0000000..b939d3f --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/exception/AsyncExceptionHandler.java @@ -0,0 +1,17 @@ +package com.chalnakchalnak.chatservice.common.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +import java.lang.reflect.Method; + +@Slf4j +public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + if (ex instanceof BaseException baseException) { + log.error("BaseException: [{}] {}", baseException.getStatus(), baseException.getStatus().getMessage()); + } + log.error("EventException: ", ex); + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseException.java b/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseException.java new file mode 100644 index 0000000..b0e5a64 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseException.java @@ -0,0 +1,14 @@ +package com.chalnakchalnak.chatservice.common.exception; + +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class BaseException extends RuntimeException{ + + private final BaseResponseStatus status; + public BaseException(BaseResponseStatus status) { + super(status.getMessage()); + this.status = status; + } +} \ No newline at end of file diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseExceptionHandler.java b/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseExceptionHandler.java new file mode 100644 index 0000000..5069a54 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/exception/BaseExceptionHandler.java @@ -0,0 +1,91 @@ +package com.chalnakchalnak.chatservice.common.exception; + +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@Hidden +@RestControllerAdvice +@Slf4j +public class BaseExceptionHandler { + + /** + * 발생한 예외 처리 + */ + + @ExceptionHandler(BaseException.class) + protected ResponseEntity> BaseError(BaseException e) { + ExceptionResponseEntity response = new ExceptionResponseEntity<>(e.getStatus()); + log.error("BaseException -> {}({})", e.getStatus(), e.getStatus().getMessage(), e); + return new ResponseEntity<>(response, response.httpStatus()); + } + + @ExceptionHandler(RuntimeException.class) + protected ResponseEntity> RuntimeError(RuntimeException e) { + ExceptionResponseEntity response = new ExceptionResponseEntity<>(BaseResponseStatus.INTERNAL_SERVER_ERROR); + log.error("RuntimeException: ", e); + + for (StackTraceElement s : e.getStackTrace()) { + System.out.println(s); + } + return new ResponseEntity<>(response, response.httpStatus()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { + log.warn("ValidationException: {}", e.getMessage()); + + String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + + ExceptionResponseEntity response = new ExceptionResponseEntity<>( + BaseResponseStatus.INVALID_INPUT, + message + ); + + return new ResponseEntity<>(response, response.httpStatus()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + protected ResponseEntity> handleJsonParseException(HttpMessageNotReadableException e) { + log.warn("Json parsing error: {}", e.getMessage()); + + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof BaseException baseEx) { + ExceptionResponseEntity response = new ExceptionResponseEntity<>(baseEx.getStatus()); + return new ResponseEntity<>(response, response.httpStatus()); + } + + cause = cause.getCause(); + } + + ExceptionResponseEntity response = new ExceptionResponseEntity<>(BaseResponseStatus.INVALID_INPUT); + return new ResponseEntity<>(response, response.httpStatus()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex) { + BaseResponseStatus status; + + if (ex instanceof HttpRequestMethodNotSupportedException) { + status = BaseResponseStatus.METHOD_NOT_ALLOWED; + } else if (ex instanceof NoHandlerFoundException) { + status = BaseResponseStatus.NOT_FOUND; + } else if (ex instanceof MethodArgumentTypeMismatchException) { + status = BaseResponseStatus.BAD_REQUEST_INVALID_PARAM; + } else { + ex.printStackTrace(); + status = BaseResponseStatus.INTERNAL_SERVER_ERROR; + } + + return new ResponseEntity<>(new ExceptionResponseEntity<>(status), status.getHttpStatusCode()); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/exception/ExceptionResponseEntity.java b/src/main/java/com/chalnakchalnak/chatservice/common/exception/ExceptionResponseEntity.java new file mode 100644 index 0000000..6afb3ce --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/exception/ExceptionResponseEntity.java @@ -0,0 +1,16 @@ +package com.chalnakchalnak.chatservice.common.exception; + +import com.chalnakchalnak.chatservice.common.response.BaseResponseStatus; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.http.HttpStatusCode; + +public record ExceptionResponseEntity(@JsonIgnore HttpStatusCode httpStatus, int code, String message) { + + public ExceptionResponseEntity(BaseResponseStatus status) { + this(status.getHttpStatusCode(), status.getCode(), status.getMessage()); + } + + public ExceptionResponseEntity(BaseResponseStatus status, String message) { + this(status.getHttpStatusCode(), status.getCode(), message); + } +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/response/BaseResponseStatus.java b/src/main/java/com/chalnakchalnak/chatservice/common/response/BaseResponseStatus.java new file mode 100644 index 0000000..3a94ce7 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/response/BaseResponseStatus.java @@ -0,0 +1,66 @@ +package com.chalnakchalnak.chatservice.common.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; + +@Getter +@AllArgsConstructor +public enum BaseResponseStatus { + + /** + * 5000 : chat-service 에러 + */ + + /** + * 5000~5099 : security 에러 + */ + INVALID_USER_UUID(HttpStatus.UNAUTHORIZED, 5000, "유효하지 않은 사용자 UUID입니다."), + NO_CHAT_ACCESS(HttpStatus.FORBIDDEN, 5001, "채팅방에 접근할 수 없습니다. 권한을 확인해주세요."), + + /** + * 5100~5199: Request 유효성 에러 + */ + BAD_REQUEST_INVALID_PARAM(HttpStatus.BAD_REQUEST, 5100, "잘못된 요청입니다. 파라미터를 확인해주세요."), + NOT_FOUND(HttpStatus.NOT_FOUND, 5101, "요청한 리소스를 찾을 수 없습니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, 5102, "허용되지 않은 HTTP 메서드입니다."), + INVALID_INPUT(HttpStatus.BAD_REQUEST, 5103, "유효하지 입력입니다"), + INVALID_OBJECT_ID(HttpStatus.BAD_REQUEST, 5104, "유효하지 않은 ObjectId입니다."), + + /** + * 5200~5299 : 채팅 에러 + */ + FAILED_PUBLISH_MESSAGE(HttpStatus.INTERNAL_SERVER_ERROR, 5200, "메시지 발행 실패"), + FAILED_CONSUME_MESSAGE(HttpStatus.INTERNAL_SERVER_ERROR, 5201, "메시지 수신 실패"), + FAILED_PUBLISH_CHAT_ROOM_SUMMARY_UPDATE(HttpStatus.INTERNAL_SERVER_ERROR, 5202, "채팅방 요약 업데이트 이벤트 발행 실패"), + FAILED_UPDATE_READ_CHECK_POINT(HttpStatus.INTERNAL_SERVER_ERROR, 5203, "읽음 체크포인트 업데이트 실패"), + + /** + * 5300~5399 : PreSignedUrl 관련 에러 + */ + UNABLE_TO_CALCULATE_HMAC(HttpStatus.INTERNAL_SERVER_ERROR, 2003, "HMAC을 계산할 수 없습니다"), + INVALID_IMAGE_MESSAGE_KEY_FORMAT(HttpStatus.BAD_REQUEST, 5300, "이미지 메시지 키 형식이 유효하지 않습니다."), + INVALID_IMAGE_MESSAGE_SENDER(HttpStatus.BAD_REQUEST, 5301, "이미지 메시지의 발신자 UUID가 키 내에 포함된 UUID와 일치하지 않습니다."), + IMAGE_FILE_NOT_FOUND_IN_S3(HttpStatus.NOT_FOUND, 5302, "S3에서 이미지 파일을 찾을 수 없습니다."), + FAILED_TO_ACCESS_S3(HttpStatus.INTERNAL_SERVER_ERROR, 5303, "S3에 접근하는 데 실패했습니다."), + + /** + * 5900~5999 : 기타 에러 + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5900,"서버 내부 오류가 발생했습니다. 관리자에게 문의해주세요."), + CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 5901, "채팅방을 찾을 수 없습니다."), + CHAT_ROOM_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, 5902, "해당 채팅방에 포함되지 않은 유저입니다."), + FAILED_SERIALIZE_MESSAGE(HttpStatus.INTERNAL_SERVER_ERROR, 5903, "메시지 직렬화에 실패했습니다."), + INVALID_MESSAGE_SEQ(HttpStatus.BAD_REQUEST, 5904, "유효하지 않은 메시지 시퀀스입니다."), + MEMBER_CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 5905, "해당 유저의 채팅방을 찾을 수 없습니다."), + CHAT_ROOM_MEMBER_EXIT_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, 5906, "채팅방 멤버 퇴장 기록을 찾을 수 없습니다."), + CHAT_MESSAGE_NOT_FOUND(HttpStatus.NOT_FOUND, 5907, "해당 채팅 메시지를 찾을 수 없습니다."),; + + + + private final HttpStatusCode httpStatusCode; + private final int code; + private final String message; + +} diff --git a/src/main/java/com/chalnakchalnak/chatservice/common/util/uuid/JavaUuidAdapter.java b/src/main/java/com/chalnakchalnak/chatservice/common/util/uuid/JavaUuidAdapter.java new file mode 100644 index 0000000..93e0d80 --- /dev/null +++ b/src/main/java/com/chalnakchalnak/chatservice/common/util/uuid/JavaUuidAdapter.java @@ -0,0 +1,15 @@ +package com.chalnakchalnak.chatservice.common.util.uuid; + +import com.chalnakchalnak.chatservice.chatroom.application.port.out.GenerateUuidPort; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class JavaUuidAdapter implements GenerateUuidPort { + + @Override + public String generateUuid() { + return UUID.randomUUID().toString(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..8fa93da --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,68 @@ +server: + port: 8080 + +spring: + datasource: + url: ${MYSQL_URL} + username: ${MYSQL_USERNAME} + password: ${MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + format_sql: true + + data: + mongodb: + uri: ${MONGO_URL}chat-service + + jackson: + time-zone: Asia/Seoul + + kafka: + bootstrap-servers: ${KAFKA_SERVERS} + producer: + key-deserializer: org.apache.kafka.common.serialization.StringSerializer + value-deserializer: org.apache.kafka.common.serialization.StringSerializer + acks: ${KAFKA_ACKS} + retries: ${KAFKA_RETRIES} + batch-size: 16384 + linger-ms: 1 + buffer-memory: 33554432 + properties: + request.timeout.ms: ${KAFKA_REQUEST_TIMEOUT_MS} + delivery.timeout.ms: ${KAFKA_DELIVERY_TIMEOUT_MS} + max.block.ms: ${KAFKA_MAX_BLOCK_MS} + consumer: + group-id: ${KAFKA_CONSUMER_GROUP_ID} + auto-offset-reset: latest + enable-auto-commit: false + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + max-poll-records: 2000 + listener: + ack-mode: manual_immediate + type: batch + +cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + region: + static: ${AWS_REGION} + s3: + bucket: ${AWS_S3_BUCKET_NAME} + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui + config-url: /chat-service/v3/api-docs/swagger-config + url: /chat-service/v3/api-docs \ No newline at end of file