Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d810a1
[feat] 파라미터로 들어오는 방 참여/취소 종류 상수화 (#84)
buzz0331 Jul 18, 2025
171fe97
[feat] 필요한 dto 선언 (#84)
buzz0331 Jul 18, 2025
032a4de
[feat] 필요한 interface(usecase, port) 선언 (#84)
buzz0331 Jul 18, 2025
e88c948
[feat] Room에 멤버 수 관련 도메인 규칙 추가 (#84)
buzz0331 Jul 18, 2025
17dea36
[feat] RoomParticipant 생성 메서드 추가 (#84)
buzz0331 Jul 18, 2025
73bf174
[feat] RoomParticipantJpaRepository 추가 (#84)
buzz0331 Jul 18, 2025
9b7b891
[feat] RoomParticipant 존재 여부 쿼리 (#84)
buzz0331 Jul 18, 2025
db8757c
[feat] 방 참여 역할 type final (#84)
buzz0331 Jul 18, 2025
82989e9
[feat] RoomParticipant에 Soft Delete 적용 (#84)
buzz0331 Jul 18, 2025
88fb7b0
[feat] insert 쿼리 및 삭제 쿼리 추가 (#84)
buzz0331 Jul 18, 2025
8b0ad9c
[feat] RoomJpaEntity 멤버수 업데이트 메서드 (#84)
buzz0331 Jul 18, 2025
c828d04
[feat] RoomJpaEntity 멤버수 업데이트 쿼리 (#84)
buzz0331 Jul 18, 2025
ef01f18
[feat] 방 참여/취소 관련 예외코드 추가 (#84)
buzz0331 Jul 18, 2025
7fc67a3
[feat] 방 참여/취소 api 핸들러 추가 (#84)
buzz0331 Jul 18, 2025
bbfd174
[feat] 방 참여/취소 api 서비스 추가 (#84)
buzz0331 Jul 18, 2025
539ced9
[test] 방 생성 통합 테스트 displayname 수정 (#84)
buzz0331 Jul 18, 2025
467af61
[test] 방 참여/취소 api 통합 테스트 작성 (#84)
buzz0331 Jul 18, 2025
82f93ce
[test] 방 참여/취소 서비스 단위 테스트 작성 (#84)
buzz0331 Jul 18, 2025
57587f2
[test] Room 도메인 규칙 단위 테스트 작성 (#84)
buzz0331 Jul 18, 2025
7b134dc
[test] Book 생성시 ISBN 랜덤 UUID 갖도록 수정 (unique 속성) (#84)
buzz0331 Jul 18, 2025
fa96e99
[refactor] 사용되지 않는 count 쿼리 제거 (followerCount로 대체됨) (#84)
buzz0331 Jul 18, 2025
15e1a92
[merge] develop (#84)
buzz0331 Jul 18, 2025
4ade6e4
[fix] native query이므로 boolean이 아닌 Long을 반환받도록 수정 (#84)
buzz0331 Jul 18, 2025
dc844b2
[fix] 방 만료 여부 검증 추가 (#84)
buzz0331 Jul 18, 2025
e4b5b01
[fix] BaseResponse 제너릭 타입 Void로 수정 (#84)
buzz0331 Jul 18, 2025
46bf492
[fix] exist 쿼리 1 반환에 따른 테스트 코드 수정 (#84)
buzz0331 Jul 18, 2025
35d1baa
[refactor] statuc 필터링 포함해서 네이티브 쿼리로 변경 (#84)
buzz0331 Jul 18, 2025
f4b4130
[refactor] statuc 필터링 포함해서 네이티브 쿼리로 변경 (#84)
buzz0331 Jul 18, 2025
e30c759
[fix] RoomParticipant 소프트 딜리트 적용으로 인한 통합 테스트 시에 deleteAll() 메서드 대신 de…
buzz0331 Jul 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public enum ErrorCode implements ResponseCode {
ROOM_PASSWORD_NOT_REQUIRED(HttpStatus.BAD_REQUEST, 100003, "공개방은 비밀번호가 필요하지 않습니다."),
ROOM_RECRUITMENT_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, 100004, "모집기간이 만료된 방입니다."),
INVALID_ROOM_SEARCH_SORT(HttpStatus.BAD_REQUEST, 100005, "방 검색 시 정렬 조건이 잘못되었습니다."),
ROOM_MEMBER_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, 100006, "방의 최대 인원 수를 초과했습니다."),
ROOM_MEMBER_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 100007, "방의 인원 수가 1 이하(방장 포함)입니다."),

/**
* 110000 : vote error
Expand Down Expand Up @@ -109,6 +111,11 @@ public enum ErrorCode implements ResponseCode {
ROOM_PARTICIPANT_NOT_FOUND(HttpStatus.NOT_FOUND, 140000, "존재하지 않는 RoomParticipant (방과 사용자 관계) 입니다."),
USER_NOT_BELONG_TO_ROOM(HttpStatus.BAD_REQUEST, 140001, "현재 모임방에 속하지 않는 유저입니다."),
ROOM_PARTICIPANT_ROLE_NOT_MATCH(HttpStatus.BAD_REQUEST, 140002, "일치하는 방에서의 사용자 역할이 없습니다."),
ROOM_JOIN_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, 140003, "일치하는 방 참여 상태가 없습니다."),
USER_CANNOT_JOIN_OR_CANCEL(HttpStatus.BAD_REQUEST, 140004, "존재하지 않는 방은 참여하기 또는 취소하기가 불가능합니다."),
USER_ALREADY_PARTICIPATE(HttpStatus.BAD_REQUEST, 140005, "사용자가 이미 방에 참여한 상태입니다."),
USER_NOT_PARTICIPATED(HttpStatus.BAD_REQUEST, 140006, "사용자가 방에 참여하지 않은 상태에서 취소하기는 불가합니다."),
HOST_CANNOT_CANCEL(HttpStatus.BAD_REQUEST, 140007, "방장은 참여 취소를 할 수 없습니다."),

/**
* 150000 : Category error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import konkuk.thip.room.adapter.in.web.request.RoomCreateRequest;
import konkuk.thip.room.adapter.in.web.request.RoomJoinRequest;
import konkuk.thip.room.adapter.in.web.response.RoomCreateResponse;
import konkuk.thip.room.application.port.in.RoomCreateUseCase;
import konkuk.thip.room.application.port.in.RoomJoinUsecase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -16,11 +19,27 @@
public class RoomCommandController {

private final RoomCreateUseCase roomCreateUseCase;
private final RoomJoinUsecase roomJoinUsecase;

/**
* 방 생성 요청
*/
Comment on lines +24 to +26

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@PostMapping("/rooms")
public BaseResponse<RoomCreateResponse> createRoom(@Valid @RequestBody RoomCreateRequest request, @UserId Long userId) {
return BaseResponse.ok(RoomCreateResponse.of(
roomCreateUseCase.createRoom(request.toCommand(), userId)
));
}

/**
* 방 참여하기/취소하기 요청
*/
@PostMapping("/rooms/{roomId}/join")
public BaseResponse<Void> joinRoom(@Valid @RequestBody final RoomJoinRequest request,
@UserId final Long userId,
@PathVariable final Long roomId) {

roomJoinUsecase.changeJoinState(request.toCommand(userId, roomId));
return BaseResponse.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package konkuk.thip.room.adapter.in.web.request;

import jakarta.validation.constraints.NotBlank;
import konkuk.thip.room.application.port.in.dto.RoomJoinCommand;

public record RoomJoinRequest(
@NotBlank(message = "방 참여 유형 파라미터는 필수입니다..")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. 오타있어요!

String type
) {
public RoomJoinCommand toCommand(Long userId, Long roomId) {
return new RoomJoinCommand(userId, roomId, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ public class RoomJpaEntity extends BaseJpaEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private CategoryJpaEntity categoryJpaEntity;

public void updateMemberCount(int count) {
this.memberCount = count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import konkuk.thip.common.entity.BaseJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import lombok.*;
import org.hibernate.annotations.SQLDelete;

@Entity
@Table(name = "room_participants")
@Getter
@SQLDelete(sql = "UPDATE room_participants SET status = 'INACTIVE' WHERE room_participant_id = ?")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

소프트 삭제 구현이 올바르게 적용되었습니다

@SQLDelete 어노테이션을 통해 물리적 삭제 대신 status를 'INACTIVE'로 변경하는 소프트 삭제 방식이 구현되었습니다. 하지만 기존 조회 쿼리들이 활성 상태의 참가자만 조회하도록 수정되었는지 확인이 필요합니다.

다음 스크립트로 기존 쿼리들이 status 필터링을 올바르게 처리하는지 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# 설명: RoomParticipant 관련 쿼리들이 status 필터링을 올바르게 처리하는지 확인

# RoomParticipant 관련 repository 메서드들 검색
rg -A 10 "findBy.*RoomParticipant|existBy.*RoomParticipant" --type java

# Native query에서 status 조건 처리 확인
rg -A 5 -B 5 "SELECT.*room_participants|FROM.*room_participants" --type java

Length of output: 2928


조회 메서드에 상태 필터 추가 필요

소프트 삭제(@SQLDelete)는 올바르게 적용되었으나, 아래 조회 로직에 status = 'ACTIVE' 필터가 빠져 있어 비활성(INACTIVE) 참가자까지 조회될 수 있습니다.

– src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java
findByUserIdAndRoomId
- 기존
java @Query(value = "SELECT * FROM room_participants WHERE user_id = :userId AND room_id = :roomId", nativeQuery = true) Optional<RoomParticipantJpaEntity> findByUserIdAndRoomId(...);
- 수정 제안
java @Query(value = "SELECT * FROM room_participants WHERE user_id = :userId AND room_id = :roomId AND status = 'ACTIVE'", nativeQuery = true)

findAllByRoomJpaEntity_RoomId
- 파생 쿼리에 상태 조건이 누락되어 있습니다.
- 방법 1: 메서드 시그니처를 findAllByRoomJpaEntity_RoomIdAndStatus(Long roomId, Status status)로 변경
- 방법 2: 엔티티에 @Where(clause = "status = 'ACTIVE'")를 추가하여 전역 필터 적용

existByUserIdAndRoomId는 이미 ... AND rp.status = 'ACTIVE' 조건이 포함되어 있어 추가 변경이 필요 없습니다.
위 두 조회 로직에 활성 상태 필터를 반영해 주세요.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java
around the methods findByUserIdAndRoomId and findAllByRoomJpaEntity_RoomId, the
queries lack a filter for status = 'ACTIVE', causing inactive participants to be
included in results. To fix this, update the findByUserIdAndRoomId query to add
"AND status = 'ACTIVE'" in the WHERE clause, and for
findAllByRoomJpaEntity_RoomId, either modify the method signature to include a
status parameter and filter by it or add @Where(clause = "status = 'ACTIVE'")
annotation on the entity to globally filter by active status. No changes are
needed for existByUserIdAndRoomId as it already includes the status filter.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public enum RoomParticipantRole {
HOST("호스트"),
MEMBER("팀원");

private String type;
private final String type;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻


RoomParticipantRole(String type) {
this.type = type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,15 @@ public Category findCategoryByValue(String value) {
() -> new EntityNotFoundException(CATEGORY_NOT_FOUND));
return Category.from(categoryJpaEntity.getValue());
}

@Override
public void updateMemberCount(Room room) {
RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(room.getId()).orElseThrow(
() -> new EntityNotFoundException(ROOM_NOT_FOUND)
);

roomJpaEntity.updateMemberCount(room.getMemberCount());

roomJpaRepository.save(roomJpaEntity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,36 @@

import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.common.exception.code.ErrorCode;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity;
import konkuk.thip.room.adapter.out.mapper.RoomParticipantMapper;
import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository;
import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.domain.RoomParticipant;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND;
import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND;

@Repository
@RequiredArgsConstructor
public class RoomParticipantCommandPersistenceAdapter implements RoomParticipantCommandPort {

private final RoomParticipantJpaRepository roomParticipantJpaRepository;
private final RoomParticipantMapper roomParticipantMapper;

private final UserJpaRepository userJpaRepository;
private final RoomJpaRepository roomJpaRepository;

@Override
public RoomParticipant findByUserIdAndRoomId(Long userId, Long roomId) {
RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findByUserJpaEntity_UserIdAndRoomJpaEntity_RoomId(userId, roomId).orElseThrow(
RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findByUserIdAndRoomId(userId, roomId).orElseThrow(
() -> new EntityNotFoundException(ErrorCode.ROOM_PARTICIPANT_NOT_FOUND)
);

Expand All @@ -30,8 +40,31 @@ public RoomParticipant findByUserIdAndRoomId(Long userId, Long roomId) {

@Override
public List<RoomParticipant> findAllByRoomId(Long roomId) {
return roomParticipantJpaRepository.findAllByRoomJpaEntity_RoomId(roomId).stream()
return roomParticipantJpaRepository.findAllByRoomId(roomId).stream()
.map(roomParticipantMapper::toDomainEntity)
.toList();
}

@Override
public void save(RoomParticipant roomParticipant) {
UserJpaEntity userJpaEntity = userJpaRepository.findById(roomParticipant.getUserId()).orElseThrow(
() -> new EntityNotFoundException(USER_NOT_FOUND));

RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(roomParticipant.getRoomId()).orElseThrow(
() -> new EntityNotFoundException(ROOM_NOT_FOUND)
);

roomParticipantJpaRepository.save(roomParticipantMapper.toJpaEntity(
roomParticipant, userJpaEntity, roomJpaEntity
));
}

@Override
public void deleteByUserIdAndRoomId(Long userId, Long roomId) {
RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findByUserIdAndRoomId(userId, roomId).orElseThrow(
() -> new EntityNotFoundException(ErrorCode.ROOM_PARTICIPANT_NOT_FOUND)
);

roomParticipantJpaRepository.delete(roomParticipantJpaEntity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package konkuk.thip.room.adapter.out.persistence;

import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository;
import konkuk.thip.room.application.port.out.RoomParticipantQueryPort;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class RoomParticipantQueryPersistenceAdapter implements RoomParticipantQueryPort {

private final RoomParticipantJpaRepository roomParticipantJpaRepository;

@Override
public boolean existByUserIdAndRoomId(Long userId, Long roomId) {
return roomParticipantJpaRepository.existByUserIdAndRoomId(userId, roomId);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@

import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity;
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 RoomParticipantJpaRepository extends JpaRepository<RoomParticipantJpaEntity, Long>{
public interface RoomParticipantJpaRepository extends JpaRepository<RoomParticipantJpaEntity, Long>, RoomParticipantQueryRepository{

@Query(value = "SELECT * FROM room_participants WHERE user_id = :userId AND room_id = :roomId AND status = 'ACTIVE'", nativeQuery = true)
Optional<RoomParticipantJpaEntity> findByUserIdAndRoomId(@Param("userId") Long userId, @Param("roomId") Long roomId);

@Query(value = "SELECT * FROM room_participants WHERE room_id = :roomId AND status = 'ACTIVE'", nativeQuery = true)
List<RoomParticipantJpaEntity> findAllByRoomId(Long roomId);

@Query(
value = "SELECT EXISTS (SELECT 1 FROM room_participants rp WHERE rp.user_id = :userId AND rp.room_id = :roomId AND rp.status = 'ACTIVE')",
nativeQuery = true
)
boolean existByUserIdAndRoomId(@Param("userId") Long userId, @Param("roomId") Long roomId);

Optional<RoomParticipantJpaEntity> findByUserJpaEntity_UserIdAndRoomJpaEntity_RoomId(Long userId, Long roomId);
List<RoomParticipantJpaEntity> findAllByRoomJpaEntity_RoomId(Long roomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package konkuk.thip.room.adapter.out.persistence.repository.roomparticipant;

public interface RoomParticipantQueryRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package konkuk.thip.room.adapter.out.persistence.repository.roomparticipant;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class RoomParticipantQueryRepositoryImpl implements RoomParticipantQueryRepository{

}
Comment on lines +1 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

빈 구현체 클래스의 필요성을 확인해주세요.

현재 RoomParticipantQueryRepositoryImpl가 비어있는 구현체입니다. 커스텀 쿼리 메서드를 추가할 계획이 없다면 불필요한 클래스일 수 있습니다.

다음 스크립트로 이 인터페이스가 어떻게 사용되는지 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# RoomParticipantQueryRepository 인터페이스 사용 패턴 확인
ast-grep --pattern $'interface RoomParticipantQueryRepository {
  $$$
}'

# 구현체에서 실제로 구현해야 할 메서드가 있는지 확인
rg -A 5 "RoomParticipantQueryRepository"

Length of output: 2020


RoomParticipantQueryRepositoryImpl 빈 구현체 제거 또는 커스텀 쿼리 정의 필요

RoomParticipantQueryRepository 인터페이스가 메서드를 하나도 선언하지 않아, 현재 빈 구현체인 RoomParticipantQueryRepositoryImpl는 실제로 사용되지 않습니다.
커스텀 쿼리를 구현할 계획이 없다면 해당 인터페이스와 구현체를 제거하고, RoomParticipantJpaRepository만 사용하도록 정리해주세요.
만약 향후 커스텀 쿼리가 필요하다면, 인터페이스에 메서드를 선언한 뒤 구현체에 로직을 추가하는 방향으로 유지하시면 됩니다.

확인 및 조치가 필요한 위치:

  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantQueryRepository.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantQueryRepositoryImpl.java
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantQueryRepositoryImpl.java
and RoomParticipantQueryRepository.java at the start of the files, the
RoomParticipantQueryRepository interface declares no methods and the
RoomParticipantQueryRepositoryImpl class is an empty implementation, making it
unused. To fix this, either remove both the interface and its empty
implementation class if no custom queries are planned, or if custom queries are
needed later, define the required methods in the interface and implement them in
the class accordingly.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package konkuk.thip.room.application.port.in;

import konkuk.thip.room.application.port.in.dto.RoomJoinCommand;

public interface RoomJoinUsecase {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usecase -> UseCase


void changeJoinState(RoomJoinCommand roomJoinCommand);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package konkuk.thip.room.application.port.in.dto;

public record RoomJoinCommand(
Long userId,
Long roomId,
String type
) {
}
Comment on lines +3 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

타입 안전성을 위해 enum 사용을 고려해보세요.

type 필드가 String으로 되어 있는데, "join"과 "cancel" 같은 고정된 값들을 다루는 것 같습니다. 런타임 에러를 방지하기 위해 enum을 사용하는 것이 좋겠습니다.

다음과 같이 enum을 도입하는 것을 제안합니다:

+public enum RoomJoinType {
+    JOIN, CANCEL
+}
+
 public record RoomJoinCommand(
         Long userId,
         Long roomId,
-        String type
+        RoomJoinType type
 ) {
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/application/port/in/dto/RoomJoinCommand.java
lines 3 to 8, the type field is currently a String representing fixed values
like "join" and "cancel". To improve type safety and prevent runtime errors,
define an enum with these fixed values and replace the String type field with
this enum type in the RoomJoinCommand record.

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface RoomCommandPort {
Long save(Room room);

Category findCategoryByValue(String value);

void updateMemberCount(Room room);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ public interface RoomParticipantCommandPort {
RoomParticipant findByUserIdAndRoomId(Long userId, Long roomId);
List<RoomParticipant> findAllByRoomId(Long roomId);

void save(RoomParticipant roomParticipant);

void deleteByUserIdAndRoomId(Long userId, Long roomId);

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package konkuk.thip.room.application.port.out;


public interface RoomParticipantQueryPort {
boolean existByUserIdAndRoomId(Long userId, Long roomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package konkuk.thip.room.application.service;

import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.common.exception.InvalidStateException;
import konkuk.thip.common.exception.code.ErrorCode;
import konkuk.thip.room.application.port.in.RoomJoinUsecase;
import konkuk.thip.room.application.port.in.dto.RoomJoinCommand;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.application.port.out.RoomParticipantQueryPort;
import konkuk.thip.room.domain.Room;
import konkuk.thip.room.domain.RoomJoinType;
import konkuk.thip.room.domain.RoomParticipant;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static konkuk.thip.room.adapter.out.jpa.RoomParticipantRole.MEMBER;

@Service
@RequiredArgsConstructor
public class RoomJoinService implements RoomJoinUsecase {

private final RoomParticipantQueryPort roomParticipantQueryPort;
private final RoomCommandPort roomCommandPort;
private final RoomParticipantCommandPort roomParticipantCommandPort;

@Override
@Transactional
public void changeJoinState(RoomJoinCommand roomJoinCommand) {
RoomJoinType type = RoomJoinType.from(roomJoinCommand.type());

// 방이 존재하지 않거나 만료된 경우
Room room;
try {
room = roomCommandPort.findById(roomJoinCommand.roomId());
} catch (EntityNotFoundException e) {
throw new InvalidStateException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL);
}

boolean isParticipate = roomParticipantQueryPort.existByUserIdAndRoomId(roomJoinCommand.userId(), roomJoinCommand.roomId());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 어차피 RoomParticipant 도메인이 필요하니 boolean 으로 roomParticipant의 존재여부를 조회하는 것보다는, 처음부터 RoomParticipant 도메인 자체를 조회하는건 어떤가요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기에는 요청 타입에 따라 분기한 뒤 필요한 경우에만 도메인을 조회하는 방식이 도메인의 의도를 명확히 드러낸다고 판단하여 그렇게 구현하였는데, 리뷰 주신 내용을 바탕으로 다시 검토해보니, Optional로 한 번만 조회하고, 존재 여부에 따라 분기 처리하는 방식도 흐름이 명확하고 불필요한 중복 쿼리를 줄일 수 있다는 점에서 장점이 있을 것 같네요.

말씀 주신대로 findBy를 통해 Optional을 반환하도록 리팩토링해보겠습니다!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 Port 에서 제공하는 findById 메서드가 Optional 이 아니라면(저희 도메인을 조회하는 메서드는 전부 도메인 객체가 반환타입 + NOT_FOUND exception throw 플로우였으니), try-catch를 활용하는 방법도 있을 것 같습니다!

room.validateRoomExpired();
Comment on lines +34 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

검증 순서 조정 및 구체적인 에러 처리 필요

방의 만료 상태를 먼저 확인한 후 참여 상태를 확인하는 것이 더 논리적입니다:

 Room room;
 try {
     room = roomCommandPort.findById(roomJoinCommand.roomId());
+    room.validateRoomExpired(); // 만료 검증을 먼저 수행
 } catch (EntityNotFoundException e) {
-    throw new InvalidStateException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL);
+    throw new EntityNotFoundException(ErrorCode.ROOM_NOT_FOUND);
 }
 
 boolean isParticipate = roomParticipantQueryPort.existByUserIdAndRoomId(roomJoinCommand.userId(), roomJoinCommand.roomId());
-room.validateRoomExpired();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Room room;
try {
room = roomCommandPort.findById(roomJoinCommand.roomId());
} catch (EntityNotFoundException e) {
throw new InvalidStateException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL);
}
boolean isParticipate = roomParticipantQueryPort.existByUserIdAndRoomId(roomJoinCommand.userId(), roomJoinCommand.roomId());
room.validateRoomExpired();
Room room;
try {
room = roomCommandPort.findById(roomJoinCommand.roomId());
room.validateRoomExpired(); // 만료 검증을 먼저 수행
} catch (EntityNotFoundException e) {
throw new EntityNotFoundException(ErrorCode.ROOM_NOT_FOUND);
}
boolean isParticipate = roomParticipantQueryPort.existByUserIdAndRoomId(
roomJoinCommand.userId(),
roomJoinCommand.roomId()
);
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/application/service/RoomJoinService.java
around lines 34 to 42, the current code checks if the user participates in the
room before validating if the room is expired. Change the order to first
validate the room's expiration status by calling room.validateRoomExpired()
immediately after retrieving the room, then check the user's participation
status. This ensures logical flow by verifying the room's validity before
checking user participation.

@hd0rable hd0rable Jul 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2: 코래 말처럼 검증순서를 방 만료 확인 -> RoomParticipant 도메인 존재 여부확인 순으로 하는 것은 어떨까요?
유즈케이스 관점에서 보면 “내가 이 방에 참여 가능한가?”를 판단하기 전에, 그 방 자체가 유효하고 참여 가능한 상태인가?를 먼저 확인하는것이 비지니스 로직에 맞다고 생각합니다.

또한, RoomParticipant 여부를 먼저 확인하면, 이미 참여 여부를 확인하는 DB 쿼리가 실행된 후에 방이 이미 만료된 상태임이 확인될 경우, 해당 RoomParticipant 쿼리는 필요 없는 호출이 될 것 같아요 DB 리소스 측면에서도 위와같은 방향으로 코드를 수정하는 것이 좋아 보입니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 수정하겠습니다!


// 참여하기 요청
if(type.isJoinType()) {
// 이미 참여한 상태
if(isParticipate) {
throw new InvalidStateException(ErrorCode.USER_ALREADY_PARTICIPATE);
}

RoomParticipant roomParticipant = RoomParticipant.withoutId(roomJoinCommand.userId(), roomJoinCommand.roomId(), MEMBER.getType());
roomParticipantCommandPort.save(roomParticipant);

//Room의 memberCount 업데이트
room.increaseMemberCount();
Comment on lines +54 to +55

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 저희가 개발시에 빼먹을 수도 있는 부분이라고 생각 + RoomParticipant와는 다른 애그리거트인 Room의 상태변경이 이루어진다고 생각하는데, 추후에 어떻게 최적화를 할 수 있을지(ex. event driven) 고민해보면 좋을거 같습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 애그리거트이기 때문에 이벤트 처리 기반으로 상태 변경을 하자는 말씀이신가요..?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나~~중에 얘기긴 합니다 하하

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조금 덧붙이자면, 다른 애그리거트이기 때문에 분리해보자 라기보다는 해당 로직처럼 여러 애그리거트가 업데이트 되어야하는 플로우는 저희가 (특히 제가) 로직을 빼먹거나 놓칠 가능성이 있다고 생각하는데, 이걸 개선할 방법이 없을까? 라는 생각을 해보자 라는 말이었습니다

}

// 취소하기 요청
if(!type.isJoinType()) {
// 참여하지 않은 상태
if(!isParticipate) {
throw new InvalidStateException(ErrorCode.USER_NOT_PARTICIPATED);
}

// 방장이 참여 취소를 요청한 경우
RoomParticipant roomParticipant = roomParticipantCommandPort.findByUserIdAndRoomId(roomJoinCommand.userId(), roomJoinCommand.roomId());
roomParticipant.validateHostCancelRoom();

roomParticipantCommandPort.deleteByUserIdAndRoomId(roomJoinCommand.userId(), roomJoinCommand.roomId());
Comment on lines +65 to +69

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : RoomParticipant 도메인이 '방장이 참여 취소를 요청했는지를 검사한다' 라는 메서드를 제공하기 보다는, '해당 참여자의 참여 취소를 한다' 라는 메서드를 제공하고, 이 메서드 내부에서 '방장이면 exception throw' 라는 로직을 수행하는 플로우는 어떤가요?

이러면 서비스는
RoomParticipant 한테 '참여 취소' 요청을 보내고, update 된 도메인을 DB에 반영한다
라는 단순한 흐름을 유지할 수 있을 것 같습니다

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그게 좀더 명확할 것 같네요! 수정하겠습니다


//Room의 memberCount 업데이트
room.decreaseMemberCount();
}

// 방의 상태 업데이트
roomCommandPort.updateMemberCount(room);
}


}
Loading