Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4f5a335
[feat] 기록 생성 관련 dto (#45)
buzz0331 Jul 5, 2025
405a5b2
[feat] 기록 생성 Usecase (#45)
buzz0331 Jul 5, 2025
983492e
[feat] 기록 생성 Controller (#45)
buzz0331 Jul 5, 2025
8088911
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Server…
buzz0331 Jul 5, 2025
5aec75d
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Server…
buzz0331 Jul 5, 2025
2fd757d
[feat] Record 도메인 규칙 선언 (#45)
buzz0331 Jul 5, 2025
475a13f
[feat] RecordCommandPort 정의 (#45)
buzz0331 Jul 5, 2025
7f9c6c9
[feat] Controller에서 필요한 dto 정의 (#45)
buzz0331 Jul 5, 2025
09e0327
[feat] 기록 생성 api 핸들러 추가 (#45)
buzz0331 Jul 5, 2025
9dde2d2
[feat] 기록 생성 서비스 구현 (#45)
buzz0331 Jul 5, 2025
7c70201
[feat] 기록 생성 관련 persistence 선언 (#45)
buzz0331 Jul 5, 2025
9e5d3e8
[feat] 기록 생성 관련 에러코드 추가 (#45)
buzz0331 Jul 5, 2025
9c2ff07
[test] Record 생성 api 통합 테스트 (#45)
buzz0331 Jul 5, 2025
02ae826
[test] Record 도메인 단위 테스트 (#45)
buzz0331 Jul 5, 2025
b16f767
[test] 모든 테스트에 클래스 단위 DisplayName 지정 (#45)
buzz0331 Jul 5, 2025
55a7afa
[test] 테스트 메서드 네이밍 수정 (#45)
buzz0331 Jul 6, 2025
36cbc0d
[feat] Room, UserRoom에 필요한 도메인 규칙 정의 (#45)
buzz0331 Jul 6, 2025
fb3ec7d
[feat] UserRoom Persistence port 정의 (#45)
buzz0331 Jul 6, 2025
b5e14ee
[feat] UserRoom 관련 쿼리 (#45)
buzz0331 Jul 6, 2025
7654186
[feat] UserRoomQueryPort 추가 (#45)
buzz0331 Jul 6, 2025
f244055
[feat] validateOverview 수정 (#45)
buzz0331 Jul 6, 2025
e5aca74
[feat] 기록 생성 api 로직 수정 (#45)
buzz0331 Jul 6, 2025
2e657bb
[teat] Record, UserRoom 도메인 단위 테스트 (#45)
buzz0331 Jul 6, 2025
ee10143
[teat] Record 통합 테스트 수정 (#45)
buzz0331 Jul 6, 2025
3e9f385
[teat] record, userroom 관련 필요한 에러코드 추가 (#45)
buzz0331 Jul 6, 2025
63e1675
[chore] VoteCreateService todo 추가 (#45)
buzz0331 Jul 6, 2025
5a3fdf7
[refactor] 에러 구문 백분율 처리 오류 제거 (#45)
buzz0331 Jul 6, 2025
dc9212e
[refactor] userpercentage 100 넘어갈 경우 100으로 통일 (#45)
buzz0331 Jul 6, 2025
05328fd
[refactor] 컨트롤러 핸들러 파라미터 불변성 보장 (#45)
buzz0331 Jul 7, 2025
a407341
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Server…
buzz0331 Jul 8, 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
15 changes: 14 additions & 1 deletion src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,20 @@ public enum ErrorCode implements ResponseCode {
*/
VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."),
VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다."),
INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다.")
INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다."),

/**
* 120000 : record error
*/
RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, 120000, "존재하지 않는 RECORD 입니다."),
RECORD_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 120001, "총평이 될 수 없는 RECORD 입니다."),
INVALID_RECORD_PAGE_RANGE(HttpStatus.BAD_REQUEST, 120002, "RECORD의 page 값이 유효하지 않습니다."),
RECORD_CANNOT_WRITE_IN_EXPIRED_ROOM(HttpStatus.BAD_REQUEST, 120003, "만료된 방에는 기록을 남길 수 없습니다."),

/**
* 130000 : userRoom error
*/
USER_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 130000, "존재하지 않는 USER_ROOM (방과 사용자 관계) 입니다."),

;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
package konkuk.thip.record.adapter.in.web;

import jakarta.validation.Valid;
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import konkuk.thip.record.adapter.in.web.request.RecordCreateRequest;
import konkuk.thip.record.adapter.in.web.response.RecordCreateResponse;
import konkuk.thip.record.application.port.in.RecordCreateUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class RecordCommandController {
private final RecordCreateUseCase recordCreateUseCase;

@PostMapping("/rooms/{roomId}/record")
public BaseResponse<RecordCreateResponse> createRecord(
@UserId final Long userId,
@PathVariable final Long roomId,
@Valid @RequestBody final RecordCreateRequest recordCreateRequest) {
return BaseResponse.ok(
RecordCreateResponse.of(
recordCreateUseCase.createRecord(recordCreateRequest.toCommand(roomId, userId))
));
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package konkuk.thip.record.adapter.in.web.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import konkuk.thip.record.application.port.in.dto.RecordCreateCommand;

public record RecordCreateRequest (
@NotNull(message = "page는 필수입니다.")
Integer page,

@NotNull(message = "isOverview(= 총평 여부)는 필수입니다.")
Boolean isOverview,

@NotBlank(message = "기록 내용은 필수입니다.")
@Size(max = 500, message = "기록 내용은 최대 500자 입니다.")
String content
) {
public RecordCreateCommand toCommand(Long roomId, Long creatorId) {
return new RecordCreateCommand(
creatorId,
roomId,
page,
isOverview,
content
);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package konkuk.thip.record.adapter.in.web.response;

public record RecordCreateResponse(
Long recordId
) {
public static RecordCreateResponse of(Long recordId) {
return new RecordCreateResponse(recordId);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
package konkuk.thip.record.adapter.out.persistence;

import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.record.adapter.out.mapper.RecordMapper;
import konkuk.thip.record.application.port.out.RecordCommandPort;
import konkuk.thip.record.domain.Record;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import konkuk.thip.user.adapter.out.persistence.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

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 RecordCommandPersistenceAdapter implements RecordCommandPort {

private final RecordJpaRepository recordJpaRepository;
private final UserJpaRepository userJpaRepository;
private final RoomJpaRepository roomJpaRepository;
private final RecordMapper recordMapper;

@Override
public Long saveRecord(Record record) {
UserJpaEntity userJpaEntity = userJpaRepository.findById(record.getCreatorId()).orElseThrow(
() -> new EntityNotFoundException(USER_NOT_FOUND)
);

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

return recordJpaRepository.save(
recordMapper.toJpaEntity(record, userJpaEntity, roomJpaEntity)
).getPostId();
}

}

This file was deleted.

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

import konkuk.thip.record.application.port.in.dto.RecordCreateCommand;

public interface RecordCreateUseCase {

Long createRecord(RecordCreateCommand command);

}

This file was deleted.

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

public record RecordCreateCommand(
Long userId,

Long roomId,

int page,

boolean isOverview,

String content
) {

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


import konkuk.thip.record.domain.Record;

public interface RecordCommandPort {

Long saveRecord(Record record);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package konkuk.thip.record.application.service;

import jakarta.transaction.Transactional;
import konkuk.thip.book.application.port.out.BookCommandPort;
import konkuk.thip.book.domain.Book;
import konkuk.thip.common.exception.BusinessException;
import konkuk.thip.common.exception.InvalidStateException;
import konkuk.thip.record.application.port.in.RecordCreateUseCase;
import konkuk.thip.record.application.port.in.dto.RecordCreateCommand;
import konkuk.thip.record.application.port.out.RecordCommandPort;
import konkuk.thip.record.domain.Record;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.domain.Room;
import konkuk.thip.user.application.port.out.UserRoomCommandPort;
import konkuk.thip.user.domain.UserRoom;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

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

@Service
@RequiredArgsConstructor
public class RecordCreateService implements RecordCreateUseCase {

private final RecordCommandPort recordCommandPort;
private final RoomCommandPort roomCommandPort;
private final BookCommandPort bookCommandPort;
private final UserRoomCommandPort userRoomCommandPort;

@Transactional
@Override
//todo updateRoomPercentage 스케줄러로 책임을 분리할지 논의
public Long createRecord(RecordCreateCommand command) {
// 1. Record 생성
Record record = Record.withoutId(
command.content(),
command.userId(),
command.page(),
command.isOverview(),
command.roomId()
);

// 2. UserRoom, Room, Book 조회
UserRoom userRoom = userRoomCommandPort.findByUserIdAndRoomId(command.userId(), command.roomId());
Room room = roomCommandPort.findById(record.getRoomId());
Book book = bookCommandPort.findById(room.getBookId());

// 3. 유효성 검증
validateRoom(room);
validateUserRoom(userRoom);
validateRecord(record, book);

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

// 5. Record 저장
return recordCommandPort.saveRecord(record);
}

private void updateRoomProgress(UserRoom userRoom, Record record, Book book, Room room) {
if(userRoom.updateUserProgress(record.getPage(), book.getPageCount())) {
// userPercentage가 업데이트되었으면 Room의 roomPercentage 업데이트
List<UserRoom> userRoomList = userRoomCommandPort.findAllByRoomId(record.getRoomId());
Double totalUserPercentage = userRoomList.stream()
.map(UserRoom::getUserPercentage)
.reduce(0.0, Double::sum);
room.updateRoomPercentage(totalUserPercentage / userRoomList.size());
}
}

private void validateUserRoom(UserRoom userRoom) {
// UserRoom의 총평 작성 가능 여부 검증
if (!userRoom.canWriteOverview()) {
String message = String.format(
"총평(isOverview)은 사용자 진행률이 80%% 이상일 때만 가능합니다. 현재 사용자 진행률 = %.2f%%",
userRoom.getUserPercentage()
);
throw new InvalidStateException(RECORD_CANNOT_BE_OVERVIEW, new IllegalStateException(message));
}
}

private void validateRoom(Room room) {
// 방이 만료되었는지 검증
if (room.isExpired()) {
throw new BusinessException(RECORD_CANNOT_WRITE_IN_EXPIRED_ROOM);
}
}

private void validateRecord(Record record, Book book) {
// 페이지 유효성 검증
record.validatePage(book.getPageCount());

// 총평 유효성 검증
record.validateOverview(book.getPageCount());
}
}

This file was deleted.

43 changes: 43 additions & 0 deletions src/main/java/konkuk/thip/record/domain/Record.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package konkuk.thip.record.domain;

import konkuk.thip.common.entity.BaseDomainEntity;
import konkuk.thip.common.exception.InvalidStateException;
import lombok.Getter;
import lombok.experimental.SuperBuilder;

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

@Getter
@SuperBuilder
public class Record extends BaseDomainEntity {
Expand All @@ -19,4 +22,44 @@ public class Record extends BaseDomainEntity {
private boolean isOverview;

private Long roomId;

public static Record withoutId(
String content,
Long creatorId,
Integer page,
boolean isOverview,
Long roomId
) {
return Record.builder()
.content(content)
.creatorId(creatorId)
.page(page)
.isOverview(isOverview)
.roomId(roomId)
.build();
}


public void validateOverview(int totalPageCount) {
// 총평 기록 생성 요청인데 page가 책의 전체 페이지 수가 아니라면 에러
if (isOverview && page != totalPageCount) {
String message = String.format(
"총평(isOverview)은 책의 전체 페이지 수(%d)와 동일한 페이지에서만 작성할 수 있습니다. 현재 페이지 = %d",
totalPageCount, page
);
throw new InvalidStateException(RECORD_CANNOT_BE_OVERVIEW, new IllegalArgumentException(message));
}
}

public void validatePage(int totalPageCount) {
if (page < 1 || page > totalPageCount) {
String message = String.format(
"페이지 범위가 잘못되었습니다. 현재 기록할 page = %d, 책 전체 page = %d",
page, totalPageCount
);
throw new InvalidStateException(INVALID_RECORD_PAGE_RANGE,
new IllegalArgumentException(message)
);
}
}
Comment on lines +43 to +64
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.

LGTM 저도 투표 관련 validation 로직 참고해서 수정해보겠습니다!

}
9 changes: 9 additions & 0 deletions src/main/java/konkuk/thip/room/domain/Room.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package konkuk.thip.room.domain;

import konkuk.thip.common.entity.BaseDomainEntity;
import konkuk.thip.common.entity.StatusType;
import lombok.Getter;
import lombok.experimental.SuperBuilder;

Expand Down Expand Up @@ -31,4 +32,12 @@ public class Room extends BaseDomainEntity {
private Long bookId;

private Long categoryId;

public boolean isExpired() {
return this.getStatus() == StatusType.EXPIRED;
}

public void updateRoomPercentage(double roomPercentage) {
this.roomPercentage = roomPercentage;
}
}
Loading