Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ public String generateReviewKeyName(Uuid uuid) {
public String generateProfileKeyName(Uuid uuid) {
return amazonConfig.getProfilePath() + '/' + uuid.getUuid();
}


}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// =====================================
// [서재] BookshelvesController
// =====================================
package com.example.booklog.domain.library.shelves.controller;

import com.example.booklog.domain.library.shelves.dto.*;
import com.example.booklog.domain.library.shelves.service.BookshelvesService;
import com.example.booklog.global.auth.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -25,101 +29,95 @@ public class BookshelvesController {
summary = "서재 생성",
description = """
새로운 서재를 생성합니다.
- 헤더: X-USER-ID (필수)
- 인증: Access Token(Bearer)
- Body: name(필수), isPublic(선택), sortOrder(선택)
- 응답: shelfId
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패"),
@ApiResponse(responseCode = "401", description = "유저 식별 실패"),
@ApiResponse(responseCode = "401", description = "인증 실패(토큰 누락/만료/위조)"),
@ApiResponse(responseCode = "409", description = "중복 서재명")
})
@PostMapping
public BookshelfCreateResponse create(
@Parameter(name = "X-USER-ID", description = "유저 식별자", required = true, example = "1")
@RequestHeader(name = "X-USER-ID") Long userId,
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody @Valid BookshelfCreateRequest req
) {
return bookshelvesService.create(userId, req);
return bookshelvesService.create(userDetails.getUserId(), req);
}

@Operation(
summary = "서재 목록 조회(프리뷰 3권 포함)",
description = """
내 서재 목록을 조회합니다.
- 헤더: X-USER-ID (필수)
- 인증: Access Token(Bearer)
- 응답: 서재 리스트 + 각 서재별 previewBooks(최대 3권)
- previewBooks: 썸네일, 제목, 저자명, 출판사
- previewBooks: 썸네일, 제목, 저자명, 출판사(리스트 UI용)
"""
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "401", description = "유저 식별 실패")
@ApiResponse(responseCode = "401", description = "인증 실패")
})
@GetMapping
public List<BookshelfListItemResponse> list(
@Parameter(name = "X-USER-ID", description = "유저 식별자", required = true, example = "1")
@RequestHeader(name = "X-USER-ID") Long userId
@AuthenticationPrincipal CustomUserDetails userDetails
) {
return bookshelvesService.list(userId);
return bookshelvesService.list(userDetails.getUserId());
}

@Operation(
summary = "서재 수정(PATCH)",
description = """
서재의 일부 필드를 변경합니다.
- 헤더: X-USER-ID (필수)
- 인증: Access Token(Bearer)
- Path: shelfId
- Body: name/isPublic/sortOrder 중 변경할 값만 전달
- 응답: 204 No Content
"""
)
@ApiResponses({
@ApiResponse(responseCode = "204", description = "성공"),
@ApiResponse(responseCode = "401", description = "유저 식별 실패"),
@ApiResponse(responseCode = "404", description = "서재 없음 또는 내 서재 아님")
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "서재 없음 또는 내 서재 아님"),
@ApiResponse(responseCode = "409", description = "중복 서재명")
})
@PatchMapping("/{shelfId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void update(
@Parameter(name = "X-USER-ID", description = "유저 식별자", required = true, example = "1")
@RequestHeader(name = "X-USER-ID") Long userId,

@Parameter(name = "shelfId", description = "서재 ID", required = true, example = "10")
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable(name = "shelfId") Long shelfId,

@RequestBody BookshelfUpdateRequest req
) {
bookshelvesService.update(userId, shelfId, req);
bookshelvesService.update(userDetails.getUserId(), shelfId, req);
}

@Operation(
summary = "서재 삭제(UNASSIGN 고정)",
description = """
서재를 삭제합니다.
- 헤더: X-USER-ID (필수)
- 인증: Access Token(Bearer)
- Path: shelfId
- 정책: bookshelf_items(서재-책 매핑)만 제거 후 서재 삭제
user_books(라이브러리)는 유지
- 정책(UNASSIGN):
- bookshelf_items(서재-책 매핑)만 제거 후 서재 삭제
- user_books(라이브러리)는 유지
- 응답: 204 No Content
"""
)
@ApiResponses({
@ApiResponse(responseCode = "204", description = "성공"),
@ApiResponse(responseCode = "401", description = "유저 식별 실패"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "404", description = "서재 없음 또는 내 서재 아님")
})
@DeleteMapping("/{shelfId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
@Parameter(name = "X-USER-ID", description = "유저 식별자", required = true, example = "1")
@RequestHeader(name = "X-USER-ID") Long userId,

@Parameter(name = "shelfId", description = "서재 ID", required = true, example = "10")
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable(name = "shelfId") Long shelfId
) {
bookshelvesService.delete(userId, shelfId);
bookshelvesService.delete(userDetails.getUserId(), shelfId);
}
}
Original file line number Diff line number Diff line change
@@ -1,134 +1,108 @@
// =====================================
// [독서기록] ReadingLogsController
// =====================================
package com.example.booklog.domain.library.shelves.controller;

import com.example.booklog.domain.library.shelves.dto.ReadingLogResponse;
import com.example.booklog.domain.library.shelves.dto.ReadingLogSaveRequest;
import com.example.booklog.domain.library.shelves.service.ReadingLogsService;
import com.example.booklog.global.auth.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter.*;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid or unused import statement.

io.swagger.v3.oas.annotations.Parameter is an annotation type, not a class with nested members. The wildcard import Parameter.* is syntactically incorrect for annotations and this import doesn't appear to be used anywhere in the file.

🔧 Suggested fix
-import io.swagger.v3.oas.annotations.Parameter.*;

If you intended to use @Parameter annotation for path variables, the correct import would be:

import io.swagger.v3.oas.annotations.Parameter;
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/library/shelves/controller/ReadingLogsController.java`
at line 14, The file ReadingLogsController.java contains an invalid/unused
import "import io.swagger.v3.oas.annotations.Parameter.*;" — replace it with the
correct annotation import "io.swagger.v3.oas.annotations.Parameter" if you
intend to use the `@Parameter` annotation, or simply remove the import entirely if
`@Parameter` is not used in ReadingLogsController; update the import statement
accordingly so it is syntactically correct and only present when referenced by
the controller code.

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "독서 기록", description = "독서 기록(날짜/읽은 페이지) 저장·수정·삭제 API")
@Tag(name = "독서 기록(Reading Logs)", description = "독서 기록 저장·수정·삭제 API")
@RestController
@RequiredArgsConstructor
public class ReadingLogsController {

private final ReadingLogsService readingLogsService;

@Operation(
summary = "독서 기록 저장",
summary = "독서 기록 저장(append)",
description = """
특정 저장 도서(userBookId)에 대해 독서 기록을 추가(append)합니다.
- UI 입력: readDate(읽은 날짜), pagesRead(그날 읽은 페이지 수)
- 서버 처리: currentPage(누적 현재 페이지)는 서버가 계산하여 저장하며,
user_books.current_page/progress_percent 등의 최신 상태도 함께 갱신됩니다.
- 인증: Access Token(Bearer)
- 입력: readDate(날짜), pagesRead(그날 읽은 페이지 수)
- 처리: 누적 currentPage는 서버 계산, user_books 최신상태 함께 갱신
"""
)
@ApiResponse(responseCode = "200", description = "저장 성공",
content = @Content(schema = @Schema(implementation = ReadingLogResponse.class)))
@ApiResponse(responseCode = "400", description = "요청값 오류/저장 도서 없음", content = @Content)
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content)
@PostMapping(value = "/api/v1/user-books/{userBookId}/reading-logs", consumes = MediaType.APPLICATION_JSON_VALUE)
public ReadingLogResponse create(
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
@RequestHeader("X-USER-ID") Long userId,

@Parameter(description = "저장 도서 ID(user_books.user_book_id)", required = true, example = "101")
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long userBookId,

@RequestBody(
//스웨거(OpenAPI 문서용) RequestBody
@io.swagger.v3.oas.annotations.parameters.RequestBody(
required = true,
description = "독서 기록 저장 요청",
content = @Content(
schema = @Schema(implementation = ReadingLogSaveRequest.class),
examples = {
@ExampleObject(
name = "기본 예시",
summary = "특정 날짜에 57페이지 읽음",
value = """
{
"readDate": "2026-01-10",
"pagesRead": 57
}
"""
)
}
examples = @ExampleObject(
name = "기본 예시",
summary = "특정 날짜에 57페이지 읽음",
value = """
{
"readDate": "2026-01-10",
"pagesRead": 57
}
"""
)
)
)
@org.springframework.web.bind.annotation.RequestBody @Valid ReadingLogSaveRequest req
) {
return readingLogsService.create(userId, userBookId, req);
return readingLogsService.create(userDetails.getUserId(), userBookId, req);
}

@Operation(
summary = "독서 기록 수정",
description = """
특정 독서 기록(logId)의 날짜/읽은 페이지를 수정합니다.
- UI 입력: readDate, pagesRead
- 서버 처리: 중간 기록 수정 시 누적 currentPage가 연쇄 변경될 수 있어,
해당 userBook의 로그들을 기준으로 currentPage/user_books 상태를 재계산합니다.
- 인증: Access Token(Bearer)
- 처리: 중간 기록 수정 시 누적값이 연쇄 변경될 수 있어 user_books 상태 재계산
"""
)
@ApiResponse(responseCode = "200", description = "수정 성공",
content = @Content(schema = @Schema(implementation = ReadingLogResponse.class)))
@ApiResponse(responseCode = "404", description = "독서 기록 없음/권한 없음", content = @Content)
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content)
@PatchMapping(value = "/api/v1/reading-logs/{logId}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ReadingLogResponse update(
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
@RequestHeader("X-USER-ID") Long userId,

@Parameter(description = "독서 기록 ID(reading_logs.log_id)", required = true, example = "5001")
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long logId,

@RequestBody(
required = true,
description = "독서 기록 수정 요청",
content = @Content(
schema = @Schema(implementation = ReadingLogSaveRequest.class),
examples = {
@ExampleObject(
name = "수정 예시",
summary = "페이지 수를 80으로 수정",
value = """
{
"readDate": "2026-01-10",
"pagesRead": 80
}
"""
)
}
)
)
@org.springframework.web.bind.annotation.RequestBody @Valid ReadingLogSaveRequest req
) {
return readingLogsService.update(userId, logId, req);
return readingLogsService.update(userDetails.getUserId(), logId, req);
}

@Operation(
summary = "독서 기록 삭제",
description = """
특정 독서 기록(logId)을 삭제합니다.
- 서버 처리: 삭제 후 해당 userBook의 로그 기반으로 user_books 최신상태를 재계산합니다.
- 인증: Access Token(Bearer)
- 처리: 삭제 후 해당 userBook 로그 기반으로 user_books 최신상태 재계산
"""
)
@ApiResponse(responseCode = "204", description = "삭제 성공", content = @Content)
@ApiResponse(responseCode = "404", description = "독서 기록 없음/권한 없음", content = @Content)
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content)
@DeleteMapping("/api/v1/reading-logs/{logId}")
@ResponseStatus(org.springframework.http.HttpStatus.NO_CONTENT)
public void delete(
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
@RequestHeader("X-USER-ID") Long userId,

@Parameter(description = "독서 기록 ID(reading_logs.log_id)", required = true, example = "5001")
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long logId
) {
readingLogsService.delete(userId, logId);
readingLogsService.delete(userDetails.getUserId(), logId);
}
}
Loading