From a89176749ecb035ebb92d725d9e24900b2ca2588 Mon Sep 17 00:00:00 2001 From: Chaemin Yu Date: Fri, 8 Aug 2025 00:12:32 +0900 Subject: [PATCH 1/2] fix: streak data structure --- .gitignore | 2 ++ package-lock.json | 18 +++++++++++ package.json | 5 +++ .../studylog/service/StreakService.java | 32 ++++--------------- 4 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 1153499..aad2a19 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,8 @@ Icon # Thumbnails ._* +node_modules + # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b247914 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "study-log", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "claude": "^0.1.1" + } + }, + "node_modules/claude": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/claude/-/claude-0.1.1.tgz", + "integrity": "sha512-j7oSibqQdIODNhkI1sEJzHMiPsF43L/GqNbcA+eDDyGM10+x2sH9NW/PK6vM3z0J2tLDKMBcc5ZjVaoRinhuCA==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1353701 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "claude": "^0.1.1" + } +} diff --git a/src/main/java/org/example/studylog/service/StreakService.java b/src/main/java/org/example/studylog/service/StreakService.java index b248cb9..222d8db 100644 --- a/src/main/java/org/example/studylog/service/StreakService.java +++ b/src/main/java/org/example/studylog/service/StreakService.java @@ -11,7 +11,7 @@ import java.time.LocalDate; import java.time.YearMonth; import java.time.format.DateTimeFormatter; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; @Service @@ -22,11 +22,13 @@ public class StreakService { private final StudyRecordRepository studyRecordRepository; private final UserRepository userRepository; + + @Transactional(readOnly = true) public Map getMonthlyStreakData(User user, String year, String month) { log.info("월별 스트릭 데이터 조회 시작: 사용자={}, {}년 {}월", user.getOauthId(), year, month); - Map streakData = new HashMap<>(); + Map streakData = new LinkedHashMap<>(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // YearMonth 객체 생성 @@ -41,35 +43,13 @@ public Map getMonthlyStreakData(User user, String year, String LocalDate date = LocalDate.of(yearInt, monthInt, day); Long recordCount = studyRecordRepository.countByUserAndCreateDateDate(user, date); - // 기록이 있는 날만 Map에 추가 -// if (recordCount > 0) { -// streakData.put(date.format(formatter), recordCount.intValue()); -// } - // 수정: 모든 날 반환 + // 모든 날짜를 순서대로 LinkedHashMap에 추가 streakData.put(date.format(formatter), recordCount.intValue()); } - log.info("월별 스트릭 데이터 조회 완료: 사용자={}, {}년 {}월, 기록 있는 날수={}", + log.info("월별 스트릭 데이터 조회 완료: 사용자={}, {}년 {}월, 총 일수={}", user.getOauthId(), year, month, streakData.size()); return streakData; } - @Transactional(readOnly = true) - public Map getMonthlyStreakDataByCode(String code, String year, String month) { - log.info("코드로 월별 스트릭 데이터 조회 시작: code={}, {}년 {}월", code, year, month); - - // code로 사용자 조회 - User user = userRepository.findByCode(code) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자 코드입니다.")); - - log.info("코드로 사용자 조회 완료: code={}, 사용자={}", code, user.getOauthId()); - - // 기존 getMonthlyStreakData 메서드 로직 재사용 - Map streakData = getMonthlyStreakData(user, year, month); - - log.info("코드로 월별 스트릭 데이터 조회 완료: code={}, 사용자={}, {}년 {}월, 기록 있는 날수={}", - code, user.getOauthId(), year, month, streakData.size()); - - return streakData; - } } \ No newline at end of file From ef93d722b05375d8d5e713b9fc7d596c5e75172d Mon Sep 17 00:00:00 2001 From: Chaemin Yu Date: Fri, 8 Aug 2025 00:50:00 +0900 Subject: [PATCH 2/2] feat: streak, main, category, record - Swagger --- .../controller/CategoryController.java | 53 ++++++++++- .../studylog/controller/MainController.java | 83 +++++++++++++++++- .../studylog/controller/StreakController.java | 87 ++++++++++++++++++- .../controller/StudyRecordController.java | 71 +++++++++++++-- 4 files changed, 277 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/example/studylog/controller/CategoryController.java b/src/main/java/org/example/studylog/controller/CategoryController.java index 30fa843..6d73286 100644 --- a/src/main/java/org/example/studylog/controller/CategoryController.java +++ b/src/main/java/org/example/studylog/controller/CategoryController.java @@ -1,5 +1,13 @@ package org.example.studylog.controller; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.studylog.dto.category.CreateCategoryRequestDTO; @@ -23,14 +31,27 @@ @RequiredArgsConstructor @Validated @Slf4j +@Tag(name = "Categories", description = "카테고리 관리 API") public class CategoryController { private final CategoryService categoryService; private final UserRepository userRepository; + @Operation(summary = "카테고리 생성", description = "새로운 카테고리를 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "카테고리 생성 성공", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\"status\": 201, \"message\": \"카테고리가 성공적으로 생성되었습니다\", \"data\": {\"id\": 1, \"name\": \"Spring Boot\", \"color\": \"#FF5733\"}}"))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 (중복된 카테고리명 등)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @PostMapping public ResponseEntity createCategory( - @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, @Valid @RequestBody CreateCategoryRequestDTO requestDTO) { try { @@ -57,9 +78,19 @@ public ResponseEntity createCategory( } } + @Operation(summary = "카테고리 목록 조회", description = "사용자의 모든 카테고리를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\"status\": 200, \"message\": \"카테고리 목록 조회 성공\", \"data\": [{\"id\": 1, \"name\": \"Spring Boot\", \"color\": \"#FF5733\"}, {\"id\": 2, \"name\": \"React\", \"color\": \"#61DAFB\"}]}"))), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @GetMapping public ResponseEntity getCategories( - @AuthenticationPrincipal CustomOAuth2User currentUser) { + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser) { try { log.info("카테고리 목록 조회: 사용자={}", currentUser.getName()); @@ -79,10 +110,24 @@ public ResponseEntity getCategories( } } + @Operation(summary = "카테고리 수정", description = "기존 카테고리의 정보를 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\"status\": 200, \"message\": \"카테고리가 성공적으로 수정되었습니다\", \"data\": {\"id\": 1, \"name\": \"Spring Framework\", \"color\": \"#6DB33F\"}}"))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 (중복된 카테고리명 등)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "카테고리를 찾을 수 없음", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @PutMapping("/{categoryId}") public ResponseEntity updateCategory( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @PathVariable Long categoryId, + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "카테고리 ID", example = "1") @PathVariable Long categoryId, @Valid @RequestBody UpdateCategoryRequestDTO requestDTO) { try { diff --git a/src/main/java/org/example/studylog/controller/MainController.java b/src/main/java/org/example/studylog/controller/MainController.java index c154a80..4883d86 100644 --- a/src/main/java/org/example/studylog/controller/MainController.java +++ b/src/main/java/org/example/studylog/controller/MainController.java @@ -1,5 +1,12 @@ package org.example.studylog.controller; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.studylog.dto.oauth.CustomOAuth2User; @@ -17,15 +24,87 @@ @RestController @RequiredArgsConstructor @Slf4j +@Tag(name = "Main", description = "메인 페이지 API") public class MainController { private final MainService mainService; private final UserRepository userRepository; + @Operation( + summary = "메인 페이지 조회", + description = "메인 페이지 데이터를 조회합니다. 인증된 사용자 또는 공유 코드로 조회 가능합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "메인 페이지 조회 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 200," + + "\"message\": \"메인 페이지 조회에 성공하였습니다.\"," + + "\"data\": {" + + "\"todayRecords\": 3," + + "\"currentStreak\": 5," + + "\"totalRecords\": 127," + + "\"categories\": [" + + "{\"id\": 1, \"name\": \"Spring Boot\", \"color\": \"#FF5733\"}," + + "{\"id\": 2, \"name\": \"React\", \"color\": \"#61DAFB\"}" + + "]," + + "\"recentRecords\": []" + + "}" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증 실패 (접근 권한 없음)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 401," + + "\"message\": \"접근 권한이 없습니다.\"," + + "\"data\": false" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "404", + description = "사용자 코드를 찾을 수 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 404," + + "\"message\": \"존재하지 않는 사용자 코드입니다.\"," + + "\"data\": null" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 500," + + "\"message\": \"내부 서버 오류입니다. 다시 접속해주세요.\"," + + "\"data\": null" + + "}" + ) + ) + ) + }) @GetMapping("/main") public ResponseEntity getMainPage( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @RequestParam(required = false) String code) { + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "사용자 공유 코드 (선택적)", example = "ABC123") @RequestParam(required = false) String code) { // code 파라미터가 있으면 코드로 조회, 없으면 기존 로직 if (code != null && !code.trim().isEmpty()) { diff --git a/src/main/java/org/example/studylog/controller/StreakController.java b/src/main/java/org/example/studylog/controller/StreakController.java index 5340512..cf84cc4 100644 --- a/src/main/java/org/example/studylog/controller/StreakController.java +++ b/src/main/java/org/example/studylog/controller/StreakController.java @@ -1,5 +1,13 @@ package org.example.studylog.controller; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.studylog.dto.oauth.CustomOAuth2User; @@ -19,17 +27,88 @@ @RestController @RequiredArgsConstructor @Slf4j +@Tag(name = "Streak", description = "스트릭(연속 학습) 관련 API") public class StreakController { private final StreakService streakService; private final UserRepository userRepository; + @Operation( + summary = "월별 스트릭 조회", + description = "특정 연도와 월의 일별 학습 기록 개수를 조회합니다. 인증된 사용자 또는 공유 코드로 조회 가능합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "스트릭 조회 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 200," + + "\"message\": \"스트릭 조회에 성공하였습니다.\"," + + "\"data\": {" + + "\"2025-07-01\": 0," + + "\"2025-07-02\": 3," + + "\"2025-07-03\": 1," + + "\"2025-07-04\": 0," + + "\"2025-07-05\": 2" + + "}" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 (유효하지 않은 년도/월)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 400," + + "\"message\": \"잘못된 접근입니다\"," + + "\"data\": {" + + "\"example\": \"올바르지 않은 월입니다\"" + + "}" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증 실패 (접근 권한 없음)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 401," + + "\"message\": \"접근 권한이 없습니다.\"," + + "\"data\": false" + + "}" + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{" + + "\"status\": 500," + + "\"message\": \"내부 서버 오류입니다. 다시 접속해주세요.\"," + + "\"data\": null" + + "}" + ) + ) + ) + }) @GetMapping("/streak") public ResponseEntity getMonthlyStreak( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @RequestParam(required = false) String code, - @RequestParam("year") String year, - @RequestParam("month") String month) { + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "사용자 공유 코드 (선택적)", example = "ABC123") @RequestParam(required = false) String code, + @Parameter(description = "조회할 년도", required = true, example = "2025") @RequestParam("year") String year, + @Parameter(description = "조회할 월 (1-12)", required = true, example = "7") @RequestParam("month") String month) { // code 파라미터가 있으면 코드로 조회 if (code != null && !code.trim().isEmpty()) { diff --git a/src/main/java/org/example/studylog/controller/StudyRecordController.java b/src/main/java/org/example/studylog/controller/StudyRecordController.java index 12e1a80..02fda53 100644 --- a/src/main/java/org/example/studylog/controller/StudyRecordController.java +++ b/src/main/java/org/example/studylog/controller/StudyRecordController.java @@ -1,6 +1,13 @@ package org.example.studylog.controller; 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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.studylog.dto.oauth.CustomOAuth2User; @@ -24,14 +31,27 @@ @RequiredArgsConstructor @Validated @Slf4j +@Tag(name = "Study Records", description = "학습 기록 관련 API") public class StudyRecordController { private final StudyRecordService studyRecordService; private final UserRepository userRepository; + @Operation(summary = "학습 기록 생성", description = "새로운 학습 기록을 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "학습 기록 생성 성공", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\"status\": 201, \"message\": \"기록 생성 성공\", \"data\": {\"recordId\": 1, \"title\": \"Spring Boot 학습\"}}"))), + @ApiResponse(responseCode = "400", description = "잘못된 요청", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @PostMapping public ResponseEntity createStudyRecord( - @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, @Valid @RequestBody CreateStudyRecordRequestDTO requestDTO) { try { @@ -59,10 +79,21 @@ public ResponseEntity createStudyRecord( } } + @Operation(summary = "학습 기록 상세 조회", description = "특정 학습 기록의 상세 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "기록을 찾을 수 없음", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @GetMapping("/{recordId}") public ResponseEntity getStudyRecord( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @PathVariable Long recordId) { + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "학습 기록 ID", example = "1") @PathVariable Long recordId) { try { log.info("기록 상세 조회 요청: 사용자={}, recordId={}", currentUser.getName(), recordId); @@ -88,10 +119,23 @@ public ResponseEntity getStudyRecord( } } + @Operation(summary = "학습 기록 수정", description = "기존 학습 기록을 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", description = "잘못된 요청", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "기록을 찾을 수 없음", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @PutMapping("/{recordId}") public ResponseEntity updateStudyRecord( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @PathVariable Long recordId, + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "학습 기록 ID", example = "1") @PathVariable Long recordId, @Valid @RequestBody UpdateStudyRecordRequestDTO requestDTO) { try { @@ -119,10 +163,21 @@ public ResponseEntity updateStudyRecord( } } + @Operation(summary = "학습 기록 삭제", description = "특정 학습 기록을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "삭제 성공", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "인증 실패", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "기록을 찾을 수 없음", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json")) + }) @DeleteMapping("/{recordId}") public ResponseEntity deleteStudyRecord( - @AuthenticationPrincipal CustomOAuth2User currentUser, - @PathVariable Long recordId) { + @Parameter(hidden = true) @AuthenticationPrincipal CustomOAuth2User currentUser, + @Parameter(description = "학습 기록 ID", example = "1") @PathVariable Long recordId) { try { log.info("기록 삭제 요청: 사용자={}, recordId={}", currentUser.getName(), recordId); @@ -224,6 +279,8 @@ public ResponseEntity getStudyRecordsWithFilter( } + @Operation(summary = "테스트 엔드포인트", description = "API 연결 테스트용 엔드포인트") + @ApiResponse(responseCode = "200", description = "테스트 성공") @GetMapping("/test") public ResponseEntity testEndpoint() { log.info("=== TEST 엔드포인트 호출됨 ===");