Skip to content

Commit 18db74e

Browse files
authored
Merge pull request #62 from Project-BookLog/feat/reading_log/1
[FEAT] 독서 기록 CRUD API 및 도서 저장 및 수정 API 리펙토링
2 parents 881b622 + 8c53ce3 commit 18db74e

24 files changed

+745
-119
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.example.booklog.domain.library.shelves.controller;
2+
3+
import com.example.booklog.domain.library.shelves.dto.ReadingLogResponse;
4+
import com.example.booklog.domain.library.shelves.dto.ReadingLogSaveRequest;
5+
import com.example.booklog.domain.library.shelves.service.ReadingLogsService;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.media.Content;
9+
import io.swagger.v3.oas.annotations.media.ExampleObject;
10+
import io.swagger.v3.oas.annotations.media.Schema;
11+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.http.MediaType;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
@Tag(name = "독서 기록", description = "독서 기록(날짜/읽은 페이지) 저장·수정·삭제 API")
20+
@RestController
21+
@RequiredArgsConstructor
22+
public class ReadingLogsController {
23+
24+
private final ReadingLogsService readingLogsService;
25+
26+
@Operation(
27+
summary = "독서 기록 저장",
28+
description = """
29+
특정 저장 도서(userBookId)에 대해 독서 기록을 추가(append)합니다.
30+
- UI 입력: readDate(읽은 날짜), pagesRead(그날 읽은 페이지 수)
31+
- 서버 처리: currentPage(누적 현재 페이지)는 서버가 계산하여 저장하며,
32+
user_books.current_page/progress_percent 등의 최신 상태도 함께 갱신됩니다.
33+
"""
34+
)
35+
@ApiResponse(responseCode = "200", description = "저장 성공",
36+
content = @Content(schema = @Schema(implementation = ReadingLogResponse.class)))
37+
@ApiResponse(responseCode = "400", description = "요청값 오류/저장 도서 없음", content = @Content)
38+
@PostMapping(value = "/api/v1/user-books/{userBookId}/reading-logs", consumes = MediaType.APPLICATION_JSON_VALUE)
39+
public ReadingLogResponse create(
40+
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
41+
@RequestHeader("X-USER-ID") Long userId,
42+
43+
@Parameter(description = "저장 도서 ID(user_books.user_book_id)", required = true, example = "101")
44+
@PathVariable Long userBookId,
45+
46+
@RequestBody(
47+
required = true,
48+
description = "독서 기록 저장 요청",
49+
content = @Content(
50+
schema = @Schema(implementation = ReadingLogSaveRequest.class),
51+
examples = {
52+
@ExampleObject(
53+
name = "기본 예시",
54+
summary = "특정 날짜에 57페이지 읽음",
55+
value = """
56+
{
57+
"readDate": "2026-01-10",
58+
"pagesRead": 57
59+
}
60+
"""
61+
)
62+
}
63+
)
64+
)
65+
@org.springframework.web.bind.annotation.RequestBody @Valid ReadingLogSaveRequest req
66+
) {
67+
return readingLogsService.create(userId, userBookId, req);
68+
}
69+
70+
@Operation(
71+
summary = "독서 기록 수정",
72+
description = """
73+
특정 독서 기록(logId)의 날짜/읽은 페이지를 수정합니다.
74+
- UI 입력: readDate, pagesRead
75+
- 서버 처리: 중간 기록 수정 시 누적 currentPage가 연쇄 변경될 수 있어,
76+
해당 userBook의 로그들을 기준으로 currentPage/user_books 상태를 재계산합니다.
77+
"""
78+
)
79+
@ApiResponse(responseCode = "200", description = "수정 성공",
80+
content = @Content(schema = @Schema(implementation = ReadingLogResponse.class)))
81+
@ApiResponse(responseCode = "404", description = "독서 기록 없음/권한 없음", content = @Content)
82+
@PatchMapping(value = "/api/v1/reading-logs/{logId}", consumes = MediaType.APPLICATION_JSON_VALUE)
83+
public ReadingLogResponse update(
84+
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
85+
@RequestHeader("X-USER-ID") Long userId,
86+
87+
@Parameter(description = "독서 기록 ID(reading_logs.log_id)", required = true, example = "5001")
88+
@PathVariable Long logId,
89+
90+
@RequestBody(
91+
required = true,
92+
description = "독서 기록 수정 요청",
93+
content = @Content(
94+
schema = @Schema(implementation = ReadingLogSaveRequest.class),
95+
examples = {
96+
@ExampleObject(
97+
name = "수정 예시",
98+
summary = "페이지 수를 80으로 수정",
99+
value = """
100+
{
101+
"readDate": "2026-01-10",
102+
"pagesRead": 80
103+
}
104+
"""
105+
)
106+
}
107+
)
108+
)
109+
@org.springframework.web.bind.annotation.RequestBody @Valid ReadingLogSaveRequest req
110+
) {
111+
return readingLogsService.update(userId, logId, req);
112+
}
113+
114+
@Operation(
115+
summary = "독서 기록 삭제",
116+
description = """
117+
특정 독서 기록(logId)을 삭제합니다.
118+
- 서버 처리: 삭제 후 해당 userBook의 로그 기반으로 user_books 최신상태를 재계산합니다.
119+
"""
120+
)
121+
@ApiResponse(responseCode = "204", description = "삭제 성공", content = @Content)
122+
@ApiResponse(responseCode = "404", description = "독서 기록 없음/권한 없음", content = @Content)
123+
@DeleteMapping("/api/v1/reading-logs/{logId}")
124+
@ResponseStatus(org.springframework.http.HttpStatus.NO_CONTENT)
125+
public void delete(
126+
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
127+
@RequestHeader("X-USER-ID") Long userId,
128+
129+
@Parameter(description = "독서 기록 ID(reading_logs.log_id)", required = true, example = "5001")
130+
@PathVariable Long logId
131+
) {
132+
readingLogsService.delete(userId, logId);
133+
}
134+
}

booklog/src/main/java/com/example/booklog/domain/library/shelves/controller/UserBooksController.java

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package com.example.booklog.domain.library.shelves.controller;
22

33
import com.example.booklog.domain.library.shelves.dto.*;
4+
import com.example.booklog.domain.library.shelves.entity.ReadingStatus;
5+
import com.example.booklog.domain.library.shelves.entity.UserBookSort;
46
import com.example.booklog.domain.library.shelves.service.UserBooksService;
57
import io.swagger.v3.oas.annotations.Operation;
68
import io.swagger.v3.oas.annotations.Parameter;
79
import io.swagger.v3.oas.annotations.media.ArraySchema;
10+
import io.swagger.v3.oas.annotations.media.Content;
811
import io.swagger.v3.oas.annotations.media.Schema;
912
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1013
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1114
import io.swagger.v3.oas.annotations.tags.Tag;
1215
import jakarta.validation.Valid;
1316
import lombok.RequiredArgsConstructor;
17+
import org.springframework.http.HttpStatus;
18+
import org.springframework.http.MediaType;
1419
import org.springframework.web.bind.annotation.*;
1520

1621
import java.util.List;
@@ -73,18 +78,18 @@ public UserBookCreateResponse create(
7378
@ApiResponse(responseCode = "400", description = "정렬/상태 값이 허용 범위를 벗어남")
7479
})
7580
@GetMapping
76-
public List<UserBookListItemResponse> list(
81+
public UserBookListResponse list(
7782
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
7883
@RequestHeader(name = "X-USER-ID") Long userId,
7984

8085
@Parameter(description = "서재 ID(선택)", example = "10")
8186
@RequestParam(name = "shelfId", required = false) Long shelfId,
8287

8388
@Parameter(description = "상태(선택)", example = "READING")
84-
@RequestParam(name = "status", required = false) String status,
89+
@RequestParam(name = "status", required = false) ReadingStatus status,
8590

8691
@Parameter(description = "정렬(선택). 기본 LATEST", example = "LATEST")
87-
@RequestParam(name = "sort", required = false, defaultValue = "LATEST") String sort
92+
@RequestParam(name = "sort", required = false, defaultValue = "LATEST") UserBookSort sort
8893
) {
8994
return userBooksService.listAll(userId, shelfId, status, sort);
9095
}
@@ -117,7 +122,7 @@ public int delete(
117122
@RequestParam(name = "shelfId", required = false) Long shelfId,
118123

119124
@Parameter(description = "상태(선택). 있으면 해당 상태를 라이브러리에서 전체 삭제", example = "STOPPED")
120-
@RequestParam(name = "status", required = false) String status
125+
@RequestParam(name = "status", required = false) ReadingStatus status
121126
) {
122127
List<Long> ids = (body == null) ? null : body.ids();
123128
return userBooksService.delete(userId, ids, shelfId, status);
@@ -155,33 +160,59 @@ public UserBookDetailResponse detail(
155160
}
156161

157162
@Operation(
158-
summary = "저장 도서 수정(상태 변경 + 서재 추가)",
163+
summary = "도서 총 페이지 입력",
159164
description = """
160-
저장 도서 일부 필드를 수정합니다. (PATCH)
165+
사용자가 직접 입력하는 총 페이지 수(pageCountSnapshot)를 저장합니다.
166+
- 책 메타(books)와 무관하게 user_books에 스냅샷으로 저장됩니다.
167+
- 입력 후 progressPercent는 currentPage 기준으로 재계산됩니다.
168+
"""
169+
)
170+
@ApiResponses({
171+
@ApiResponse(responseCode="204", description="저장 성공", content=@Content),
172+
@ApiResponse(responseCode="400", description="요청값 오류", content=@Content),
173+
@ApiResponse(responseCode="404", description="저장 도서 없음/권한 없음", content=@Content)
174+
})
175+
@PatchMapping(value = "/{userBookId}/total-page", consumes = MediaType.APPLICATION_JSON_VALUE)
176+
@ResponseStatus(HttpStatus.NO_CONTENT)
177+
public void saveTotalPage(
178+
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
179+
@RequestHeader("X-USER-ID") Long userId,
161180

162-
- 헤더: X-USER-ID (필수)
163-
- Path: userBookId
164-
- Body:
165-
- status: 선택
166-
- shelfId: 선택 → 해당 서재에 추가(A 방식)
167-
"""
181+
@Parameter(description = "저장 도서 ID(user_books.user_book_id)", required = true, example = "101")
182+
@PathVariable Long userBookId,
183+
184+
@RequestBody @Valid TotalPageSaveRequest req
185+
) {
186+
userBooksService.saveTotalPage(userId, userBookId, req);
187+
}
188+
189+
@Operation(
190+
summary = "저장 도서 수정(상태/책종류 변경, 서재 추가)",
191+
description = """
192+
user_books의 일부 필드만 변경합니다.
193+
- status: 읽기 상태 변경
194+
- format: 책 종류(종이/전자/오디오) 변경
195+
- shelfId: (A방식) 해당 서재에 '추가' (이동 아님)
196+
"""
168197
)
169198
@ApiResponses({
170-
@ApiResponse(responseCode = "200", description = "성공(수정 완료)"),
171-
@ApiResponse(responseCode = "400", description = "요청 값 오류"),
172-
@ApiResponse(responseCode = "403", description = "내 서재가 아님"),
173-
@ApiResponse(responseCode = "404", description = "저장 도서/서재 없음")
199+
@ApiResponse(responseCode="204", description="수정 성공", content=@Content),
200+
@ApiResponse(responseCode="400", description="요청값 오류", content=@Content),
201+
@ApiResponse(responseCode="403", description="내 서재가 아님", content=@Content),
202+
@ApiResponse(responseCode="404", description="저장 도서/서재 없음", content=@Content)
174203
})
175-
@PatchMapping("/{userBookId}")
204+
@PatchMapping(value = "/{userBookId}", consumes = MediaType.APPLICATION_JSON_VALUE)
205+
@ResponseStatus(HttpStatus.NO_CONTENT)
176206
public void update(
177207
@Parameter(name = "X-USER-ID", description = "유저 식별자(필수)", required = true, example = "1")
178-
@RequestHeader(name = "X-USER-ID") Long userId,
179-
180-
@Parameter(name = "userBookId", description = "저장 도서 ID(필수)", required = true, example = "100")
181-
@PathVariable(name = "userBookId") Long userBookId,
208+
@RequestHeader("X-USER-ID") Long userId,
182209

183-
@RequestBody UserBookUpdateRequest req
210+
@Parameter(description = "저장 도서 ID(user_books.user_book_id)", required = true, example = "101")
211+
@PathVariable Long userBookId,
212+
@RequestBody @Valid UserBookUpdateRequest req
184213
) {
185214
userBooksService.update(userId, userBookId, req);
186215
}
216+
217+
187218
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.example.booklog.domain.library.shelves.dto;
22

3+
import com.example.booklog.domain.library.shelves.entity.UserBookSort;
34
import jakarta.validation.constraints.NotBlank;
45

56
public record BookshelfCreateRequest(
67
@NotBlank String name,
78
Boolean isPublic,
8-
String sortOrder // LATEST/OLDEST/TITLE/AUTHOR (서재 기본 정렬 프리셋)
9+
UserBookSort sortOrder // LATEST/OLDEST/TITLE/AUTHOR (서재 기본 정렬 프리셋)
910
) {}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.example.booklog.domain.library.shelves.dto;
22

3+
import com.example.booklog.domain.library.shelves.entity.UserBookSort;
4+
35
import java.util.List;
46

57
public record BookshelfListItemResponse(
68
Long shelfId,
79
String name,
810
boolean isPublic,
9-
String sortOrder,
11+
UserBookSort sortOrder,
1012
List<ShelfPreviewBookResponse> previewBooks // ✅ 서재 내 상위 3권 프리뷰
1113
) {}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.booklog.domain.library.shelves.dto;
22

3+
import com.example.booklog.domain.library.shelves.entity.UserBookSort;
34
import jakarta.validation.constraints.Size;
45

56
public record BookshelfUpdateRequest(
@@ -8,5 +9,5 @@ public record BookshelfUpdateRequest(
89

910
Boolean isPublic,
1011

11-
String sortOrder
12+
UserBookSort sortOrder
1213
) {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.booklog.domain.library.shelves.dto;
2+
3+
import java.time.LocalDate;
4+
5+
public record ReadingLogResponse(
6+
Long logId,
7+
Long userBookId,
8+
LocalDate readDate,
9+
Integer pagesRead,
10+
Integer currentPage
11+
) {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.booklog.domain.library.shelves.dto;
2+
3+
import jakarta.validation.constraints.Min;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.time.LocalDate;
7+
8+
public record ReadingLogSaveRequest(
9+
@NotNull LocalDate readDate,
10+
@NotNull @Min(0) Integer pagesRead
11+
) {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.booklog.domain.library.shelves.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Min;
5+
import jakarta.validation.constraints.NotNull;
6+
7+
public record TotalPageSaveRequest(
8+
@Schema(description = "사용자 입력 총 페이지 수(판본/앱 기준)", example = "312")
9+
@NotNull @Min(1) Integer pageCountSnapshot
10+
) {}

booklog/src/main/java/com/example/booklog/domain/library/shelves/dto/UserBookCreateRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
public record UserBookCreateRequest(
88
@NotNull Long bookId,
99
@Nullable Long shelfId,
10-
String status // TO_READ/READING/DONE/STOPPED
10+
ReadingStatus status // TO_READ/READING/DONE/STOPPED
1111
) {}

booklog/src/main/java/com/example/booklog/domain/library/shelves/dto/UserBookDetailResponse.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.example.booklog.domain.library.shelves.dto;
22

3+
import com.example.booklog.domain.library.shelves.entity.BookFormat;
4+
import com.example.booklog.domain.library.shelves.entity.ReadingStatus;
5+
36
import java.time.LocalDate;
47

58
public record UserBookDetailResponse(
69
Long userBookId,
7-
String status,
10+
ReadingStatus status,
811
int progressPercent,
912
Integer currentPage,
1013
LocalDate startDate,
1114
LocalDate endDate,
12-
String format,
15+
BookFormat format,
1316
Integer pageCountSnapshot,
1417

1518
Long bookId,

0 commit comments

Comments
 (0)