From 7650f4a4287c0d036c242d92471d4d59ee035676 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:11:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20API=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 각 모듈별 docker파일 분리 * fix: chat_member 엔티티 image_url 제거 * feat: 채팅방 생성 repo & service 정의 * feat: 채팅 멤버 생성 service 정의 * test: 채팅방 생성 컨트롤러 단위 테스트 성공 시나리오 작성 * feat: chat_room_request dto 정의 * feat: chat_room craete controller * feat: chat_room_usecase 및 request & response schema 추가 * fix: controller success response 추가 * test: chat_room_fixture * fix: chat_room_res detail dto to_string 수정 * fix: controller 내 usecase final 키워드 추가 * feat: 채팅방 역할 상수 정의 * fix: chat_memeber role 필드 추가 * fix: chat_room create dto to_entity 메서드 static 제거 * feat: chat_member entity 정적 팩토리 메서드 추가 * feat: 채팅방 생성 서비스 로직 구현 * feat: chat_room_mapper 정의 * test: 채팅방 생성 통합 테스트 구현 * fix: 채팅방 생성 요청 background_image_url pattern 수정 * feat: 채팅방 생성 요청 캐싱 entity * feat: 생성 지연 채팅방 서비스 로직 * fix: pended_chat_room user_id 필드 추가 및 유효성 검사 & to_chat_room 메서드 추가 * fix: chat_room_req pend, create 요청 분리 * fix: 캐싱된 채팅방 정보 예외 정의 * fix: 채팅방 생성 서비스 로직 대기, 생성 메서드 분리 * feat: external-api 모듈 guid config 의존성 추가 * fix: pended_chat_room hash code 수정 * fix: 사용자 아이디로 캐싱된 정보 조회 -> 사용자 권한 예외 제거 가능하므로 삭제 * fix: controller 및 usecase 요청 분리 * test: controller 단위 테스트 pend, create 분리 * feat: 채팅방 entity pk auto_increment 속성 제거 * test: chat_room_fixture url 정보 수정 * fix: 캐싱된 채팅방 정보 로깅 추가 * test: 채팅방 생성 시나리오 테스트 수정 * fix: 채팅방 멤버 생성 시, entity 반환하도록 chat_member_service 로직 수정 * feat: 채팅방 저장 시, image url 변환 작업 수행 로직 추가 * test: aws_adapter mock bean 설정 추가 * feat: 채팅방 swagger group 등록 * docs: 채팅방 생성 대기 및 확정 api 스웨거 문서 작성 * chore: 불필요한 dockerfile 모두 제거 * chore: socket-relay dockerfile 복구 --- Dockerfile | 48 ++++++++ .../api/apis/chat/api/ChatRoomApi.java | 31 +++++ .../chat/controller/ChatRoomController.java | 40 +++++++ .../api/apis/chat/dto/ChatRoomReq.java | 35 ++++++ .../api/apis/chat/dto/ChatRoomRes.java | 44 +++++++ .../api/apis/chat/mapper/ChatRoomMapper.java | 12 ++ .../chat/service/ChatRoomSaveService.java | 78 +++++++++++++ .../apis/chat/usecase/ChatRoomUseCase.java | 25 ++++ .../co/pennyway/api/config/InfraConfig.java | 3 +- .../co/pennyway/api/config/SwaggerConfig.java | 11 ++ .../ChatRoomSaveControllerUnitTest.java | 95 +++++++++++++++ .../ChatRoomPendIntegrationTest.java | 108 ++++++++++++++++++ .../SpendingControllerIntegrationTest.java | 2 +- .../api/config/fixture/ChatRoomFixture.java | 30 +++++ .../converter/ChatMemberRoleConverter.java | 13 +++ .../common/redis/chatroom/PendedChatRoom.java | 74 ++++++++++++ .../chatroom/PendedChatRoomErrorCode.java | 28 +++++ .../PendedChatRoomErrorException.java | 21 ++++ .../chatroom/PendedChatRoomRepository.java | 6 + .../redis/chatroom/PendedChatRoomService.java | 23 ++++ .../domains/chatroom/domain/ChatRoom.java | 17 ++- .../repository/ChatRoomRepository.java | 7 ++ .../chatroom/service/ChatRoomService.java | 20 ++++ .../domains/member/domain/ChatMember.java | 25 +++- .../member/service/ChatMemberService.java | 6 + .../domains/member/type/ChatMemberRole.java | 29 +++++ 26 files changed, 819 insertions(+), 12 deletions(-) create mode 100644 Dockerfile create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/api/ChatRoomApi.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomController.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomReq.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomRes.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/mapper/ChatRoomMapper.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/service/ChatRoomSaveService.java create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/usecase/ChatRoomUseCase.java create mode 100644 pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomSaveControllerUnitTest.java create mode 100644 pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/integration/ChatRoomPendIntegrationTest.java create mode 100644 pennyway-app-external-api/src/test/java/kr/co/pennyway/api/config/fixture/ChatRoomFixture.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/converter/ChatMemberRoleConverter.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoom.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorCode.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorException.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomRepository.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomService.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/repository/ChatRoomRepository.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/service/ChatRoomService.java create mode 100644 pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/type/ChatMemberRole.java diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..672d77a6b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Common 모듈 빌드 +FROM openjdk:17 AS common-builder +WORKDIR /app + +COPY gradlew . +COPY gradle gradle +COPY build.gradle . +COPY settings.gradle . +COPY pennyway-common pennyway-common +RUN chmod +x ./gradlew + + +COPY pennyway-common pennyway-common +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle +RUN ./gradlew :pennyway-common:build -x test + +# Infra 모듈 빌드 +FROM openjdk:17 AS infra-builder +WORKDIR /app +COPY --from=common-builder /app/pennyway-common/build/libs/*.jar lib/ +COPY pennyway-infra pennyway-infra +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle +RUN ./gradlew :pennyway-infra:build -x test + +# Domain 모듈 빌드 +FROM openjdk:17 AS domain-builder +WORKDIR /app +COPY --from=common-builder /app/pennyway-common/build/libs/*.jar lib/ +COPY --from=infra-builder /app/pennyway-infra/build/libs/*.jar lib/ +COPY pennyway-domain pennyway-domain +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle +RUN ./gradlew :pennyway-domain:build -x test + +# 최종 실행 이미지 +FROM openjdk:17 + +WORKDIR /app + +# 빌드된 JAR 파일 복사 +COPY --from=common-builder /app/pennyway-common/build/libs/*.jar common.jar +COPY --from=infra-builder /app/pennyway-infra/build/libs/*.jar infra.jar +COPY --from=domain-builder /app/pennyway-domain/build/libs/*.jar domain.jar + +# 클래스패스 설정 +ENV CLASSPATH=/app/common.jar:/app/infra.jar:/app/domain.jar \ No newline at end of file diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/api/ChatRoomApi.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/api/ChatRoomApi.java new file mode 100644 index 000000000..69641482e --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/api/ChatRoomApi.java @@ -0,0 +1,31 @@ +package kr.co.pennyway.api.apis.chat.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.SchemaProperty; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; +import kr.co.pennyway.api.common.annotation.ApiExceptionExplanation; +import kr.co.pennyway.api.common.annotation.ApiResponseExplanations; +import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoomErrorCode; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "[채팅방 API]") +public interface ChatRoomApi { + @Operation(summary = "[1] 채팅방 생성 대기 요청", method = "POST", description = "채팅방 배경 이미지를 제외한 모든 정보를 서버에 저장하기 위한 API. 성공 시, 채팅방 아이디를 반환하며, 5분 후에 저장된 자동 삭제된다.") + @ApiResponse(responseCode = "200", description = "채팅방 생성 대기 요청 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoomId", schema = @Schema(implementation = Long.class)))) + ResponseEntity postChatRoom(@RequestBody ChatRoomReq.Pend request, @AuthenticationPrincipal SecurityUserDetails user); + + @Operation(summary = "[2] 채팅방 생성", method = "POST", description = "채팅방 배경 이미지를 전달하여 `[1] 채팅방 생성 대기 요청`의 요청을 확정짓는다. 저장된 채팅방 정보를 반환한다.") + @ApiResponse(responseCode = "200", description = "채팅방 생성 성공", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoom", schema = @Schema(implementation = ChatRoomRes.Detail.class)))) + @ApiResponseExplanations(errors = { + @ApiExceptionExplanation(value = PendedChatRoomErrorCode.class, constant = "NOT_FOUND", name = "채팅방 정보 탐색 실패", description = "사용자가 생성한 채팅방 정보가 존재하지 않는 경우 발생하며, 이 경우 채팅방 생성 요청은 재시도 없이 실패 처리해야 한다.") + }) + ResponseEntity createChatRoom(@RequestBody ChatRoomReq.Create request, @AuthenticationPrincipal SecurityUserDetails user); +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomController.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomController.java new file mode 100644 index 000000000..e904fb9d0 --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomController.java @@ -0,0 +1,40 @@ +package kr.co.pennyway.api.apis.chat.controller; + +import kr.co.pennyway.api.apis.chat.api.ChatRoomApi; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.apis.chat.usecase.ChatRoomUseCase; +import kr.co.pennyway.api.common.response.SuccessResponse; +import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/v2/chat-rooms") +public class ChatRoomController implements ChatRoomApi { + private static final String CHAT_ROOM_ID = "chatRoomId"; + private static final String CHAT_ROOM = "chatRoom"; + private final ChatRoomUseCase chatRoomUseCase; + + @Override + @PostMapping("/pend") + @PreAuthorize("isAuthenticated()") + public ResponseEntity postChatRoom(@RequestBody ChatRoomReq.Pend request, @AuthenticationPrincipal SecurityUserDetails user) { + return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM_ID, chatRoomUseCase.pendChatRoom(request, user.getUserId()))); + } + + @Override + @PostMapping("") + @PreAuthorize("isAuthenticated()") + public ResponseEntity createChatRoom(@RequestBody ChatRoomReq.Create request, @AuthenticationPrincipal SecurityUserDetails user) { + return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM, chatRoomUseCase.createChatRoom(request, user.getUserId()))); + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomReq.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomReq.java new file mode 100644 index 000000000..ef7caa4cf --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomReq.java @@ -0,0 +1,35 @@ +package kr.co.pennyway.api.apis.chat.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoom; + +public final class ChatRoomReq { + @Schema(title = "채팅방 생성 요청 - 대기") + public record Pend( + @NotBlank + @Size(min = 1, max = 50) + @Schema(description = "채팅방 제목. NULL 혹은 공백은 허용하지 않으며, 1~50자 이내의 문자열이어야 한다.", example = "페니웨이") + String title, + @Size(min = 1, max = 100) + @Schema(description = "채팅방 설명. NULL을 허용하며, 문자가 존재할 시 공백 허용 없이 1~100자 이내의 문자열이어야 한다.", example = "페니웨이 채팅방입니다.") + String description, + @Schema(description = "채팅방 비밀번호. NULL을 허용한다. 비밀번호는 6자리 정수만 허용", example = "123456") + @Size(min = 6, max = 6) + Integer password + ) { + public PendedChatRoom toEntity(Long id, Long userId) { + return PendedChatRoom.of(id, userId, title, description, password); + } + } + + @Schema(title = "채팅방 생성 요청 - 확정") + public record Create( + @Schema(description = "채팅방 배경 이미지 URL. NULL을 허용한다.", example = "delete/chatroom/{chatroom_id}/{uuid}_{timestamp}.{ext}") + @Pattern(regexp = "^delete/.*", message = "채팅방 배경 이미지 URL은 delete/로 시작해야 합니다.") + String backgroundImageUrl + ) { + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomRes.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomRes.java new file mode 100644 index 000000000..12d59446a --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/dto/ChatRoomRes.java @@ -0,0 +1,44 @@ +package kr.co.pennyway.api.apis.chat.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; + +import java.time.LocalDateTime; +import java.util.Objects; + +public final class ChatRoomRes { + @Schema(description = "채팅방 상세 정보") + public record Detail( + @Schema(description = "채팅방 ID", type = "long") + Long id, + @Schema(description = "채팅방 제목") + String title, + @Schema(description = "채팅방 설명") + String description, + @Schema(description = "채팅방 배경 이미지 URL") + String backgroundImageUrl, + @Schema(description = "채팅방 비공개 여부") + boolean isPrivate, + @Schema(description = "채팅방 참여자 수") + int participantCount, + @Schema(description = "채팅방 개설일") + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime createdAt + ) { + public static Detail from(ChatRoom chatRoom, int participantCount) { + return new Detail( + chatRoom.getId(), + chatRoom.getTitle(), + Objects.toString(chatRoom.getDescription(), ""), + Objects.toString(chatRoom.getBackgroundImageUrl(), ""), + chatRoom.getPassword() != null, + participantCount, + chatRoom.getCreatedAt() + ); + } + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/mapper/ChatRoomMapper.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/mapper/ChatRoomMapper.java new file mode 100644 index 000000000..9ebfa2326 --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/mapper/ChatRoomMapper.java @@ -0,0 +1,12 @@ +package kr.co.pennyway.api.apis.chat.mapper; + +import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; +import kr.co.pennyway.common.annotation.Mapper; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; + +@Mapper +public final class ChatRoomMapper { + public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, int participantCount) { + return ChatRoomRes.Detail.from(chatRoom, participantCount); + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/service/ChatRoomSaveService.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/service/ChatRoomSaveService.java new file mode 100644 index 000000000..1c663635e --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/service/ChatRoomSaveService.java @@ -0,0 +1,78 @@ +package kr.co.pennyway.api.apis.chat.service; + +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.common.storage.AwsS3Adapter; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoom; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoomErrorCode; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoomErrorException; +import kr.co.pennyway.domain.common.redis.chatroom.PendedChatRoomService; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomService; +import kr.co.pennyway.domain.domains.member.domain.ChatMember; +import kr.co.pennyway.domain.domains.member.service.ChatMemberService; +import kr.co.pennyway.domain.domains.member.type.ChatMemberRole; +import kr.co.pennyway.domain.domains.user.domain.User; +import kr.co.pennyway.domain.domains.user.exception.UserErrorCode; +import kr.co.pennyway.domain.domains.user.exception.UserErrorException; +import kr.co.pennyway.domain.domains.user.service.UserService; +import kr.co.pennyway.infra.client.aws.s3.ObjectKeyType; +import kr.co.pennyway.infra.client.guid.IdGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatRoomSaveService { + private final UserService userService; + private final ChatRoomService chatRoomService; + private final ChatMemberService chatMemberService; + private final PendedChatRoomService pendedChatRoomService; + + private final AwsS3Adapter awsS3Adapter; + private final IdGenerator idGenerator; + + /** + * 채팅방 생성 정보를 캐싱한다. + * 해당 요청은 {@link #createChatRoom(ChatRoomReq.Create, Long)}를 통해 채팅방을 확정할 수 있다. + * + * @return 캐싱된 채팅방의 ID + */ + @Transactional + public Long pendChatRoom(ChatRoomReq.Pend request, Long userId) { + Long chatRoomId = idGenerator.generate(); + log.info("Chat room ID: {}", chatRoomId); + PendedChatRoom info = pendedChatRoomService.create(request.toEntity(chatRoomId, userId)); + log.info("Pended chat room: {}", info); + + return chatRoomId; + } + + /** + * 캐싱된 채팅방을 확정하고 채팅방을 생성한다. + * 채팅방을 생성한 사용자는 채팅방의 관리자로 설정된다. + * + * @throws PendedChatRoomErrorException: {@link PendedChatRoomErrorCode#NOT_FOUND} - 캐싱된 채팅방이 존재하지 않을 경우 + */ + @Transactional + public ChatRoom createChatRoom(ChatRoomReq.Create request, Long userId) { + PendedChatRoom pendedChatRoom = pendedChatRoomService.readByUserId(userId) + .orElseThrow(() -> new PendedChatRoomErrorException(PendedChatRoomErrorCode.NOT_FOUND)); + + String originImageUrl = null; + if (request.backgroundImageUrl() != null) { + originImageUrl = awsS3Adapter.saveImage(request.backgroundImageUrl(), ObjectKeyType.CHAT_PROFILE); + } + + ChatRoom chatRoom = chatRoomService.create(pendedChatRoom.toChatRoom(originImageUrl)); + + User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND)); + ChatMember member = ChatMember.of(user.getName(), user, chatRoom, ChatMemberRole.ADMIN); + + chatMemberService.create(member); + + return chatRoom; + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/usecase/ChatRoomUseCase.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/usecase/ChatRoomUseCase.java new file mode 100644 index 000000000..b0ec861d1 --- /dev/null +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/chat/usecase/ChatRoomUseCase.java @@ -0,0 +1,25 @@ +package kr.co.pennyway.api.apis.chat.usecase; + +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; +import kr.co.pennyway.api.apis.chat.mapper.ChatRoomMapper; +import kr.co.pennyway.api.apis.chat.service.ChatRoomSaveService; +import kr.co.pennyway.common.annotation.UseCase; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class ChatRoomUseCase { + private final ChatRoomSaveService chatRoomSaveService; + + public Long pendChatRoom(ChatRoomReq.Pend request, Long userId) { + return chatRoomSaveService.pendChatRoom(request, userId); + } + + public ChatRoomRes.Detail createChatRoom(ChatRoomReq.Create request, Long userId) { + ChatRoom chatRoom = chatRoomSaveService.createChatRoom(request, userId); + + return ChatRoomMapper.toChatRoomResDetail(chatRoom, 1); + } +} diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/InfraConfig.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/InfraConfig.java index 90a3686d1..79b3a5ff0 100644 --- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/InfraConfig.java +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/InfraConfig.java @@ -18,7 +18,8 @@ }) @EnablePennywayInfraConfig({ PennywayInfraConfigGroup.FCM, - PennywayInfraConfigGroup.DISTRIBUTED_COORDINATION_CONFIG + PennywayInfraConfigGroup.DISTRIBUTED_COORDINATION_CONFIG, + PennywayInfraConfigGroup.GUID_GENERATOR_CONFIG }) public class InfraConfig { } diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/SwaggerConfig.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/SwaggerConfig.java index bc17045fc..c011ea9b3 100644 --- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/SwaggerConfig.java +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/config/SwaggerConfig.java @@ -112,6 +112,17 @@ public GroupedOpenApi socketApi() { .build(); } + @Bean + public GroupedOpenApi chatApi() { + String[] targets = {"kr.co.pennyway.api.apis.chat"}; + + return GroupedOpenApi.builder() + .packagesToScan(targets) + .group("채팅") + .addOperationCustomizer(customizer()) + .build(); + } + @Bean public GroupedOpenApi backOfficeApi() { String[] targets = {"kr.co.pennyway.api.apis.question"}; diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomSaveControllerUnitTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomSaveControllerUnitTest.java new file mode 100644 index 000000000..33f847910 --- /dev/null +++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/controller/ChatRoomSaveControllerUnitTest.java @@ -0,0 +1,95 @@ +package kr.co.pennyway.api.apis.chat.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; +import kr.co.pennyway.api.apis.chat.usecase.ChatRoomUseCase; +import kr.co.pennyway.api.config.fixture.ChatRoomFixture; +import kr.co.pennyway.api.config.supporter.WithSecurityMockUser; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.mockito.BDDMockito.given; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = ChatRoomController.class) +@ActiveProfiles("test") +public class ChatRoomSaveControllerUnitTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ChatRoomUseCase chatRoomUseCase; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext) { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .defaultRequest(MockMvcRequestBuilders.get("/**").with(csrf())) + .build(); + } + + @Test + @DisplayName("채팅방 캐싱 성공") + @WithSecurityMockUser + void createChatRoomSuccess() throws Exception { + // given + ChatRoom fixture = ChatRoomFixture.PRIVATE_CHAT_ROOM.toEntity(); + ChatRoomReq.Pend request = new ChatRoomReq.Pend(fixture.getTitle(), fixture.getDescription(), fixture.getPassword()); + + given(chatRoomUseCase.pendChatRoom(request, 1L)).willReturn(1L); + + // when + ResultActions result = performPostChatRoomPending(request); + + // then + result.andDo(print()) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("채팅방 생성 성공") + @WithSecurityMockUser + void createChatRoomSuccess2() throws Exception { + // given + ChatRoom fixture = ChatRoomFixture.PRIVATE_CHAT_ROOM.toEntity(); + ChatRoomReq.Create request = new ChatRoomReq.Create(fixture.getBackgroundImageUrl()); + + given(chatRoomUseCase.createChatRoom(request, 1L)).willReturn(ChatRoomRes.Detail.from(fixture, 1)); + + // when + ResultActions result = performPostChatRoom(request); + + // then + result.andDo(print()) + .andExpect(status().isOk()); + } + + private ResultActions performPostChatRoomPending(ChatRoomReq.Pend request) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.post("/v2/chat-rooms/pend") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))); + } + + private ResultActions performPostChatRoom(ChatRoomReq.Create request) throws Exception { + return mockMvc.perform(MockMvcRequestBuilders.post("/v2/chat-rooms") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))); + } +} diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/integration/ChatRoomPendIntegrationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/integration/ChatRoomPendIntegrationTest.java new file mode 100644 index 000000000..44e462023 --- /dev/null +++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/chat/integration/ChatRoomPendIntegrationTest.java @@ -0,0 +1,108 @@ +package kr.co.pennyway.api.apis.chat.integration; + +import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq; +import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes; +import kr.co.pennyway.api.common.response.SuccessResponse; +import kr.co.pennyway.api.common.security.jwt.access.AccessTokenClaim; +import kr.co.pennyway.api.common.storage.AwsS3Adapter; +import kr.co.pennyway.api.config.ExternalApiDBTestConfig; +import kr.co.pennyway.api.config.ExternalApiIntegrationTest; +import kr.co.pennyway.api.config.fixture.ChatRoomFixture; +import kr.co.pennyway.api.config.fixture.UserFixture; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import kr.co.pennyway.domain.domains.user.domain.User; +import kr.co.pennyway.domain.domains.user.service.UserService; +import kr.co.pennyway.infra.client.aws.s3.ObjectKeyType; +import kr.co.pennyway.infra.common.jwt.JwtProvider; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import java.util.Map; + +import static org.mockito.BDDMockito.given; + + +@Slf4j +@ExternalApiIntegrationTest +public class ChatRoomPendIntegrationTest extends ExternalApiDBTestConfig { + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private UserService userService; + + @Autowired + private JwtProvider accessTokenProvider; + + @MockBean + private AwsS3Adapter awsS3Adapter; + + @LocalServerPort + private int port; + + @Test + @DisplayName("사용자는 채팅방 생성에 성공한다.") + void success() { + // given + User user = userService.createUser(UserFixture.GENERAL_USER.toUser()); + ChatRoom room = ChatRoomFixture.PRIVATE_CHAT_ROOM.toEntity(); + ChatRoomReq.Pend request = new ChatRoomReq.Pend(room.getTitle(), room.getDescription(), room.getPassword()); + ChatRoomReq.Create request2 = new ChatRoomReq.Create(room.getBackgroundImageUrl()); + given(awsS3Adapter.saveImage(room.getBackgroundImageUrl(), ObjectKeyType.CHAT_PROFILE)).willReturn("chatroom/1"); + + // when + ResponseEntity>> response = postPending(user, request); + ResponseEntity>> response2 = postCreating(user, request2); + ChatRoomRes.Detail detail = response2.getBody().getData().get("chatRoom"); + + // then + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode(), "201 Created 응답을 받아야 합니다."); + Assertions.assertEquals(request.title(), detail.title(), "생성된 채팅방의 제목이 일치해야 합니다."); + Assertions.assertEquals(request.description(), detail.description(), "생성된 채팅방의 설명이 일치해야 합니다."); + Assertions.assertTrue(detail.isPrivate(), "생성된 채팅방은 비공개여야 합니다."); + } + + private ResponseEntity>> postPending(User user, ChatRoomReq.Pend request) { + return restTemplate.exchange( + "http://localhost:" + port + "/v2/chat-rooms/pend", + HttpMethod.POST, + createHttpEntity(user, request), + new ParameterizedTypeReference<>() { + } + ); + } + + private ResponseEntity>> postCreating(User user, ChatRoomReq.Create request) { + return restTemplate.exchange( + "http://localhost:" + port + "/v2/chat-rooms", + HttpMethod.POST, + createHttpEntity(user, request), + new ParameterizedTypeReference<>() { + } + ); + } + + private HttpEntity createHttpEntity(User user, ChatRoomReq.Pend request) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessTokenProvider.generateToken(AccessTokenClaim.of(user.getId(), user.getRole().name()))); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new HttpEntity<>(request, headers); + } + + private HttpEntity createHttpEntity(User user, ChatRoomReq.Create request) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessTokenProvider.generateToken(AccessTokenClaim.of(user.getId(), user.getRole().name()))); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new HttpEntity<>(request, headers); + } +} diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/integration/SpendingControllerIntegrationTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/integration/SpendingControllerIntegrationTest.java index e81d254c9..55f0c2100 100644 --- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/integration/SpendingControllerIntegrationTest.java +++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/integration/SpendingControllerIntegrationTest.java @@ -58,7 +58,7 @@ public class SpendingControllerIntegrationTest extends ExternalApiDBTestConfig { @Order(1) @Nested @DisplayName("지출 내역 추가하기") - class CreateSpending { + class PendSpending { @Test @DisplayName("request의 categoryId가 -1인 경우, spendingCustomCategory가 null인 Spending을 생성한다.") @Transactional diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/config/fixture/ChatRoomFixture.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/config/fixture/ChatRoomFixture.java new file mode 100644 index 000000000..16e591a3f --- /dev/null +++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/config/fixture/ChatRoomFixture.java @@ -0,0 +1,30 @@ +package kr.co.pennyway.api.config.fixture; + +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; + +public enum ChatRoomFixture { + PRIVATE_CHAT_ROOM("페니웨이", "페니웨이 채팅방입니다.", "delete/chatroom/1/fsdflasdfa_12121210.jpg", 123456), + PUBLIC_CHAT_ROOM("페니웨이", "페니웨이 채팅방입니다.", "delete/chatroom/1/fsdflasdfa_12121210.jpg", null); + + private final String title; + private final String description; + private final String backgroundImageUrl; + private final Integer password; + + ChatRoomFixture(String title, String description, String backgroundImageUrl, Integer password) { + this.title = title; + this.description = description; + this.backgroundImageUrl = backgroundImageUrl; + this.password = password; + } + + public ChatRoom toEntity() { + return ChatRoom.builder() + .id(1L) + .title(title) + .description(description) + .backgroundImageUrl(backgroundImageUrl) + .password(password) + .build(); + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/converter/ChatMemberRoleConverter.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/converter/ChatMemberRoleConverter.java new file mode 100644 index 000000000..17724ad2f --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/converter/ChatMemberRoleConverter.java @@ -0,0 +1,13 @@ +package kr.co.pennyway.domain.common.converter; + +import jakarta.persistence.Converter; +import kr.co.pennyway.domain.domains.member.type.ChatMemberRole; + +@Converter +public class ChatMemberRoleConverter extends AbstractLegacyEnumAttributeConverter { + private static final String ENUM_NAME = "채팅방 멤버 역할"; + + public ChatMemberRoleConverter() { + super(ChatMemberRole.class, false, ENUM_NAME); + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoom.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoom.java new file mode 100644 index 000000000..d248969b3 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoom.java @@ -0,0 +1,74 @@ +package kr.co.pennyway.domain.common.redis.chatroom; + +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.util.StringUtils; + +import java.util.Objects; + +@Getter +@RedisHash(value = "pendedChatRoom", timeToLive = 5) +public class PendedChatRoom { + @Id + private final Long userId; + private final Long chatRoomId; + private final String title; + private final String description; + private final Integer password; + + private PendedChatRoom(Long chatRoomId, Long userId, String title, String description, Integer password) { + validate(chatRoomId, userId, title, description, password); + this.chatRoomId = chatRoomId; + this.userId = userId; + this.title = title; + this.description = description; + this.password = password; + } + + public static PendedChatRoom of(Long chatRoomId, Long userId, String title, String description, Integer password) { + return new PendedChatRoom(chatRoomId, userId, title, description, password); + } + + private void validate(Long id, Long userId, String title, String description, Integer password) { + Objects.requireNonNull(id, "채팅방 ID는 null일 수 없습니다."); + Objects.requireNonNull(userId, "사용자 ID는 null일 수 없습니다."); + + if (!StringUtils.hasText(title) || title.length() > 50) { + throw new IllegalArgumentException("제목은 null이거나 빈 문자열이 될 수 없으며, 50자 이하로 제한됩니다."); + } + + if (description != null && description.length() > 100) { + throw new IllegalArgumentException("설명은 null이거나 빈 문자열이 될 수 있으며, 100자 이하로 제한됩니다."); + } + + if (password != null && password < 0 && password.toString().length() != 6) { + throw new IllegalArgumentException("비밀번호는 null이거나, 6자리 정수여야 하며, 음수는 허용하지 않습니다."); + } + } + + public ChatRoom toChatRoom(String backgroundImageUrl) { + return ChatRoom.builder() + .id(chatRoomId) + .title(title) + .description(description) + .backgroundImageUrl(backgroundImageUrl) + .password(password) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PendedChatRoom that)) return false; + return userId.equals(that.userId) && chatRoomId.equals(that.chatRoomId); + } + + @Override + public int hashCode() { + int result = userId.hashCode(); + result = ((1 << 5) - 1) * result + chatRoomId.hashCode(); + return result; + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorCode.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorCode.java new file mode 100644 index 000000000..d32670198 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorCode.java @@ -0,0 +1,28 @@ +package kr.co.pennyway.domain.common.redis.chatroom; + +import kr.co.pennyway.common.exception.BaseErrorCode; +import kr.co.pennyway.common.exception.CausedBy; +import kr.co.pennyway.common.exception.ReasonCode; +import kr.co.pennyway.common.exception.StatusCode; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum PendedChatRoomErrorCode implements BaseErrorCode { + // 404 NOT_FOUND + NOT_FOUND(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "채팅방 정보를 찾을 수 없습니다."), + ; + + private final StatusCode statusCode; + private final ReasonCode reasonCode; + private final String message; + + @Override + public CausedBy causedBy() { + return CausedBy.of(statusCode, reasonCode); + } + + @Override + public String getExplainError() throws NoSuchFieldError { + return message; + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorException.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorException.java new file mode 100644 index 000000000..582c0bd2f --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomErrorException.java @@ -0,0 +1,21 @@ +package kr.co.pennyway.domain.common.redis.chatroom; + +import kr.co.pennyway.common.exception.CausedBy; +import kr.co.pennyway.common.exception.GlobalErrorException; + +public class PendedChatRoomErrorException extends GlobalErrorException { + private final PendedChatRoomErrorCode pendedChatRoomErrorCode; + + public PendedChatRoomErrorException(PendedChatRoomErrorCode pendedChatRoomErrorCode) { + super(pendedChatRoomErrorCode); + this.pendedChatRoomErrorCode = pendedChatRoomErrorCode; + } + + public CausedBy causedBy() { + return pendedChatRoomErrorCode.causedBy(); + } + + public String getExplainError() { + return pendedChatRoomErrorCode.getExplainError(); + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomRepository.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomRepository.java new file mode 100644 index 000000000..fe53e26f3 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomRepository.java @@ -0,0 +1,6 @@ +package kr.co.pennyway.domain.common.redis.chatroom; + +import org.springframework.data.repository.CrudRepository; + +public interface PendedChatRoomRepository extends CrudRepository { +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomService.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomService.java new file mode 100644 index 000000000..0d1c7a310 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/common/redis/chatroom/PendedChatRoomService.java @@ -0,0 +1,23 @@ +package kr.co.pennyway.domain.common.redis.chatroom; + +import kr.co.pennyway.common.annotation.DomainService; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +@Slf4j +@DomainService +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class PendedChatRoomService { + private final PendedChatRoomRepository pendedChatRoomRepository; + + public PendedChatRoom create(PendedChatRoom pendedChatRoom) { + return pendedChatRoomRepository.save(pendedChatRoom); + } + + public Optional readByUserId(Long userId) { + return pendedChatRoomRepository.findById(userId); + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/domain/ChatRoom.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/domain/ChatRoom.java index 965b03a4f..ff98f28c9 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/domain/ChatRoom.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/domain/ChatRoom.java @@ -1,6 +1,8 @@ package kr.co.pennyway.domain.domains.chatroom.domain; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import kr.co.pennyway.domain.common.model.DateAuditable; import lombok.AccessLevel; import lombok.Builder; @@ -13,6 +15,7 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; +import java.util.Objects; @Entity @Getter @@ -23,7 +26,6 @@ @SQLDelete(sql = "UPDATE chat_room SET deleted_at = NOW() WHERE id = ?") public class ChatRoom extends DateAuditable { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @@ -35,9 +37,10 @@ public class ChatRoom extends DateAuditable { private LocalDateTime deletedAt; @Builder - public ChatRoom(String title, String description, String backgroundImageUrl, Integer password) { - validate(title, description, password); + public ChatRoom(Long id, String title, String description, String backgroundImageUrl, Integer password) { + validate(id, title, description, password); + this.id = id; this.title = title; this.description = description; this.backgroundImageUrl = backgroundImageUrl; @@ -53,6 +56,12 @@ public void update(String title, String description, String backgroundImageUrl, this.password = password; } + private void validate(Long id, String title, String description, Integer password) { + Objects.requireNonNull(id, "채팅방 ID는 null일 수 없습니다."); + + validate(title, description, password); + } + private void validate(String title, String description, Integer password) { if (!StringUtils.hasText(title) || title.length() > 50) { throw new IllegalArgumentException("제목은 null이거나 빈 문자열이 될 수 없으며, 50자 이하로 제한됩니다."); diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/repository/ChatRoomRepository.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/repository/ChatRoomRepository.java new file mode 100644 index 000000000..c1f44fd85 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/repository/ChatRoomRepository.java @@ -0,0 +1,7 @@ +package kr.co.pennyway.domain.domains.chatroom.repository; + +import kr.co.pennyway.domain.common.repository.ExtendedRepository; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; + +public interface ChatRoomRepository extends ExtendedRepository { +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/service/ChatRoomService.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/service/ChatRoomService.java new file mode 100644 index 000000000..83f244977 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/chatroom/service/ChatRoomService.java @@ -0,0 +1,20 @@ +package kr.co.pennyway.domain.domains.chatroom.service; + +import kr.co.pennyway.common.annotation.DomainService; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import kr.co.pennyway.domain.domains.chatroom.repository.ChatRoomRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@DomainService +@RequiredArgsConstructor +public class ChatRoomService { + private final ChatRoomRepository chatRoomRepository; + + @Transactional + public ChatRoom create(ChatRoom chatRoom) { + return chatRoomRepository.save(chatRoom); + } +} diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/domain/ChatMember.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/domain/ChatMember.java index 17e8d0953..dfb704e46 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/domain/ChatMember.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/domain/ChatMember.java @@ -1,8 +1,10 @@ package kr.co.pennyway.domain.domains.member.domain; import jakarta.persistence.*; +import kr.co.pennyway.domain.common.converter.ChatMemberRoleConverter; import kr.co.pennyway.domain.common.model.DateAuditable; import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import kr.co.pennyway.domain.domains.member.type.ChatMemberRole; import kr.co.pennyway.domain.domains.user.domain.User; import lombok.AccessLevel; import lombok.Builder; @@ -30,7 +32,9 @@ public class ChatMember extends DateAuditable { private Long id; private String name; - private String profileImageUrl; + + @Convert(converter = ChatMemberRoleConverter.class) + private ChatMemberRole role; @ColumnDefault("false") private boolean banned; @@ -49,22 +53,32 @@ public class ChatMember extends DateAuditable { private ChatRoom chatRoom; @Builder - public ChatMember(String name, String profileImageUrl, User user, ChatRoom chatRoom) { - validate(name, user, chatRoom); + public ChatMember(String name, User user, ChatRoom chatRoom, ChatMemberRole role) { + validate(name, user, chatRoom, role); this.name = name; - this.profileImageUrl = profileImageUrl; this.user = user; this.chatRoom = chatRoom; + this.role = role; + } + + public static ChatMember of(String name, User user, ChatRoom chatRoom, ChatMemberRole role) { + return ChatMember.builder() + .name(name) + .user(user) + .chatRoom(chatRoom) + .role(role) + .build(); } - private void validate(String name, User user, ChatRoom chatRoom) { + private void validate(String name, User user, ChatRoom chatRoom, ChatMemberRole role) { if (!StringUtils.hasText(name)) { throw new IllegalArgumentException("name은 null이거나 빈 문자열이 될 수 없습니다."); } Objects.requireNonNull(user, "user는 null이 될 수 없습니다."); Objects.requireNonNull(chatRoom, "chatRoom은 null이 될 수 없습니다."); + Objects.requireNonNull(role, "role은 null이 될 수 없습니다."); } @Override @@ -72,7 +86,6 @@ public String toString() { return "ChatMember{" + "id=" + id + ", name='" + name + '\'' + - ", profileImageUrl='" + profileImageUrl + '\'' + ", banned=" + banned + ", notifyEnabled=" + notifyEnabled + ", deletedAt=" + deletedAt + diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java index 6f42333de..d7fb8840d 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java @@ -1,6 +1,7 @@ package kr.co.pennyway.domain.domains.member.service; import kr.co.pennyway.common.annotation.DomainService; +import kr.co.pennyway.domain.domains.member.domain.ChatMember; import kr.co.pennyway.domain.domains.member.repository.ChatMemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,6 +13,11 @@ public class ChatMemberService { private final ChatMemberRepository chatMemberRepository; + @Transactional + public ChatMember create(ChatMember chatMember) { + return chatMemberRepository.save(chatMember); + } + @Transactional(readOnly = true) public boolean isExists(Long chatRoomId, Long userId) { return chatMemberRepository.existsByChatRoomIdAndUserId(chatRoomId, userId); diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/type/ChatMemberRole.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/type/ChatMemberRole.java new file mode 100644 index 000000000..e5dbcd565 --- /dev/null +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/type/ChatMemberRole.java @@ -0,0 +1,29 @@ +package kr.co.pennyway.domain.domains.member.type; + +import com.fasterxml.jackson.annotation.JsonValue; +import kr.co.pennyway.domain.common.converter.LegacyCommonType; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ChatMemberRole implements LegacyCommonType { + ADMIN("0", "ADMIN"), + MEMBER("1", "MEMBER");; + + private final String code; + private final String type; + + @Override + public String getCode() { + return code; + } + + @JsonValue + public String getType() { + return type; + } + + @Override + public String toString() { + return type; + } +}