Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api: ✨ 채팅방 생성 API #173

Merged
merged 42 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ad32850
feat: 각 모듈별 docker파일 분리
psychology50 Oct 12, 2024
d87f73e
fix: chat_member 엔티티 image_url 제거
psychology50 Oct 12, 2024
7011782
feat: 채팅방 생성 repo & service 정의
psychology50 Oct 12, 2024
6dfa61d
feat: 채팅 멤버 생성 service 정의
psychology50 Oct 12, 2024
d8d5050
test: 채팅방 생성 컨트롤러 단위 테스트 성공 시나리오 작성
psychology50 Oct 12, 2024
4947706
feat: chat_room_request dto 정의
psychology50 Oct 12, 2024
c62c2b9
feat: chat_room craete controller
psychology50 Oct 12, 2024
a000def
feat: chat_room_usecase 및 request & response schema 추가
psychology50 Oct 12, 2024
5bf727f
fix: controller success response 추가
psychology50 Oct 12, 2024
112bde8
test: chat_room_fixture
psychology50 Oct 12, 2024
cb820e2
fix: chat_room_res detail dto to_string 수정
psychology50 Oct 12, 2024
6e1b5ff
fix: controller 내 usecase final 키워드 추가
psychology50 Oct 12, 2024
3234101
feat: 채팅방 역할 상수 정의
psychology50 Oct 12, 2024
cde0640
fix: chat_memeber role 필드 추가
psychology50 Oct 12, 2024
dc0e6aa
fix: chat_room create dto to_entity 메서드 static 제거
psychology50 Oct 12, 2024
706cbce
feat: chat_member entity 정적 팩토리 메서드 추가
psychology50 Oct 12, 2024
f572c4d
feat: 채팅방 생성 서비스 로직 구현
psychology50 Oct 12, 2024
f4299f2
feat: chat_room_mapper 정의
psychology50 Oct 12, 2024
4871cc9
test: 채팅방 생성 통합 테스트 구현
psychology50 Oct 12, 2024
1fb4881
fix: 채팅방 생성 요청 background_image_url pattern 수정
psychology50 Oct 12, 2024
5e09fea
feat: 채팅방 생성 요청 캐싱 entity
psychology50 Oct 12, 2024
8a15943
feat: 생성 지연 채팅방 서비스 로직
psychology50 Oct 12, 2024
bbdc5c7
fix: pended_chat_room user_id 필드 추가 및 유효성 검사 & to_chat_room 메서드 추가
psychology50 Oct 12, 2024
8db2c5d
fix: chat_room_req pend, create 요청 분리
psychology50 Oct 12, 2024
b87edaa
fix: 캐싱된 채팅방 정보 예외 정의
psychology50 Oct 12, 2024
6db4e27
fix: 채팅방 생성 서비스 로직 대기, 생성 메서드 분리
psychology50 Oct 12, 2024
0518dcb
feat: external-api 모듈 guid config 의존성 추가
psychology50 Oct 12, 2024
e5f97a0
fix: pended_chat_room hash code 수정
psychology50 Oct 12, 2024
36ef597
fix: 사용자 아이디로 캐싱된 정보 조회 -> 사용자 권한 예외 제거 가능하므로 삭제
psychology50 Oct 12, 2024
d1120c0
fix: controller 및 usecase 요청 분리
psychology50 Oct 12, 2024
8e3f549
test: controller 단위 테스트 pend, create 분리
psychology50 Oct 12, 2024
1f8c9d0
feat: 채팅방 entity pk auto_increment 속성 제거
psychology50 Oct 12, 2024
f6e0481
test: chat_room_fixture url 정보 수정
psychology50 Oct 12, 2024
98c1933
fix: 캐싱된 채팅방 정보 로깅 추가
psychology50 Oct 12, 2024
756fdc9
test: 채팅방 생성 시나리오 테스트 수정
psychology50 Oct 12, 2024
3c6b644
fix: 채팅방 멤버 생성 시, entity 반환하도록 chat_member_service 로직 수정
psychology50 Oct 12, 2024
bd9ae14
feat: 채팅방 저장 시, image url 변환 작업 수행 로직 추가
psychology50 Oct 12, 2024
b16cd4f
test: aws_adapter mock bean 설정 추가
psychology50 Oct 12, 2024
2f5488c
feat: 채팅방 swagger group 등록
psychology50 Oct 12, 2024
f690a47
docs: 채팅방 생성 대기 및 확정 api 스웨거 문서 작성
psychology50 Oct 12, 2024
b20b668
chore: 불필요한 dockerfile 모두 제거
psychology50 Oct 12, 2024
f8bc393
chore: socket-relay dockerfile 복구
psychology50 Oct 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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())));
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Long> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
})
@EnablePennywayInfraConfig({
PennywayInfraConfigGroup.FCM,
PennywayInfraConfigGroup.DISTRIBUTED_COORDINATION_CONFIG
PennywayInfraConfigGroup.DISTRIBUTED_COORDINATION_CONFIG,
PennywayInfraConfigGroup.GUID_GENERATOR_CONFIG
})
public class InfraConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"};
Expand Down
Loading
Loading