From 650b80210a78ca0e4cee33c4ce91e37f82782152 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Tue, 14 Jan 2025 21:43:28 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[FEAT]=20=EC=B5=9C=EA=B7=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=90=9C=20=EB=8F=99=EC=82=AC-=EB=AA=85=EC=82=AC=20?= =?UTF-8?q?=EC=8C=8D=20=EC=B5=9C=EC=8B=A0=EC=88=9C=205=EA=B0=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/news/application/NewsService.java | 17 +++++++++++++- .../news/dto/response/RecentKeywordsRes.java | 23 +++++++++++++++++++ .../news/presentation/NewsController.java | 15 ++++++++++++ .../record/repository/KeywordRepository.java | 2 ++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/movelog/domain/news/dto/response/RecentKeywordsRes.java diff --git a/src/main/java/com/movelog/domain/news/application/NewsService.java b/src/main/java/com/movelog/domain/news/application/NewsService.java index 3233444..aef9679 100644 --- a/src/main/java/com/movelog/domain/news/application/NewsService.java +++ b/src/main/java/com/movelog/domain/news/application/NewsService.java @@ -5,6 +5,7 @@ import com.movelog.domain.news.dto.request.CreateNewsReq; import com.movelog.domain.news.dto.request.NewsHeadLineReq; import com.movelog.domain.news.dto.response.HeadLineRes; +import com.movelog.domain.news.dto.response.RecentKeywordsRes; import com.movelog.domain.record.domain.Keyword; import com.movelog.domain.record.domain.VerbType; import com.movelog.domain.record.exception.KeywordNotFoundException; @@ -29,6 +30,7 @@ public class NewsService { private final HeadLineGeneratorService headLineGeneratorService; private final UserService userService; + private final UserRepository userRepository; private final KeywordRepository keywordRepository; private final NewsRepository newsRepository; private final S3Util s3Util; @@ -64,6 +66,20 @@ public void createNews(UserPrincipal userPrincipal, Long keywordId, CreateNewsRe } + public List getRecentKeywords(UserPrincipal userPrincipal) { + User user = validateUser(userPrincipal); + // id가 5인 유저 정보(테스트용) + // User user = userRepository.findById(5L).orElseThrow(UserNotFoundException::new); + List recentKeywords = keywordRepository.findTop5ByUserOrderByCreatedAtDesc(user); + + return recentKeywords.stream() + .map(keyword -> RecentKeywordsRes.builder() + .keywordId(keyword.getKeywordId()) + .verb(VerbType.getStringVerbType(keyword.getVerbType())) + .noun(keyword.getKeyword()) + .build()) + .toList(); + } // User 정보 검증 @@ -80,5 +96,4 @@ private Keyword validateKeyword(Long keywordId) { return keywordOptional.get(); } - } diff --git a/src/main/java/com/movelog/domain/news/dto/response/RecentKeywordsRes.java b/src/main/java/com/movelog/domain/news/dto/response/RecentKeywordsRes.java new file mode 100644 index 0000000..6a2f068 --- /dev/null +++ b/src/main/java/com/movelog/domain/news/dto/response/RecentKeywordsRes.java @@ -0,0 +1,23 @@ +package com.movelog.domain.news.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class RecentKeywordsRes { + @Schema( type = "int", example ="1", description="키워드 ID") + private Long keywordId; + + @Schema( type = "String", example ="클라이밍", description="명사") + private String noun; + + @Schema( type = "String", example ="시작하다", description="동사") + private String verb; + +} diff --git a/src/main/java/com/movelog/domain/news/presentation/NewsController.java b/src/main/java/com/movelog/domain/news/presentation/NewsController.java index 3b0e599..8fa663e 100644 --- a/src/main/java/com/movelog/domain/news/presentation/NewsController.java +++ b/src/main/java/com/movelog/domain/news/presentation/NewsController.java @@ -4,6 +4,7 @@ import com.movelog.domain.news.dto.request.CreateNewsReq; import com.movelog.domain.news.dto.request.NewsHeadLineReq; import com.movelog.domain.news.dto.response.HeadLineRes; +import com.movelog.domain.news.dto.response.RecentKeywordsRes; import com.movelog.global.config.security.token.CurrentUser; import com.movelog.global.config.security.token.UserPrincipal; import com.movelog.global.payload.Message; @@ -72,6 +73,20 @@ public ResponseEntity createNews( } + @Operation(summary = "뉴스 추천 기록 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "뉴스 추천 기록 조회 성공", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))), + @ApiResponse(responseCode = "400", description = "뉴스 추천 기록 조회 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/recommend") + public ResponseEntity getRecentKeywords( + @Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + List response = newsService.getRecentKeywords(userPrincipal); + return ResponseEntity.ok(ApiResponseUtil.success(response)); + } diff --git a/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java b/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java index d4f5e6c..dc22690 100644 --- a/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java +++ b/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java @@ -11,4 +11,6 @@ @Repository public interface KeywordRepository extends JpaRepository { List findByUser(User user); + + List findTop5ByUserOrderByCreatedAtDesc(User user); } From 4ec804f71ab93e6af69f211bced40d622fa788a2 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Tue, 14 Jan 2025 21:55:17 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[DOCS]=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=82=B4=20=EC=96=B4=ED=9C=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기록 -> 키워드 --- .../com/movelog/domain/news/presentation/NewsController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/movelog/domain/news/presentation/NewsController.java b/src/main/java/com/movelog/domain/news/presentation/NewsController.java index 8fa663e..ee30aa6 100644 --- a/src/main/java/com/movelog/domain/news/presentation/NewsController.java +++ b/src/main/java/com/movelog/domain/news/presentation/NewsController.java @@ -73,7 +73,7 @@ public ResponseEntity createNews( } - @Operation(summary = "뉴스 추천 기록 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ") + @Operation(summary = "뉴스 추천 키워드 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "뉴스 추천 기록 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))), @@ -91,4 +91,6 @@ public ResponseEntity getRecentKeywords( + + } From d8ecb0b3ffda48517c59507a516fd97c9343c54f Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Tue, 14 Jan 2025 22:17:36 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[DOCS]=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../movelog/domain/news/presentation/NewsController.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/movelog/domain/news/presentation/NewsController.java b/src/main/java/com/movelog/domain/news/presentation/NewsController.java index ee30aa6..097ff5a 100644 --- a/src/main/java/com/movelog/domain/news/presentation/NewsController.java +++ b/src/main/java/com/movelog/domain/news/presentation/NewsController.java @@ -39,7 +39,8 @@ public class NewsController { @Operation(summary = "뉴스 헤드라인 생성 API", description = "뉴스 헤드라인을 생성하는 API입니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "뉴스 헤드라인 생성 성공", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))), + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = HeadLineRes.class))), @ApiResponse(responseCode = "400", description = "뉴스 헤드라인 생성 실패", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) @@ -76,7 +77,8 @@ public ResponseEntity createNews( @Operation(summary = "뉴스 추천 키워드 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "뉴스 추천 기록 조회 성공", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))), + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = RecentKeywordsRes.class))), @ApiResponse(responseCode = "400", description = "뉴스 추천 기록 조회 실패", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) @@ -93,4 +95,5 @@ public ResponseEntity getRecentKeywords( + } From fdc1d5991b5e34a136b4b98cb7a2c19e2a135699 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 00:45:06 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[REFACTOR]=20=EA=B8=B0=EC=A1=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20DTO=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20API=20=EB=AA=85=EC=84=B8,=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=ED=86=B5=EC=9D=BC,=20validation=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/TodayRecordStatus.java | 24 ++++++++ .../record/presentation/RecordController.java | 57 +++++++++++++------ .../domain/record/service/RecordService.java | 31 +++++++--- 3 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/movelog/domain/record/dto/response/TodayRecordStatus.java diff --git a/src/main/java/com/movelog/domain/record/dto/response/TodayRecordStatus.java b/src/main/java/com/movelog/domain/record/dto/response/TodayRecordStatus.java new file mode 100644 index 0000000..562e149 --- /dev/null +++ b/src/main/java/com/movelog/domain/record/dto/response/TodayRecordStatus.java @@ -0,0 +1,24 @@ +package com.movelog.domain.record.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class TodayRecordStatus { + + @Schema( type = "boolean", example ="true", description="했어요에 대한 기록이 있는지에 대한 여부") + private boolean isDo; + + @Schema( type = "boolean", example ="false", description="먹었어요에 대한 기록이 있는지에 대한 여부") + private boolean isEat; + + @Schema( type = "boolean", example ="true", description="갔어요에 대한 기록이 있는지에 대한 여부") + private boolean isGo; + +} diff --git a/src/main/java/com/movelog/domain/record/presentation/RecordController.java b/src/main/java/com/movelog/domain/record/presentation/RecordController.java index b73c872..fe99618 100644 --- a/src/main/java/com/movelog/domain/record/presentation/RecordController.java +++ b/src/main/java/com/movelog/domain/record/presentation/RecordController.java @@ -1,46 +1,69 @@ package com.movelog.domain.record.presentation; import com.movelog.domain.record.dto.request.CreateRecordReq; +import com.movelog.domain.record.dto.response.RecentRecordImagesRes; +import com.movelog.domain.record.dto.response.TodayRecordStatus; import com.movelog.domain.record.service.RecordService; import com.movelog.global.config.security.token.UserPrincipal; -import com.movelog.global.payload.ApiResponse; +import com.movelog.global.payload.Message; +import com.movelog.global.util.ApiResponseUtil; 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.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.Set; + @RestController @RequestMapping("api/v1/record") @RequiredArgsConstructor public class RecordController { private final RecordService recordService; @Operation(summary = "기록 추가 API", description = "기록을 추가하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "기록 추가 성공", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))), + @ApiResponse(responseCode = "400", description = "기록 추가 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "기록 추가 실패(서버 에러), Request Body 내용을 확인해주세요.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) @PostMapping - public ResponseEntity createRecord( + public ResponseEntity createRecord( @Parameter(description = "User의 토큰을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal, @Parameter(description = "Schemas의 CreateRecordReq를 참고해주세요.", required = true) @RequestPart CreateRecordReq createRecordReq, - @RequestPart(value = "img", required = false) MultipartFile img) { - recordService.createRecord(userPrincipal.getId(), createRecordReq, img); - - ApiResponse result = ApiResponse.builder() - .check(true) - .information("기록을 추가했어요") - .build(); - return ResponseEntity.ok(result); + @RequestPart(value = "img", required = false) MultipartFile img + ) { + recordService.createRecord(5L, createRecordReq, img); + return ResponseEntity.ok(ApiResponseUtil.success(Message.builder().message("기록이 생성되었습니다.").build())); } + @Operation(summary = "오늘 기준 기록 현황 API", description = "오늘 기준 기록 확인하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "오늘 기준 기록 현황 조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = TodayRecordStatus.class))), + @ApiResponse(responseCode = "400", description = "오늘 기준 기록 현황 조회 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) @GetMapping("/today") - public ResponseEntity retrieveTodayRecord( + public ResponseEntity retrieveTodayRecord( @Parameter(description = "User의 토큰을 입력해주세요.", required = false) @AuthenticationPrincipal UserPrincipal userPrincipal ) {; - - ApiResponse result = ApiResponse.builder() - .check(true) - .information(recordService.retrieveTodayRecord(5L)) - .build(); - return ResponseEntity.ok(result); + TodayRecordStatus result = recordService.retrieveTodayRecord(5L); + return ResponseEntity.ok(ApiResponseUtil.success(result)); } + + } diff --git a/src/main/java/com/movelog/domain/record/service/RecordService.java b/src/main/java/com/movelog/domain/record/service/RecordService.java index 9b4dc1e..d248259 100644 --- a/src/main/java/com/movelog/domain/record/service/RecordService.java +++ b/src/main/java/com/movelog/domain/record/service/RecordService.java @@ -4,11 +4,13 @@ import com.movelog.domain.record.domain.Record; import com.movelog.domain.record.domain.VerbType; import com.movelog.domain.record.dto.request.CreateRecordReq; +import com.movelog.domain.record.dto.response.TodayRecordStatus; import com.movelog.domain.record.repository.KeywordRepository; import com.movelog.domain.record.repository.RecordRepository; import com.movelog.domain.user.domain.User; import com.movelog.domain.user.domain.repository.UserRepository; import com.movelog.global.util.S3Util; +import jakarta.validation.ConstraintViolation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,6 +37,8 @@ public class RecordService { public void createRecord(Long userId, CreateRecordReq createRecordReq, MultipartFile img) { User user = validUserById(userId); // User user = validUserById(5L); + validateCreateRecordReq(createRecordReq); + String recordImgUrl = s3Util.uploadToRecordFolder(img); log.info("recordImgUrl: {}", recordImgUrl); @@ -68,7 +72,7 @@ private User validUserById(Long userId) { return userOptional.get(); } - public Map retrieveTodayRecord(Long userId) { + public TodayRecordStatus retrieveTodayRecord(Long userId) { // 유저 유효성 검사 및 조회 User user = validUserById(userId); @@ -94,13 +98,26 @@ public Map retrieveTodayRecord(Long userId) { log.info("Today VerbTypes: {}", todayVerbTypes); // 모든 VerbType에 대해 존재 여부를 반환 - return Arrays.stream(VerbType.values()) - .collect(Collectors.toMap( - VerbType::getVerbType, // 키: VerbType의 문자열 값 - todayVerbTypes::contains // 값: 오늘 VerbType에 포함 여부 - )); - } + TodayRecordStatus todayRecordStatus = TodayRecordStatus.builder() + .isDo(verbTypeExists(todayVerbTypes, VerbType.DO)) + .isEat(verbTypeExists(todayVerbTypes, VerbType.EAT)) + .isGo(verbTypeExists(todayVerbTypes, VerbType.GO)) + .build(); + return todayRecordStatus; + + } + private boolean verbTypeExists(Set todayVerbTypes, VerbType verbType) { + return todayVerbTypes.contains(verbType); + } + private void validateCreateRecordReq(CreateRecordReq createRecordReq) { + if (createRecordReq.getVerbType() == null || createRecordReq.getVerbType().isEmpty()) { + throw new IllegalArgumentException("verbType is required."); + } + if (createRecordReq.getNoun() == null || createRecordReq.getNoun().isEmpty()) { + throw new IllegalArgumentException("noun is required."); + } + } } From 0291e6cbe788f6e2bc0e581339d4ebe9ac224fd4 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 00:47:17 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[REFACTOR]=20=EA=B8=B0=EC=A1=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20DTO=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20API=20=EB=AA=85=EC=84=B8,=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=ED=86=B5=EC=9D=BC,=20validation=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../movelog/domain/record/presentation/RecordController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/movelog/domain/record/presentation/RecordController.java b/src/main/java/com/movelog/domain/record/presentation/RecordController.java index fe99618..af98fb4 100644 --- a/src/main/java/com/movelog/domain/record/presentation/RecordController.java +++ b/src/main/java/com/movelog/domain/record/presentation/RecordController.java @@ -45,7 +45,7 @@ public ResponseEntity createRecord( @Parameter(description = "Schemas의 CreateRecordReq를 참고해주세요.", required = true) @RequestPart CreateRecordReq createRecordReq, @RequestPart(value = "img", required = false) MultipartFile img ) { - recordService.createRecord(5L, createRecordReq, img); + recordService.createRecord(userPrincipal.getId(), createRecordReq, img); return ResponseEntity.ok(ApiResponseUtil.success(Message.builder().message("기록이 생성되었습니다.").build())); } From bb920864c0b6fed47f8355a6572cdd783b78ae99 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 01:14:14 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[REFACTOR]=20Cascade=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/movelog/domain/record/domain/Keyword.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/movelog/domain/record/domain/Keyword.java b/src/main/java/com/movelog/domain/record/domain/Keyword.java index 31ee192..66774c3 100644 --- a/src/main/java/com/movelog/domain/record/domain/Keyword.java +++ b/src/main/java/com/movelog/domain/record/domain/Keyword.java @@ -32,10 +32,10 @@ public class Keyword extends BaseEntity { @JoinColumn(name = "user_id") private User user; - @OneToMany(mappedBy = "keyword") + @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) private List records = new ArrayList<>(); - @OneToMany(mappedBy = "keyword") + @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) private List news = new ArrayList<>(); @Builder From 1ec2d792c660fd223460a8f5e30b14cdccb45e26 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 01:17:57 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[FEAT]=20=EC=B5=9C=EA=B7=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/RecentRecordImagesRes.java | 23 +++++++++++++++++++ .../record/presentation/RecordController.java | 22 +++++++++++++++--- .../record/repository/KeywordRepository.java | 5 ++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/movelog/domain/record/dto/response/RecentRecordImagesRes.java diff --git a/src/main/java/com/movelog/domain/record/dto/response/RecentRecordImagesRes.java b/src/main/java/com/movelog/domain/record/dto/response/RecentRecordImagesRes.java new file mode 100644 index 0000000..0b05009 --- /dev/null +++ b/src/main/java/com/movelog/domain/record/dto/response/RecentRecordImagesRes.java @@ -0,0 +1,23 @@ +package com.movelog.domain.record.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class RecentRecordImagesRes { + + @Schema( type = "String", example ="https://movelog.s3.ap-northeast-2.amazonaws.com/record/2021-08-01/1.jpg", description="최근 기록 이미지 URL") + private String imageUrl; + + @Schema( type = "LocalDateTime", example ="2021-08-01T00:00:00", description="최근 기록 이미지 생성 시간") + private LocalDateTime createdAt; + +} diff --git a/src/main/java/com/movelog/domain/record/presentation/RecordController.java b/src/main/java/com/movelog/domain/record/presentation/RecordController.java index af98fb4..74f078b 100644 --- a/src/main/java/com/movelog/domain/record/presentation/RecordController.java +++ b/src/main/java/com/movelog/domain/record/presentation/RecordController.java @@ -13,16 +13,14 @@ 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 jakarta.validation.ConstraintViolation; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.ErrorResponse; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.List; import java.util.Set; @RestController @@ -65,5 +63,23 @@ public ResponseEntity retrieveTodayRecord( return ResponseEntity.ok(ApiResponseUtil.success(result)); } + @Operation(summary = "최근 기록 이미지 조회 API", description = "사용자가 선택한 명사-동사 쌍에 해당하는 최근 기록 이미지(5개)를 조회하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "최근 기록 이미지 조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = RecentRecordImagesRes.class))), + @ApiResponse(responseCode = "400", description = "최근 기록 이미지 조회 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/image/{keywordId}") + public ResponseEntity retrieveRecentRecordImages( + @Parameter(description = "User의 토큰을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal, + @Parameter(description = "키워드 ID(동사-명사 쌍에 대한 ID)를 입력해주세요.", required = true) @PathVariable Long keywordId + ) { + List result = recordService.retrieveRecentRecordImages(userPrincipal, keywordId); + return ResponseEntity.ok(ApiResponseUtil.success(result)); + } + + } diff --git a/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java b/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java index dc22690..ec569ce 100644 --- a/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java +++ b/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java @@ -2,6 +2,7 @@ import com.movelog.domain.record.domain.Keyword; import com.movelog.domain.record.domain.Record; +import com.movelog.domain.record.domain.VerbType; import com.movelog.domain.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -13,4 +14,8 @@ public interface KeywordRepository extends JpaRepository { List findByUser(User user); List findTop5ByUserOrderByCreatedAtDesc(User user); + + boolean existsByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType); + + Keyword findByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType); } From 8c4fc7d4377d8af6408190903b488641f1b03dfb Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 01:18:25 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[FEAT]=20=EC=B5=9C=EA=B7=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=83=9D=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../record/repository/RecordRepository.java | 3 ++ .../domain/record/service/RecordService.java | 54 +++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/movelog/domain/record/repository/RecordRepository.java b/src/main/java/com/movelog/domain/record/repository/RecordRepository.java index 3482844..a30d621 100644 --- a/src/main/java/com/movelog/domain/record/repository/RecordRepository.java +++ b/src/main/java/com/movelog/domain/record/repository/RecordRepository.java @@ -11,5 +11,8 @@ @Repository public interface RecordRepository extends JpaRepository { + List findByKeywordInAndActionTimeBetween(List keywords, LocalDateTime startTime, LocalDateTime endTime); + + List findTop5ByKeywordOrderByActionTimeDesc(Keyword keyword); } diff --git a/src/main/java/com/movelog/domain/record/service/RecordService.java b/src/main/java/com/movelog/domain/record/service/RecordService.java index d248259..576c48a 100644 --- a/src/main/java/com/movelog/domain/record/service/RecordService.java +++ b/src/main/java/com/movelog/domain/record/service/RecordService.java @@ -4,11 +4,13 @@ import com.movelog.domain.record.domain.Record; import com.movelog.domain.record.domain.VerbType; import com.movelog.domain.record.dto.request.CreateRecordReq; +import com.movelog.domain.record.dto.response.RecentRecordImagesRes; import com.movelog.domain.record.dto.response.TodayRecordStatus; import com.movelog.domain.record.repository.KeywordRepository; import com.movelog.domain.record.repository.RecordRepository; import com.movelog.domain.user.domain.User; import com.movelog.domain.user.domain.repository.UserRepository; +import com.movelog.global.config.security.token.UserPrincipal; import com.movelog.global.util.S3Util; import jakarta.validation.ConstraintViolation; import lombok.RequiredArgsConstructor; @@ -45,14 +47,23 @@ public void createRecord(Long userId, CreateRecordReq createRecordReq, Multipart String verb = createRecordReq.getVerbType(); try { VerbType verbType = VerbType.fromValue(verb); + String noun = createRecordReq.getNoun(); - Keyword keyword = Keyword.builder() - .user(user) - .keyword(createRecordReq.getNoun()) - .verbType(verbType) - .build(); + Keyword keyword; + + // 사용자의 키워드에 존재하지 않는 경우 (새로 등록) + if (!keywordRepository.existsByUserAndKeywordAndVerbType(user, noun, verbType)) { + keyword = Keyword.builder() + .user(user) + .keyword(noun) + .verbType(verbType) + .build(); - keywordRepository.save(keyword); + keywordRepository.save(keyword); + } + else{ + keyword = keywordRepository.findByUserAndKeywordAndVerbType(user, noun, verbType); + } Record record = Record.builder() .keyword(keyword) @@ -61,16 +72,13 @@ public void createRecord(Long userId, CreateRecordReq createRecordReq, Multipart .build(); recordRepository.save(record); + } catch (IllegalArgumentException e) { throw new RuntimeException("Invalid verb type: " + verb, e); } } - private User validUserById(Long userId) { - Optional userOptional = userRepository.findById(userId); - return userOptional.get(); - } public TodayRecordStatus retrieveTodayRecord(Long userId) { // 유저 유효성 검사 및 조회 @@ -108,6 +116,31 @@ public TodayRecordStatus retrieveTodayRecord(Long userId) { } + public List retrieveRecentRecordImages(UserPrincipal userPrincipal, Long keywordId) { + User user = validUserById(userPrincipal.getId()); + // User user = validUserById(5L); + Keyword keyword = validKeywordById(keywordId); + List records = recordRepository.findTop5ByKeywordOrderByActionTimeDesc(keyword); + + return records.stream() + .map(record -> RecentRecordImagesRes.builder() + .imageUrl(record.getRecordImage()) + .createdAt(record.getCreatedAt()) + .build()) + .collect(Collectors.toList()); + + } + + private User validUserById(Long userId) { + Optional userOptional = userRepository.findById(userId); + return userOptional.get(); + } + + private Keyword validKeywordById(Long keywordId) { + Optional keywordOptional = keywordRepository.findById(keywordId); + return keywordOptional.get(); + } + private boolean verbTypeExists(Set todayVerbTypes, VerbType verbType) { return todayVerbTypes.contains(verbType); } @@ -120,4 +153,5 @@ private void validateCreateRecordReq(CreateRecordReq createRecordReq) { throw new IllegalArgumentException("noun is required."); } } + }