Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
70c30c1
[refactor] 방생성 api service 코드 수정 (#181)
seongjunnoh Aug 10, 2025
0163960
[test] 방생성 api 통합 테스트 코드 수정 (#181)
seongjunnoh Aug 10, 2025
dad52a6
[refactor] 투표 생성 시 userPercentage, roomPercentage 값 업데이트하도록 로직 수정 (#181)
seongjunnoh Aug 10, 2025
1afa3e2
[refactor] 기존 테스트 코드를 투표 생성 controller 단위 테스트, api 통합 테스트 코드로 분리 (#181)
seongjunnoh Aug 10, 2025
d8674b9
[test] 투표 생성 시 DB에 userPercentage, roomPercentage 값 잘 업데이트 되는지 확인하는 서…
seongjunnoh Aug 10, 2025
5d5f23f
[refactor] RoomParticipant 도메인 생성을 위한 팩토리 메서드를 member 생성용, host 생성용으로…
seongjunnoh Aug 14, 2025
1d110f3
[refactor] RoomParticipant를 생성하려는 목적에 따라 다른 팩토리 메서드를 호출하도록 코드 수정 (#181)
seongjunnoh Aug 14, 2025
db185f3
[refactor] VoteItem 도메인 생성시 count값을 0으로 초기화하도록 팩토리 메서드 수정 (#181)
seongjunnoh Aug 14, 2025
34e46ae
[feat] 방 참여자의 활동 이후, 방 참여자와 방의 progress 값 업데이트를 위한 헬퍼 서비스 추가 (#181)
seongjunnoh Aug 14, 2025
f2c3faf
[refactor] 헬퍼 서비스 도입에 따라 기존 기록, 투표 생성 서비스 코드 수정 (#181)
seongjunnoh Aug 14, 2025
ef2444b
[refactor] 투표 생성 api 서비스 테스트 코드에서 progress 값 검증코드 수정 (#181)
seongjunnoh Aug 14, 2025
691e99e
develop merge
seongjunnoh Aug 14, 2025
3cc47d5
[refactor] helper service 어노테이션으로 수정 (#181)
seongjunnoh Aug 14, 2025
3b7cbc5
[refactor] helper service 에 transactional 어노테이션 삭제 (#181)
seongjunnoh Aug 14, 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 @@ -13,12 +13,11 @@
import konkuk.thip.room.application.service.validator.RoomParticipantValidator;
import konkuk.thip.room.domain.Room;
import konkuk.thip.room.domain.RoomParticipant;
import konkuk.thip.roompost.application.service.helper.RoomProgressHelper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static konkuk.thip.common.exception.code.ErrorCode.RECORD_CANNOT_BE_OVERVIEW;

@Service
Expand All @@ -31,6 +30,7 @@ public class RecordCreateService implements RecordCreateUseCase {
private final RoomParticipantCommandPort roomParticipantCommandPort;

private final RoomParticipantValidator roomParticipantValidator;
private final RoomProgressHelper roomProgressHelper;

@Override
@Transactional
Expand All @@ -57,32 +57,15 @@ public RecordCreateResult createRecord(RecordCreateCommand command) {
validateRoomParticipant(roomParticipant, command.isOverview());
validateRecord(record, book);

// 4. RoomParticipant의 currentPage, userPercentage 업데이트
updateRoomProgress(roomParticipant, record, book, room);

// 5. Record 저장
// 4. 문제없는 경우 Record 저장
Long newRecordId = recordCommandPort.saveRecord(record);

// 6. Room, RoomParticipant 업데이트
roomCommandPort.update(room);
roomParticipantCommandPort.update(roomParticipant);
// 5. RoomParticipant, Room progress 정보 update
roomProgressHelper.updateUserAndRoomProgress(record.getCreatorId(), record.getRoomId(), record.getPage());
Comment on lines +63 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

와우 공통 로직을 분리하니까 확실히 응용서비스 로직이 간단해졌네요! 굿굿


return RecordCreateResult.of(newRecordId, command.roomId());
}

private void updateRoomProgress(RoomParticipant roomParticipant, Record record, Book book, Room room) {
if(roomParticipant.updateUserProgress(record.getPage(), book.getPageCount())) {
// userPercentage가 업데이트되었으면 Room의 roomPercentage 업데이트
List<RoomParticipant> roomParticipantList = roomParticipantCommandPort.findAllByRoomId(record.getRoomId());
Double totalUserPercentage = roomParticipantList.stream()
.filter(participant -> !roomParticipant.getId().equals(participant.getId())) // 현재 업데이트 중인 사용자 제외
.map(RoomParticipant::getUserPercentage)
.reduce(0.0, Double::sum);
totalUserPercentage += roomParticipant.getUserPercentage();
room.updateRoomPercentage(totalUserPercentage / roomParticipantList.size());
}
}

private void validateRoomParticipant(RoomParticipant roomParticipant, boolean isOverview) {
// UserRoom의 총평 작성 가능 여부 검증
if (!roomParticipant.canWriteOverview() && isOverview) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import konkuk.thip.book.application.port.out.BookApiQueryPort;
import konkuk.thip.book.application.port.out.BookCommandPort;
import konkuk.thip.book.domain.Book;
import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole;
import konkuk.thip.room.application.port.in.RoomCreateUseCase;
import konkuk.thip.room.application.port.in.dto.RoomCreateCommand;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.domain.Category;
import konkuk.thip.room.domain.Room;
import konkuk.thip.room.domain.RoomParticipant;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,6 +20,7 @@
public class RoomCreateService implements RoomCreateUseCase {

private final RoomCommandPort roomCommandPort;
private final RoomParticipantCommandPort roomParticipantCommandPort;
private final BookCommandPort bookCommandPort;
private final BookApiQueryPort bookApiQueryPort;

Expand All @@ -42,11 +45,13 @@ public Long createRoom(RoomCreateCommand command, Long userId) {
bookId,
category
);
Long savedRoomId = roomCommandPort.save(room);

// TODO : 방 생성한 사람 (= api 호출 토큰에 포함된 userId) 이 해당 방에 속한 멤버라는 사실을 DB에 영속화 해야함
// UserRoom 도메인이 정리되면 개발 ㄱㄱ
// 4. 방장 RoomParticipant 생성 및 DB save
RoomParticipant roomParticipant = RoomParticipant.hostWithoutId(userId, savedRoomId);
roomParticipantCommandPort.save(roomParticipant);

return roomCommandPort.save(room);
return savedRoomId;
}

private Long resolveBookAndEnsurePage(String isbn) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private void handleJoin(RoomJoinCommand roomJoinCommand, Optional<RoomParticipan
throw new BusinessException(ErrorCode.USER_ALREADY_PARTICIPATE);
});

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

//Room의 memberCount 업데이트
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/konkuk/thip/room/domain/RoomParticipant.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ public class RoomParticipant extends BaseDomainEntity {

private Long roomId;

public static RoomParticipant withoutId(Long userId, Long roomId, String roomParticipantRole) {
public static RoomParticipant memberWithoutId(Long userId, Long roomId) {
return RoomParticipant.builder()
.currentPage(0)
.userPercentage(0.0)
.userId(userId)
.roomId(roomId)
.roomParticipantRole(roomParticipantRole)
.roomParticipantRole(RoomParticipantRole.MEMBER.getType())
.build();
}

public static RoomParticipant hostWithoutId(Long userId, Long roomId) {
return RoomParticipant.builder()
.currentPage(0)
.userPercentage(0.0)
.userId(userId)
.roomId(roomId)
.roomParticipantRole(RoomParticipantRole.HOST.getType())
.build();
}
Comment on lines +24 to 42
Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package konkuk.thip.roompost.application.service.helper;

import konkuk.thip.book.application.port.out.BookCommandPort;
import konkuk.thip.book.domain.Book;
import konkuk.thip.common.annotation.HelperService;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.domain.Room;
import konkuk.thip.room.domain.RoomParticipant;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@HelperService
@RequiredArgsConstructor
public class RoomProgressHelper {

private final RoomParticipantCommandPort roomParticipantCommandPort;
private final RoomCommandPort roomCommandPort;
private final BookCommandPort bookCommandPort;

public void updateUserAndRoomProgress(Long userId, Long roomId, int currentPage) {
RoomParticipant roomParticipant = roomParticipantCommandPort.getByUserIdAndRoomIdOrThrow(userId, roomId);
Room room = roomCommandPort.getByIdOrThrow(roomId);
Book book = bookCommandPort.findById(room.getBookId());

// 1. 유저 진행률 update
boolean updated = roomParticipant.updateUserProgress(currentPage, book.getPageCount());
if (!updated) return; // update 되지 않았으면 종료

// 2. 방 평균 진행률 update
List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(roomId);
double total = all.stream()
.filter(p -> !roomParticipant.getId().equals(p.getId())) // 현재 유저 제외
.mapToDouble(RoomParticipant::getUserPercentage)
.sum();
total += roomParticipant.getUserPercentage();
room.updateRoomPercentage(total / all.size());

// 3. 영속화
roomCommandPort.update(room);
roomParticipantCommandPort.update(roomParticipant);
}
Comment on lines +33 to +45
Copy link

@coderabbitai coderabbitai bot Aug 14, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

참여자 목록 공집합 및 평균 계산 시 예외 가능성 방어

  • all.size()가 0이면 0으로 나누기 예외가 발생합니다. 정상 플로우에서는 최소 1명이 존재하겠지만, 데이터 불일치나 경합 상황에서 방어가 필요합니다.
  • 동일 트랜잭션에서 평균만 갱신되고 영속화 순서가 반대로 되더라도 큰 문제는 없으나, 조기 종료 경로(!updated) 뒤에 불필요한 조회/계산을 하지 않는 현재 구조는 적절합니다.

간단한 방어 로직을 추가해 주세요.

         // 2. 방 평균 진행률 update
         List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(roomId);
-        double total = all.stream()
+        if (all.isEmpty()) {
+            return; // 참여자가 없으면 평균 계산 불가
+        }
+        double total = all.stream()
                 .filter(p -> !roomParticipant.getId().equals(p.getId()))    // 현재 유저 제외
                 .mapToDouble(RoomParticipant::getUserPercentage)
                 .sum();
         total += roomParticipant.getUserPercentage();
         room.updateRoomPercentage(total / all.size());
 
         // 3. 영속화
         roomCommandPort.update(room);
         roomParticipantCommandPort.update(roomParticipant);
📝 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
// 2. 방 평균 진행률 update
List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(roomId);
double total = all.stream()
.filter(p -> !roomParticipant.getId().equals(p.getId())) // 현재 유저 제외
.mapToDouble(RoomParticipant::getUserPercentage)
.sum();
total += roomParticipant.getUserPercentage();
room.updateRoomPercentage(total / all.size());
// 3. 영속화
roomCommandPort.update(room);
roomParticipantCommandPort.update(roomParticipant);
}
// 2. 방 평균 진행률 update
List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(roomId);
if (all.isEmpty()) {
return; // 참여자가 없으면 평균 계산 불가
}
double total = all.stream()
.filter(p -> !roomParticipant.getId().equals(p.getId())) // 현재 유저 제외
.mapToDouble(RoomParticipant::getUserPercentage)
.sum();
total += roomParticipant.getUserPercentage();
room.updateRoomPercentage(total / all.size());
// 3. 영속화
roomCommandPort.update(room);
roomParticipantCommandPort.update(roomParticipant);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/roompost/application/service/helper/RoomProgressHelper.java
around lines 33 to 45, protect against division-by-zero when computing the
average: check if the participant list `all` is empty (or size == 0) before
dividing; if empty, set the room percentage to a safe default (e.g., 0.0) and
skip the average calculation, otherwise perform the existing sum/average logic
and persist updates as before.

Copy link
Contributor

Choose a reason for hiding this comment

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

방을 생성할때 무조건 방의 HOST를 위해 최소 하나의 RoomParticipant가 만들어지는데 참여자가 없을리가 있나?

Copy link

Choose a reason for hiding this comment

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

@buzz0331 맞는 말씀입니다! 비즈니스 로직상 방 생성 시 항상 HOST 역할의 RoomParticipant가 생성되고, 이 헬퍼 메서드가 호출되는 경로(투표 생성, 기록 생성)에서는 모두 사용자가 해당 방의 참여자인지 사전 검증을 거치기 때문에 논리적으로 참여자 목록이 비어있을 수 없습니다.

해당 방어 로직은 불필요한 over-engineering일 가능성이 높습니다. 비즈니스 규칙이 명확하게 보장하는 상황에서는 과도한 방어 코드가 오히려 코드의 가독성을 해칠 수 있죠.

기존 코드 그대로 유지하시는 것이 적절할 것 같습니다.


✏️ Learnings added
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#183
File: src/main/java/konkuk/thip/roompost/application/service/helper/RoomProgressHelper.java:33-45
Timestamp: 2025-08-14T12:29:00.102Z
Learning: 방 생성 시 항상 HOST 역할의 RoomParticipant가 생성되며, RoomProgressHelper.updateUserAndRoomProgress() 메서드는 사용자가 해당 방의 참여자임이 사전 검증된 상황에서만 호출되므로 참여자 목록이 비어있을 수 없다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import konkuk.thip.book.application.port.out.BookCommandPort;
import konkuk.thip.book.domain.Book;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.application.service.validator.RoomParticipantValidator;
import konkuk.thip.room.domain.Room;
import konkuk.thip.roompost.application.service.helper.RoomProgressHelper;
import konkuk.thip.vote.application.port.in.VoteCreateUseCase;
import konkuk.thip.vote.application.port.in.dto.VoteCreateCommand;
import konkuk.thip.vote.application.port.in.dto.VoteCreateResult;
Expand All @@ -22,14 +25,19 @@
@Slf4j
public class VoteCreateService implements VoteCreateUseCase {

private final RoomParticipantValidator roomParticipantValidator;
private final RoomParticipantCommandPort roomParticipantCommandPort;
private final VoteCommandPort voteCommandPort;
private final RoomCommandPort roomCommandPort;
private final BookCommandPort bookCommandPort;

private final RoomProgressHelper roomProgressHelper;

@Transactional
@Override
//todo UserRoom 업데이트 로직 추가 필요!!
public VoteCreateResult createVote(VoteCreateCommand command) {
roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.userId());
Copy link
Member

Choose a reason for hiding this comment

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

굿굿


// 1. validate
Vote vote = Vote.withoutId(
command.content(),
Expand All @@ -39,7 +47,9 @@ public VoteCreateResult createVote(VoteCreateCommand command) {
command.roomId()
);

validateVote(vote);
Room room = roomCommandPort.getByIdOrThrow(vote.getRoomId());
Book book = bookCommandPort.findById(room.getBookId());
validateVote(vote, book);

// 2. vote 저장
Long savedVoteId = voteCommandPort.saveVote(vote);
Expand All @@ -48,19 +58,18 @@ public VoteCreateResult createVote(VoteCreateCommand command) {
List<VoteItem> voteItems = command.voteItemCreateCommands().stream()
.map(itemCmd -> VoteItem.withoutId(
itemCmd.itemName(),
0,
savedVoteId
))
.toList();
voteCommandPort.saveAllVoteItems(voteItems);

// 4. RoomParticipant, Room progress 정보 update
roomProgressHelper.updateUserAndRoomProgress(vote.getCreatorId(), vote.getRoomId(), vote.getPage());

return VoteCreateResult.of(savedVoteId, command.roomId());
}

private void validateVote(Vote vote) {
Room room = roomCommandPort.getByIdOrThrow(vote.getRoomId());
Book book = bookCommandPort.findById(room.getBookId());

private void validateVote(Vote vote, Book book) {
// 페이지 유효성 검증
vote.validatePage(book.getPageCount());

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/konkuk/thip/vote/domain/VoteItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public class VoteItem extends BaseDomainEntity {

private Long voteId;

public static VoteItem withoutId(String itemName, int count, Long voteId) {
public static VoteItem withoutId(String itemName, Long voteId) {
return VoteItem.builder()
.id(null)
.itemName(itemName)
.count(count)
.count(0)
.voteId(voteId)
.build();
}
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/konkuk/thip/common/util/TestEntityFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ public static BookJpaEntity createBook() {
.build();
}

/**
* page 값을 지정할 수 있는 custom 생성자
*/
public static BookJpaEntity createBook(int page) {
return BookJpaEntity.builder()
.title("책제목")
.authorName("저자")
.isbn(UUID.randomUUID().toString().replace("-", "").substring(0, 13))
.bestSeller(false)
.publisher("출판사")
.imageUrl("img")
.pageCount(page)
.description("설명")
.build();
}

public static BookJpaEntity createBookWithISBN(String isbn) {
return BookJpaEntity.builder()
.title("책제목")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import konkuk.thip.common.util.TestEntityFactory;
import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity;
import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository;
import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository;
import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository;
import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserRole;
Expand All @@ -24,6 +26,7 @@
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
Expand Down Expand Up @@ -63,13 +66,17 @@ class RoomCreateAPITest {
@Autowired
private RoomJpaRepository roomJpaRepository;

@Autowired
private RoomParticipantJpaRepository roomParticipantJpaRepository;

@AfterEach
void tearDown() {
roomJpaRepository.deleteAll();
bookJpaRepository.deleteAll();
userJpaRepository.deleteAll();
categoryJpaRepository.deleteAll();
aliasJpaRepository.deleteAll();
roomParticipantJpaRepository.deleteAllInBatch();
roomJpaRepository.deleteAllInBatch();
bookJpaRepository.deleteAllInBatch();
userJpaRepository.deleteAllInBatch();
categoryJpaRepository.deleteAllInBatch();
aliasJpaRepository.deleteAllInBatch();
}

private void saveUserAndCategory() {
Expand Down Expand Up @@ -273,4 +280,35 @@ void room_create_book_not_exist() throws Exception {
);
}

@Test
@DisplayName("방 생성에 성공하면, 방장의 정보가 DB에 저장된다.")
@Transactional // RoomParticipant -> Room, User 의 manyToOne 지연로딩을 위해 추가
void room_create_room_participant_save_success() throws Exception {
//given : user, category, pageCount값이 있는 book 생성, request 생성
saveUserAndCategory();
saveBookWithPageCount();

Long userId = userJpaRepository.findAll().get(0).getUserId();
Long bookId = bookJpaRepository.findAll().get(0).getBookId();
Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId();

Map<String, Object> request = buildRoomCreateRequest();

//when
ResultActions result = mockMvc.perform(post("/rooms")
.requestAttr("userId", userId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)
));

//then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.data.roomId").exists());

RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAll().get(0);
assertThat(roomParticipantJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId);

RoomJpaEntity savedRoomJpaEntity = roomJpaRepository.findAll().get(0);
assertThat(roomParticipantJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(savedRoomJpaEntity.getRoomId());
}
Comment on lines +283 to +313
Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

}
Comment on lines +283 to 314
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

검증 보강 제안: 역할/초기값까지 확인하고 조회도 더 견고하게

현재는 참가자-유저/방 매핑만 검증합니다. 다음을 추가하면 회귀를 더 잘 잡을 수 있습니다.

  • 역할이 HOST인지 검증
  • 초기값(예: currentPage=0, userPercentage=0.0) 검증
  • findAll().get(0) 대신 응답에서 roomId를 파싱해 (userId, roomId)로 조회하면 테스트가 견고해집니다

예시 수정:

         result.andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.roomId").exists());

-        RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAll().get(0);
-        assertThat(roomParticipantJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId);
-
-        RoomJpaEntity savedRoomJpaEntity = roomJpaRepository.findAll().get(0);
-        assertThat(roomParticipantJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(savedRoomJpaEntity.getRoomId());
+        String json = result.andReturn().getResponse().getContentAsString();
+        Long roomId = objectMapper.readTree(json).path("data").path("roomId").asLong();
+
+        RoomParticipantJpaEntity roomParticipant = roomParticipantJpaRepository
+                .findByUserIdAndRoomId(userId, roomId)
+                .orElseThrow();
+        assertThat(roomParticipant.getUserJpaEntity().getUserId()).isEqualTo(userId);
+        assertThat(roomParticipant.getRoomJpaEntity().getRoomId()).isEqualTo(roomId);
+        // 초기 상태 검증(도메인 기본값에 맞게 조정)
+        assertThat(roomParticipant.getRoomParticipantRole()).isNotNull(); // .isEqualTo(RoomParticipantRole.HOST) 권장
+        assertThat(roomParticipant.getCurrentPage()).isZero();
+        assertThat(roomParticipant.getUserPercentage()).isEqualTo(0.0);

🏁 Script executed:

#!/bin/bash
set -e

# RoomParticipantJpaEntity 클래스 위치 확인
rg "class RoomParticipantJpaEntity" -n

# RoomParticipantJpaRepository 인터페이스 및 findByUserIdAndRoomId 메서드 존재 여부 확인
rg "interface RoomParticipantJpaRepository" -n
rg "findByUserIdAndRoomId" -n

# 엔티티 필드 및 기본값 설정 확인
rg "currentPage" -n src/main/java
rg "userPercentage" -n src/main/java

Length of output: 8152


아래 스크립트로 RoomParticipantJpaEntity의 역할 필드 기본값을 확인해보겠습니다.


🏁 Script executed:

#!/bin/bash
set -e

# 역할(role) 필드 선언 및 기본값 확인
rg "roomParticipantRole" -n src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java

# 엔티티 파일 상단 일부 출력
sed -n '1,200p' src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java

Length of output: 2311


아래 스크립트로 도메인 RoomParticipant의 기본 역할 설정과 방 생성 서비스(RoomCreateService)에서 역할이 어떻게 초기화되는지 확인해 보겠습니다.


🏁 Script executed:

#!/bin/bash
set -e

# 1. 도메인 RoomParticipant 클래스 내용 확인
rg "class RoomParticipant" -n src/main/java
sed -n '1,200p' src/main/java/konkuk/thip/room/domain/RoomParticipant.java

# 2. 방 생성 서비스에서 RoomParticipant를 생성하는 로직 확인
rg "class RoomCreateService" -n src/main/java
sed -n '1,200p' src/main/java/konkuk/thip/room/application/service/RoomCreateService.java

Length of output: 6197


검증 보강 제안: 역할/초기값 검증 및 조회 견고화

현재 테스트는 참가자–유저/방 매핑만 검증하고 있습니다. 아래 항목을 추가해 회귀를 더욱 잘 잡아주세요.

  • 방장 역할이 HOST인지 검증
  • 초기 상태인 currentPage = 0, userPercentage = 0.0 검증
  • findAll().get(0) 대신, 응답에서 roomId를 파싱한 뒤
    findByUserIdAndRoomId(userId, roomId)로 조회

예시 수정:

         result.andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.roomId").exists());

-        RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAll().get(0);
-        assertThat(roomParticipantJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId);
-
-        RoomJpaEntity savedRoomJpaEntity = roomJpaRepository.findAll().get(0);
-        assertThat(roomParticipantJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(savedRoomJpaEntity.getRoomId());
+        String json = result.andReturn().getResponse().getContentAsString();
+        Long roomId = objectMapper.readTree(json).path("data").path("roomId").asLong();
+
+        RoomParticipantJpaEntity roomParticipant = roomParticipantJpaRepository
+                .findByUserIdAndRoomId(userId, roomId)
+                .orElseThrow();
+        assertThat(roomParticipant.getUserJpaEntity().getUserId()).isEqualTo(userId);
+        assertThat(roomParticipant.getRoomJpaEntity().getRoomId()).isEqualTo(roomId);
+        assertThat(roomParticipant.getRoomParticipantRole()).isEqualTo(RoomParticipantRole.HOST);
+        assertThat(roomParticipant.getCurrentPage()).isZero();
+        assertThat(roomParticipant.getUserPercentage()).isEqualTo(0.0);
📝 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
@Test
@DisplayName("방 생성에 성공하면, 방장의 정보가 DB에 저장된다.")
@Transactional // RoomParticipant -> Room, User 의 manyToOne 지연로딩을 위해 추가
void room_create_room_participant_save_success() throws Exception {
//given : user, category, pageCount값이 있는 book 생성, request 생성
saveUserAndCategory();
saveBookWithPageCount();
Long userId = userJpaRepository.findAll().get(0).getUserId();
Long bookId = bookJpaRepository.findAll().get(0).getBookId();
Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId();
Map<String, Object> request = buildRoomCreateRequest();
//when
ResultActions result = mockMvc.perform(post("/rooms")
.requestAttr("userId", userId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)
));
//then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.data.roomId").exists());
RoomParticipantJpaEntity roomParticipantJpaEntity = roomParticipantJpaRepository.findAll().get(0);
assertThat(roomParticipantJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId);
RoomJpaEntity savedRoomJpaEntity = roomJpaRepository.findAll().get(0);
assertThat(roomParticipantJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(savedRoomJpaEntity.getRoomId());
}
}
@Test
@DisplayName("방 생성에 성공하면, 방장의 정보가 DB에 저장된다.")
@Transactional // RoomParticipant -> Room, User 의 manyToOne 지연로딩을 위해 추가
void room_create_room_participant_save_success() throws Exception {
//given : user, category, pageCount값이 있는 book 생성, request 생성
saveUserAndCategory();
saveBookWithPageCount();
Long userId = userJpaRepository.findAll().get(0).getUserId();
Long bookId = bookJpaRepository.findAll().get(0).getBookId();
Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId();
Map<String, Object> request = buildRoomCreateRequest();
//when
ResultActions result = mockMvc.perform(post("/rooms")
.requestAttr("userId", userId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)
));
//then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.data.roomId").exists());
String json = result.andReturn().getResponse().getContentAsString();
Long roomId = objectMapper.readTree(json).path("data").path("roomId").asLong();
RoomParticipantJpaEntity roomParticipant = roomParticipantJpaRepository
.findByUserIdAndRoomId(userId, roomId)
.orElseThrow();
assertThat(roomParticipant.getUserJpaEntity().getUserId()).isEqualTo(userId);
assertThat(roomParticipant.getRoomJpaEntity().getRoomId()).isEqualTo(roomId);
assertThat(roomParticipant.getRoomParticipantRole()).isEqualTo(RoomParticipantRole.HOST);
assertThat(roomParticipant.getCurrentPage()).isZero();
assertThat(roomParticipant.getUserPercentage()).isEqualTo(0.0);
}
}
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateAPITest.java around
lines 283 to 314, enhance the test by verifying that the participant's role is
HOST and that currentPage and userPercentage are initialized to 0 and 0.0
respectively. Also, replace the use of findAll().get(0) with extracting the
roomId from the response JSON and then querying the repository using
findByUserIdAndRoomId(userId, roomId) to make the retrieval more robust and
precise.

Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void alreadyParticipated() {

given(roomCommandPort.findById(ROOM_ID)).willReturn(Optional.of(room));
given(roomParticipantCommandPort.findByUserIdAndRoomIdOptional(USER_ID, ROOM_ID))
.willReturn(Optional.of(RoomParticipant.withoutId(USER_ID, ROOM_ID, MEMBER.getType())));
.willReturn(Optional.of(RoomParticipant.memberWithoutId(USER_ID, ROOM_ID)));

assertThatThrownBy(() -> roomJoinService.changeJoinState(command))
.isInstanceOf(BusinessException.class)
Expand Down Expand Up @@ -100,7 +100,7 @@ void notParticipated() {
@DisplayName("정상적으로 취소 시 참여자 제거 및 인원수 감소")
void successCancel() {
RoomJoinCommand command = new RoomJoinCommand(USER_ID, ROOM_ID, "cancel");
RoomParticipant participant = RoomParticipant.withoutId(USER_ID, ROOM_ID, MEMBER.getType());
RoomParticipant participant = RoomParticipant.memberWithoutId(USER_ID, ROOM_ID);

given(roomCommandPort.findById(ROOM_ID)).willReturn(Optional.of(room));
given(roomParticipantCommandPort.findByUserIdAndRoomIdOptional(USER_ID, ROOM_ID))
Expand Down
Loading