Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -141,7 +141,7 @@ public enum ErrorCode implements ResponseCode {
INVALID_MY_ROOM_TYPE(HttpStatus.BAD_REQUEST, 140009, "유저가 참가한 방 목록 검색 요청에 유효하지 않은 MY ROOM type 이 있습니다."),
INVALID_MY_ROOM_CURSOR(HttpStatus.BAD_REQUEST, 140010, "유저가 참가한 방 목록 검색 요청에 유효하지 않은 cursor 가 있습니다"),
ROOM_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 140011, "방 접근 권한이 없습니다."),

ROOM_HOST_CANNOT_LEAVE(HttpStatus.BAD_REQUEST, 140012, "방장은 방을 나갈 수 없습니다."),
/**
* 150000 : Category error
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ public enum SwaggerResponseDescription {
ROOM_ACCESS_FORBIDDEN,
ROOM_POST_TYPE_NOT_MATCH
))),
ROOM_LEAVE(new LinkedHashSet<>(Set.of(
ROOM_NOT_FOUND,
ROOM_PARTICIPANT_NOT_FOUND,
ROOM_HOST_CANNOT_LEAVE
))),


// Record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
import konkuk.thip.room.adapter.in.web.response.RoomRecruitCloseResponse;
import konkuk.thip.room.application.port.in.RoomCreateUseCase;
import konkuk.thip.room.application.port.in.RoomJoinUseCase;
import konkuk.thip.room.application.port.in.RoomParticipantDeleteUseCase;
import konkuk.thip.room.application.port.in.RoomRecruitCloseUseCase;
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;
import org.springframework.web.bind.annotation.*;

import static konkuk.thip.common.swagger.SwaggerResponseDescription.*;

Expand All @@ -34,6 +32,7 @@ public class RoomCommandController {
private final RoomCreateUseCase roomCreateUseCase;
private final RoomJoinUseCase roomJoinUsecase;
private final RoomRecruitCloseUseCase roomRecruitCloseUsecase;
private final RoomParticipantDeleteUseCase roomParticipantDeleteUseCase;
private final PostLikeUseCase postLikeUseCase;

/**
Expand Down Expand Up @@ -102,4 +101,16 @@ public BaseResponse<RoomPostIsLikeResponse> likeRoomPost(
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(RoomPostIsLikeResponse.of(postLikeUseCase.changeLikeStatusPost(request.toCommand(userId, postId))));
}

@Operation(
summary = "방 나가기",
description = "방장을 제외한 방의 멤버들이 방에서 나갑니다."
)
@ExceptionDescription(ROOM_LEAVE)
@DeleteMapping("/rooms/{roomId}/leave")
public BaseResponse<Void> deleteRoomParticipant(
@Parameter(hidden = true) @UserId final Long userId,
@Parameter(description = "나갈 방의 ID", example = "1") @PathVariable final Long roomId) {
return BaseResponse.ok(roomParticipantDeleteUseCase.leaveRoom(userId, roomId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,7 @@ public void updateStartDate(LocalDate localDate) {
public void updateIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}

@VisibleForTesting
public void updateRoomPercentage(double roomPercentage) {this.roomPercentage = roomPercentage;}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package konkuk.thip.room.application.port.in;

public interface RoomParticipantDeleteUseCase {
Void leaveRoom(Long userId, Long roomId);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package konkuk.thip.room.application.port.out;

import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.common.exception.code.ErrorCode;
import konkuk.thip.room.domain.RoomParticipant;

import java.util.List;
import java.util.Optional;

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

public interface RoomParticipantCommandPort {

Optional<RoomParticipant> findByUserIdAndRoomIdOptional(Long userId, Long roomId);

default RoomParticipant getByUserIdAndRoomIdOrThrow(Long userId, Long roomId) {
return findByUserIdAndRoomIdOptional(userId, roomId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.ROOM_PARTICIPANT_NOT_FOUND));
.orElseThrow(() -> new EntityNotFoundException(ROOM_PARTICIPANT_NOT_FOUND));
}

List<RoomParticipant> findAllByRoomId(Long roomId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package konkuk.thip.room.application.service;

import konkuk.thip.room.application.port.in.RoomParticipantDeleteUseCase;
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 konkuk.thip.roompost.application.service.manager.RoomProgressManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class RoomParticipantDeleteService implements RoomParticipantDeleteUseCase {

private final RoomCommandPort roomCommandPort;
private final RoomParticipantCommandPort roomParticipantCommandPort;

private final RoomProgressManager roomProgressManager;

@Override
@Transactional
public Void leaveRoom(Long userId, Long roomId) {

// 1. 방 조회 및 검증
Room room = roomCommandPort.getByIdOrThrow(roomId);

// 2. 사용자가 방 참여자인지 확인
RoomParticipant roomParticipant = roomParticipantCommandPort.getByUserIdAndRoomIdOrThrow(userId, room.getId());
// 2-1. 방 나가기 권한 검증
roomParticipant.validateRoomLeavable();

// 3. 방 멤버수 감소 / 방 진행률 업데이트
roomProgressManager.removeUserProgressAndUpdateRoomProgress(roomParticipant.getId(), room);

// 4. 방나가기
roomParticipantCommandPort.deleteByUserIdAndRoomId(userId, room.getId());
return null;
}
}
8 changes: 8 additions & 0 deletions src/main/java/konkuk/thip/room/domain/RoomParticipant.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package konkuk.thip.room.domain;

import konkuk.thip.common.entity.BaseDomainEntity;
import konkuk.thip.common.exception.InvalidStateException;
import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole;
import lombok.Getter;
import lombok.experimental.SuperBuilder;

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

@Getter
@SuperBuilder
public class RoomParticipant extends BaseDomainEntity {
Expand Down Expand Up @@ -68,5 +71,10 @@ private boolean checkRole(RoomParticipantRole roomParticipantRole) {
return this.roomParticipantRole.equals(roomParticipantRole.getType());
}

public void validateRoomLeavable() {
if (isHost()) {
throw new InvalidStateException(ROOM_HOST_CANNOT_LEAVE);
}
}
Comment on lines +74 to +78
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM


}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public RecordCreateResult createRecord(RecordCreateCommand command) {
Long newRecordId = recordCommandPort.saveRecord(record);

// 5. RoomParticipant, Room progress 정보 update
roomProgressManager.updateUserAndRoomProgress(record.getCreatorId(), record.getRoomId(), record.getPage());
roomProgressManager.updateUserAndRoomProgress(roomParticipant, room, book, record.getPage());

return RecordCreateResult.of(newRecordId, command.roomId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import konkuk.thip.roompost.application.port.in.RecordDeleteUseCase;
import konkuk.thip.roompost.application.port.in.dto.record.RecordDeleteCommand;
import konkuk.thip.roompost.application.port.out.RecordCommandPort;
import konkuk.thip.roompost.application.service.manager.RoomProgressManager;
import konkuk.thip.roompost.domain.Record;
import konkuk.thip.room.application.service.validator.RoomParticipantValidator;
import lombok.RequiredArgsConstructor;
Expand All @@ -20,6 +21,7 @@ public class RecordDeleteService implements RecordDeleteUseCase {
private final PostLikeCommandPort postLikeCommandPort;

private final RoomParticipantValidator roomParticipantValidator;
private final RoomProgressManager roomProgressManager;

@Override
@Transactional
Expand All @@ -41,6 +43,7 @@ public Long deleteRecord(RecordDeleteCommand command) {
// 3-3. 기록 삭제
recordCommandPort.delete(record);

//TODO// 4. 유저 방 진행도 업데이트
return command.roomId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.room.domain.RoomParticipant;
import konkuk.thip.roompost.application.service.manager.RoomProgressManager;
import konkuk.thip.roompost.application.port.in.VoteCreateUseCase;
import konkuk.thip.roompost.application.port.in.dto.vote.VoteCreateCommand;
Expand Down Expand Up @@ -47,6 +48,7 @@ public VoteCreateResult createVote(VoteCreateCommand command) {
command.roomId()
);

RoomParticipant roomParticipant = roomParticipantCommandPort.getByUserIdAndRoomIdOrThrow(command.userId(), command.roomId());
Room room = roomCommandPort.getByIdOrThrow(vote.getRoomId());
Book book = bookCommandPort.findById(room.getBookId());
validateVote(vote, book);
Expand All @@ -64,7 +66,7 @@ public VoteCreateResult createVote(VoteCreateCommand command) {
voteCommandPort.saveAllVoteItems(voteItems);

// 4. RoomParticipant, Room progress 정보 update
roomProgressManager.updateUserAndRoomProgress(vote.getCreatorId(), vote.getRoomId(), vote.getPage());
roomProgressManager.updateUserAndRoomProgress(roomParticipant, room, book, vote.getPage());

return VoteCreateResult.of(savedVoteId, command.roomId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public Long deleteVote(VoteDeleteCommand command) {
// 3-3. 투표 삭제
voteCommandPort.delete(vote);

//TODO// 4. 유저 방 진행도 업데이트
return command.roomId();
Comment on lines +44 to 45
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

투표 삭제 시 진행도 누락: TODO를 실제 로직으로 연결해 주세요

투표 삭제가 사용자/방 진행도 산정에 영향이 있다면, 현재 상태에서는 진행도가 갱신되지 않아 불일치가 발생할 수 있습니다. 본 PR의 목표(방 나가기)와 직접적 연관이 적더라도, 최소한 TODO를 추적 가능한 이슈로 전환하거나 실제 RoomProgressManager 연동을 추가하는 편이 안전합니다.

원하시면 RoomProgressManager 연동 패턴(주입, 트랜잭션 전후 위치, 예외 전파 기준)에 맞춰 구체 구현 스케치를 드리겠습니다.

다음 스크립트로 현재 코드베이스에 진행도 관련 매니저/메서드가 어떤 것이 있는지 빠르게 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# 진행도 매니저 및 관련 메서드 탐색
rg -n -C3 'class\s+RoomProgressManager\b|RoomProgressManager|progress' --type=java

# 삭제/감소류 메서드 단서 탐색
rg -n -C2 -e 'remove.*Progress|update.*Progress|decrease.*Progress' --type=java

Length of output: 43585


🏁 Script executed:

#!/bin/bash
# Locate VoteDeleteService and display context around TODO
rg -n -C3 'class\s\+VoteDeleteService' --type=java

# Show lines 1–80 of VoteDeleteService for full context
sed -n '1,80p' src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java

Length of output: 1872


투표 삭제 후 진행도 업데이트 로직 누락 – RoomProgressManager 연결 필요

VoteDeleteService 에서 TODO로 남아 있는 “유저 방 진행도 업데이트”를 실제 로직으로 연결해야 합니다. 현재 삭제한 투표에 따른 사용자·방의 진행도 불일치가 발생할 수 있습니다.

  • 조치 사항
    • VoteDeleteServiceRoomProgressManager 주입
    • 댓글/좋아요·투표 삭제 직후에
      roomProgressManager.removeUserProgressAndUpdateRoomProgress(
          /* 방참여자 ID(또는 userId→roomParticipant 조회) */, command.roomId()
      );
      혹은 비즈니스에 맞게 updateUserAndRoomProgress(userId, roomId, page) 호출 추가
    • 다른 서비스(VoteCreateService, RecordDeleteService, RoomParticipantDeleteService)의 RoomProgressManager 연동 패턴 참고

위 변경을 통해 “TODO”를 추적 가능한 실제 이슈로 전환하거나 즉시 반영하여 방 진행도 일관성을 보장해 주세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java
around lines 44-45, the TODO for "유저 방 진행도 업데이트" must be implemented: inject
RoomProgressManager into this service (constructor field), and immediately after
removing the vote call the appropriate RoomProgressManager method (e.g.
removeUserProgressAndUpdateRoomProgress(participantId, command.roomId()) or
updateUserAndRoomProgress(userId, command.roomId(), page)) — obtain the room
participant id via existing userId→roomParticipant lookup if needed; follow the
same injection and invocation pattern used in
VoteCreateService/RecordDeleteService/RoomParticipantDeleteService so that
room/user progress stays consistent after vote deletion.

}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package konkuk.thip.roompost.application.service.manager;

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;
Expand All @@ -17,19 +16,14 @@ public class RoomProgressManager {

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());

public void updateUserAndRoomProgress(RoomParticipant roomParticipant, Room room, Book book, int currentPage) {
// 1. 유저 진행률 update
boolean updated = roomParticipant.updateUserProgress(currentPage, book.getPageCount());
if (!updated) return; // update 되지 않았으면 종료

// 2. 방 평균 진행률 update
List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(roomId);
List<RoomParticipant> all = roomParticipantCommandPort.findAllByRoomId(room.getId());
double total = all.stream()
.filter(p -> !roomParticipant.getId().equals(p.getId())) // 현재 유저 제외
.mapToDouble(RoomParticipant::getUserPercentage)
Expand All @@ -41,4 +35,21 @@ public void updateUserAndRoomProgress(Long userId, Long roomId, int currentPage)
roomCommandPort.update(room);
roomParticipantCommandPort.update(roomParticipant);
}

public void removeUserProgressAndUpdateRoomProgress(Long removeRoomParticipantId, Room room) {

// 나간 유저를 제외한 방 평균 진행률 update
List<RoomParticipant> remainingParticipants = roomParticipantCommandPort.findAllByRoomId(room.getId());
double total = remainingParticipants.stream()
.filter(p -> !p.getId().equals(removeRoomParticipantId)) // 나간 유저 제외
.mapToDouble(RoomParticipant::getUserPercentage)
.sum();
room.updateRoomPercentage(total / (remainingParticipants.size() - 1));

// 방 멤버 수 감소
room.decreaseMemberCount();
// 영속화
roomCommandPort.update(room);
Comment on lines +49 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳

}

}
Loading