Skip to content
12 changes: 11 additions & 1 deletion src/docs/asciidoc/record-query.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ include::{snippets}/record-controller-test/특정_책_기록_감정_필터_조
include::{snippets}/record-controller-test/특정_책_기록_감정_필터_조회_성공/http-response.adoc[opts=optional]
include::{snippets}/record-controller-test/특정_책_기록_감정_필터_조회_성공/response-fields.adoc[opts=optional]

=== 기록 개별 조회
기록 ID로 단건 기록을 조회합니다.
이미지가 연결된 기록은 저장된 key를 기준으로 조회 시점에 presigned GET URL로 변환된 `imgUrls` 로 내려갑니다.

include::{snippets}/record-controller-test/기록_개별_조회_성공/http-request.adoc[opts=optional]
include::{snippets}/record-controller-test/기록_개별_조회_성공/request-headers.adoc[opts=optional]
include::{snippets}/record-controller-test/기록_개별_조회_성공/path-parameters.adoc[opts=optional]
include::{snippets}/record-controller-test/기록_개별_조회_성공/http-response.adoc[opts=optional]
include::{snippets}/record-controller-test/기록_개별_조회_성공/response-fields.adoc[opts=optional]

=== 독서 기록 감정별 개수 조회
사용자가 작성한 전체 독서 기록 수와 감정별 기록 수를 함께 반환합니다.
특정 책의 전체 기록 수와 감정별 기록 수를 함께 반환합니다.

include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/http-request.adoc[opts=optional]
include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/request-headers.adoc[opts=optional]
Expand Down
9 changes: 3 additions & 6 deletions src/main/java/app/nook/library/domain/Library.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import app.nook.timeline.domain.Timeline;
import app.nook.user.domain.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -19,8 +16,8 @@

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Table(
name = "library",
uniqueConstraints = {
Expand Down
29 changes: 16 additions & 13 deletions src/main/java/app/nook/record/controller/RecordController.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ public ApiResponse<CursorResponse<BookRecordDto.BookRecordItemDto, String>> getU
RecordListCursorCodec.decode(cursor),
order
);

if (response.getItems().isEmpty()) {
return ApiResponse.onSuccess(null, SuccessCode.NO_CONTENT);
}

return ApiResponse.onSuccess(response, SuccessCode.OK);
}

Expand All @@ -64,20 +59,16 @@ public ApiResponse<CursorResponse<BookRecordDto.RecordItemDto, Long>> getBookRec
) {
CursorResponse<BookRecordDto.RecordItemDto, Long> response =
recordQueryService.getBookRecords(user, bookId, size, cursor, emotion);

if (response.getItems().isEmpty()) {
return ApiResponse.onSuccess(null, SuccessCode.NO_CONTENT);
}

return ApiResponse.onSuccess(response, SuccessCode.OK);
}

@GetMapping("/emotions")
@GetMapping("/emotions/{bookId}")
public ApiResponse<BookRecordDto.RecordEmotionCountResponse> getRecordEmotionCounts(
@CurrentUser User user
@CurrentUser User user,
@PathVariable Long bookId
) {
return ApiResponse.onSuccess(
recordQueryService.getRecordEmotionCounts(user),
recordQueryService.getRecordEmotionCounts(user,bookId),
SuccessCode.OK
);
}
Expand Down Expand Up @@ -121,4 +112,16 @@ public ApiResponse<RecordResponseDto.RecordCountDto> countRecords(
);
}

@GetMapping("/{recordId}")
public ApiResponse<BookRecordDto.RecordItemDto> getRecordDetail(
@CurrentUser User user,
@PathVariable Long recordId
) {
return ApiResponse.onSuccess(
recordQueryService.getRecordDetail(user, recordId),
SuccessCode.OK
);
}


}
2 changes: 1 addition & 1 deletion src/main/java/app/nook/record/domain/enums/Emotion.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public enum Emotion {

FUN, EMPATHIZING, USEFUL, COMPLICATED, SAD, UNCOMFORTABLE
FUN, EMPATHIZING, USEFUL, COMPLICATED, SAD, UNCOMFORTABLE, EMPTY
Comment thread
JiwonLee42 marked this conversation as resolved.

}
2 changes: 1 addition & 1 deletion src/main/java/app/nook/record/dto/BookRecordDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public record RecordItemDto(
){}

public record RecordEmotionDto(
Emotion emotion,
String emotion,
long recordCount
){}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ public List<BookRecordDto.BookRecordItemDto> findRecordsByCursor(
public List<Record> findBookRecordsByCursor(
Long userId, Long bookId, Long cursor, Emotion emotion, int size
);
public BookRecordDto.RecordEmotionCountResponse countRecordsByEmotion(Long userId);
public BookRecordDto.RecordEmotionCountResponse countRecordsByEmotion(Long userId, Long bookId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,22 +284,28 @@ public List<Record> findBookRecordsByCursor(
}

// 감상별 독서 기록 개수 조회
public BookRecordDto.RecordEmotionCountResponse countRecordsByEmotion(Long userId) {
public BookRecordDto.RecordEmotionCountResponse countRecordsByEmotion(Long userId, Long bookId) {
Long totalCount = Optional.ofNullable(
queryFactory
.select(record.count())
.from(record)
.where(record.library.user.id.eq(userId))
.where(
record.library.user.id.eq(userId),
record.library.book.id.eq(bookId)
)
.fetchOne()
).orElse(0L);

List<BookRecordDto.RecordEmotionDto> emotionCounts = queryFactory
.select(Projections.constructor(BookRecordDto.RecordEmotionDto.class,
record.emotion,
record.emotion.stringValue(),
record.count()
))
.from(record)
.where(record.library.user.id.eq(userId))
.where(
record.library.user.id.eq(userId),
record.library.book.id.eq(bookId)
)
.groupBy(record.emotion)
.fetch();
Comment thread
JiwonLee42 marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ List<Record> findRecentByLibraryId(

@EntityGraph(attributePaths = "images")
Optional<Record> findWithImagesById(Long id);

@EntityGraph(attributePaths = {"images", "library", "library.user"})
Optional<Record> findWithDetailById(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import app.nook.library.repository.LibraryRepository;
import app.nook.record.domain.Record;
import app.nook.record.domain.RecordImage;
import app.nook.record.domain.enums.Emotion;
import app.nook.record.dto.RecordRequestDto;
import app.nook.record.dto.RecordResponseDto;
import app.nook.record.dto.RecordUpdateRequestDto;
Expand Down Expand Up @@ -73,7 +74,7 @@ public void createRecord(

Record newRecord = Record.create(
library,
requestDto.emotion(),
normalizeEmotion(requestDto.emotion()),
requestDto.content()
);

Expand Down Expand Up @@ -104,7 +105,7 @@ public void updateRecord(
throw new CustomException(RecordErrorCode.RECORD_NOT_AUTHORIZED);
}

record.update(requestDto.content(), requestDto.emotion());
record.update(requestDto.content(), normalizeEmotion(requestDto.emotion()));

// 이미지 업데이트 시에 동기화 처리
syncRecordImages(record, requestedImageKeys);
Expand Down Expand Up @@ -150,6 +151,10 @@ private List<String> filterImageKeys(List<String> imageKeys) {
.toList();
}

private Emotion normalizeEmotion(Emotion emotion) {
return emotion == null ? Emotion.EMPTY : emotion;
}

// 기록 이미지 저장
private void saveRecordImages(Record record, List<String> imageKeys) {
for (int index = 0; index < imageKeys.size(); index++) {
Expand Down
49 changes: 46 additions & 3 deletions src/main/java/app/nook/record/service/RecordQueryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import app.nook.record.domain.enums.SortType;
import app.nook.record.dto.BookRecordDto;
import app.nook.record.dto.RecordListCursor;
import app.nook.record.exception.RecordErrorCode;
import app.nook.record.repository.RecordRepository;
import app.nook.record.util.RecordListCursorCodec;
import app.nook.r2.service.PresignedUrlService;
Expand All @@ -21,7 +22,10 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -78,8 +82,33 @@ public CursorResponse<BookRecordDto.BookRecordItemDto, String> getUserRecords(
}

// 독서 기록 감상별 개수 조회
public BookRecordDto.RecordEmotionCountResponse getRecordEmotionCounts(User user) {
return recordRepository.countRecordsByEmotion(user.getId());
public BookRecordDto.RecordEmotionCountResponse getRecordEmotionCounts(User user, Long bookId) {
if (!bookRepository.existsById(bookId)) {
throw new CustomException(BookErrorCode.BOOK_NOT_FOUND);
}

if (!libraryRepository.existsByUserIdAndBookId(user.getId(), bookId)) {
throw new CustomException(LibraryErrorCode.BOOK_NOT_EXIST);
}

BookRecordDto.RecordEmotionCountResponse response = recordRepository.countRecordsByEmotion(user.getId(), bookId);

Map<Emotion, Long> emotionCountMap = new EnumMap<>(Emotion.class);
response.emotionCounts().forEach(item -> emotionCountMap.put(Emotion.valueOf(item.emotion()), item.recordCount()));

List<BookRecordDto.RecordEmotionDto> normalizedEmotionCounts = Arrays.stream(Emotion.values())
.filter(emotion -> emotion != Emotion.EMPTY)
.map(emotion -> new BookRecordDto.RecordEmotionDto(
emotion.name(),
emotionCountMap.getOrDefault(emotion, 0L)
))
.toList();

List<BookRecordDto.RecordEmotionDto> emotionCountsWithAll = new java.util.ArrayList<>();
emotionCountsWithAll.add(new BookRecordDto.RecordEmotionDto("ALL", response.totalCount()));
emotionCountsWithAll.addAll(normalizedEmotionCounts);

return new BookRecordDto.RecordEmotionCountResponse(response.totalCount(), emotionCountsWithAll);
Comment thread
JiwonLee42 marked this conversation as resolved.
}

// 해당 책의 독서 기록 목록 조회
Expand Down Expand Up @@ -146,10 +175,24 @@ private Emotion parseEmotionFilter(String emotion) {
}

try {
return Emotion.valueOf(emotion.trim().toUpperCase());
Emotion parsedEmotion = Emotion.valueOf(emotion.trim().toUpperCase());
if (parsedEmotion == Emotion.EMPTY) {
throw new CustomException(CommonErrorCode.INVALID_REQUEST);
}
return parsedEmotion;
} catch (IllegalArgumentException exception) {
throw new CustomException(CommonErrorCode.INVALID_REQUEST);
}
}

public BookRecordDto.RecordItemDto getRecordDetail(User user, Long recordId) {
Record record = recordRepository.findWithDetailById(recordId)
.orElseThrow(() -> new CustomException(RecordErrorCode.RECORD_NOT_FOUND));

if (!record.getLibrary().getUser().getId().equals(user.getId())) {
throw new CustomException(RecordErrorCode.RECORD_NOT_AUTHORIZED);
}

return recordConverter.toRecordItemDto(user.getId(), record);
}
}
10 changes: 10 additions & 0 deletions src/main/java/app/nook/user/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ public ApiResponse<UserDTO.LoginResponse> devSignUp(
);
}

@PostMapping("/reissue")
public ApiResponse<UserDTO.TokenReissueResponse> reissueAccessToken(
@Valid @RequestBody UserDTO.TokenReissueRequest request
) {
return ApiResponse.onSuccess(
userService.reissueAccessToken(request.getRefreshToken()),
SuccessCode.OK
);
}

// 현재 로그인한 유저 확인
@GetMapping("/me")
public ApiResponse<UserDTO.LoginResponse> user(
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/app/nook/user/dto/UserDTO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package app.nook.user.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -17,21 +20,45 @@ public static class LoginResponse {
private String email;
private String nickName;
private String accessToken;
private String refreshToken;
}

@AllArgsConstructor
@Getter
@NoArgsConstructor
public static class DevLoginRequest{
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "올바르지 않은 이메일 형식입니다.")
private String email;

@NotBlank(message = "닉네임은 필수입니다.")
@Pattern(regexp = "^[a-zA-Z0-9가-힣]{2,20}$", message = "닉네임은 2~20자의 영문, 숫자, 한글만 사용할 수 있습니다.")
private String nickName;
}

@AllArgsConstructor
@Getter
@NoArgsConstructor
public static class DevSignUpRequest {
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "올바르지 않은 이메일 형식입니다.")
private String email;

@NotBlank(message = "닉네임은 필수입니다.")
@Pattern(regexp = "^[a-zA-Z0-9가-힣]{2,20}$", message = "닉네임은 2~20자의 영문, 숫자, 한글만 사용할 수 있습니다.")
private String nickName;
}

@AllArgsConstructor
@Getter
@NoArgsConstructor
public static class TokenReissueRequest {
@NotBlank(message = "리프레시토큰은 필수입니다.")
private String refreshToken;
}

public record TokenReissueResponse(
String accessToken,
String refreshToken
) {}
}
Loading
Loading