From c8a445e91b4e0d6bdd52dca4f26e29d83bb818f5 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 19:33:12 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[FEAT]=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=A1=9D=20=EB=82=B4=20=EB=AA=85=EC=82=AC?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=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 --- .../record/dto/request/SearchKeywordReq.java | 16 +++++++++++++ .../record/dto/response/SearchKeywordRes.java | 24 +++++++++++++++++++ .../record/presentation/RecordController.java | 20 ++++++++++++++++ .../record/repository/KeywordRepository.java | 2 ++ .../domain/record/service/RecordService.java | 23 +++++++++++++++++- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/movelog/domain/record/dto/request/SearchKeywordReq.java create mode 100644 src/main/java/com/movelog/domain/record/dto/response/SearchKeywordRes.java diff --git a/src/main/java/com/movelog/domain/record/dto/request/SearchKeywordReq.java b/src/main/java/com/movelog/domain/record/dto/request/SearchKeywordReq.java new file mode 100644 index 0000000..8446bc8 --- /dev/null +++ b/src/main/java/com/movelog/domain/record/dto/request/SearchKeywordReq.java @@ -0,0 +1,16 @@ +package com.movelog.domain.record.dto.request; + +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 SearchKeywordReq { + @Schema( type = "String", example ="헬스", description="검색할 명사") + private String searchKeyword; +} diff --git a/src/main/java/com/movelog/domain/record/dto/response/SearchKeywordRes.java b/src/main/java/com/movelog/domain/record/dto/response/SearchKeywordRes.java new file mode 100644 index 0000000..a6f7c05 --- /dev/null +++ b/src/main/java/com/movelog/domain/record/dto/response/SearchKeywordRes.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 SearchKeywordRes { + + @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/record/presentation/RecordController.java b/src/main/java/com/movelog/domain/record/presentation/RecordController.java index 74f078b..9a33fe7 100644 --- a/src/main/java/com/movelog/domain/record/presentation/RecordController.java +++ b/src/main/java/com/movelog/domain/record/presentation/RecordController.java @@ -1,7 +1,9 @@ package com.movelog.domain.record.presentation; import com.movelog.domain.record.dto.request.CreateRecordReq; +import com.movelog.domain.record.dto.request.SearchKeywordReq; import com.movelog.domain.record.dto.response.RecentRecordImagesRes; +import com.movelog.domain.record.dto.response.SearchKeywordRes; import com.movelog.domain.record.dto.response.TodayRecordStatus; import com.movelog.domain.record.service.RecordService; import com.movelog.global.config.security.token.UserPrincipal; @@ -81,5 +83,23 @@ public ResponseEntity retrieveRecentRecordImages( } + @Operation(summary = "기록 내 명사 검색 API", description = "사용자가 생성한 기록 중 명사를 통해 동사-명사 쌍을 검색하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "기록 내 명사 검색 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = SearchKeywordRes.class))), + @ApiResponse(responseCode = "400", description = "기록 내 명사 검색 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/search") + public ResponseEntity searchKeyword( + @Parameter(description = "User의 토큰을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal, + @Parameter(description = "검색할 명사를 입력해주세요.", required = true) @RequestBody SearchKeywordReq searchKeywordReq + ) { + List result = recordService.searchKeyword(userPrincipal, searchKeywordReq); + 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 ec569ce..4cb26b3 100644 --- a/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java +++ b/src/main/java/com/movelog/domain/record/repository/KeywordRepository.java @@ -18,4 +18,6 @@ public interface KeywordRepository extends JpaRepository { boolean existsByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType); Keyword findByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType); + + List findAllByUserAndKeywordContaining(User user, String 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 576c48a..fe3837b 100644 --- a/src/main/java/com/movelog/domain/record/service/RecordService.java +++ b/src/main/java/com/movelog/domain/record/service/RecordService.java @@ -4,7 +4,9 @@ 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.request.SearchKeywordReq; import com.movelog.domain.record.dto.response.RecentRecordImagesRes; +import com.movelog.domain.record.dto.response.SearchKeywordRes; import com.movelog.domain.record.dto.response.TodayRecordStatus; import com.movelog.domain.record.repository.KeywordRepository; import com.movelog.domain.record.repository.RecordRepository; @@ -131,6 +133,26 @@ public List retrieveRecentRecordImages(UserPrincipal user } + + public List searchKeyword(UserPrincipal userPrincipal, SearchKeywordReq searchKeywordReq) { + User user = validUserById(userPrincipal.getId()); + // User user = validUserById(5L); + String keyword = searchKeywordReq.getSearchKeyword(); + List keywords = keywordRepository.findAllByUserAndKeywordContaining(user, keyword); + + log.info("Search Keyword: {}", searchKeywordReq.getSearchKeyword()); + keywords.forEach(k -> log.info("Keyword in DB: {}", k.getKeyword())); + + + return keywords.stream() + .map(k -> SearchKeywordRes.builder() + .keywordId(k.getKeywordId()) + .noun(k.getKeyword()) + .verb(VerbType.getStringVerbType(k.getVerbType())) + .build()) + .collect(Collectors.toList()); + } + private User validUserById(Long userId) { Optional userOptional = userRepository.findById(userId); return userOptional.get(); @@ -153,5 +175,4 @@ private void validateCreateRecordReq(CreateRecordReq createRecordReq) { throw new IllegalArgumentException("noun is required."); } } - } From d9c0b55d0df3b22b13ff2fb4ec1742a3e95abdd1 Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 20:37:12 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[FEAT]=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=A1=9D=20=EB=82=B4=20=EB=AA=85=EC=82=AC?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EB=82=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/record/service/RecordService.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 fe3837b..6302203 100644 --- a/src/main/java/com/movelog/domain/record/service/RecordService.java +++ b/src/main/java/com/movelog/domain/record/service/RecordService.java @@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.text.Collator; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -139,18 +140,25 @@ public List searchKeyword(UserPrincipal userPrincipal, SearchK // User user = validUserById(5L); String keyword = searchKeywordReq.getSearchKeyword(); List keywords = keywordRepository.findAllByUserAndKeywordContaining(user, keyword); - - log.info("Search Keyword: {}", searchKeywordReq.getSearchKeyword()); keywords.forEach(k -> log.info("Keyword in DB: {}", k.getKeyword())); + // Collator 생성 + Collator collator = Collator.getInstance(Locale.KOREA); - return keywords.stream() + // Collator를 이용한 오름차순 정렬 + List sortedResults = keywords.stream() .map(k -> SearchKeywordRes.builder() .keywordId(k.getKeywordId()) .noun(k.getKeyword()) .verb(VerbType.getStringVerbType(k.getVerbType())) .build()) + .sorted((o1, o2) -> collator.compare(o1.getNoun(), o2.getNoun())) // Collator로 비교 .collect(Collectors.toList()); + + // 정렬된 명사 목록 출력 + sortedResults.forEach(r -> log.info("Sorted Noun: {}", r.getNoun())); + + return sortedResults; } private User validUserById(Long userId) { From 80d80f494d617b6eb49cd4b4a2ecfe984fbe25ae Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 22:42:08 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[DOCS]=20=EB=89=B4=EC=8A=A4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=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 | 5 ++++- 1 file changed, 4 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 097ff5a..40fdeaa 100644 --- a/src/main/java/com/movelog/domain/news/presentation/NewsController.java +++ b/src/main/java/com/movelog/domain/news/presentation/NewsController.java @@ -55,7 +55,7 @@ public ResponseEntity createHeadLine( } - @Operation(summary = "뉴스 생성 및 저장 API(기존 이미지 기록 기반)", description = "사용자의 기존 기록 이미지로 뉴스를 생성합니다.") + @Operation(summary = "뉴스 생성 및 저장 API", description = "새로운 뉴스를 생성하고 저장합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "뉴스 생성 및 저장 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))), @@ -96,4 +96,7 @@ public ResponseEntity getRecentKeywords( + + + } From ac4ea0ff1ddc7cbe21edc6ee05ac968a4600061e Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 23:27:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[FEAT]=20=EC=B5=9C=EA=B7=BC=20=EC=9D=BC?= =?UTF-8?q?=EC=A3=BC=EC=9D=BC=20=EA=B0=84=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=83=9D=EC=84=B1=ED=95=9C=20=EB=89=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=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 | 27 ++++++++++++++ .../domain/repository/NewsRepository.java | 18 ++++++++++ .../news/dto/response/RecentNewsRes.java | 36 +++++++++++++++++++ .../news/presentation/NewsController.java | 20 +++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/main/java/com/movelog/domain/news/dto/response/RecentNewsRes.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 aef9679..262ca58 100644 --- a/src/main/java/com/movelog/domain/news/application/NewsService.java +++ b/src/main/java/com/movelog/domain/news/application/NewsService.java @@ -6,6 +6,7 @@ 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.news.dto.response.RecentNewsRes; import com.movelog.domain.record.domain.Keyword; import com.movelog.domain.record.domain.VerbType; import com.movelog.domain.record.exception.KeywordNotFoundException; @@ -17,10 +18,13 @@ import com.movelog.global.config.security.token.UserPrincipal; import com.movelog.global.util.S3Util; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -81,6 +85,29 @@ public List getRecentKeywords(UserPrincipal userPrincipal) { .toList(); } + public List getRecentNews(UserPrincipal userPrincipal, Integer page) { + User user = validateUser(userPrincipal); + // User user = userRepository.findById(5L).orElseThrow(UserNotFoundException::new); + + //페이징 객체 생성 + Pageable pageable = PageRequest.of(page, 15); + + // 최근 일주일간 생성한 뉴스 목록 조회 + LocalDateTime createdAt = LocalDateTime.now().minusDays(7); + List recentNews = newsRepository.findRecentNewsByUser(user, createdAt, pageable); + + return recentNews.stream() + .map(news -> RecentNewsRes.builder() + .newsId(news.getNewsId()) + .newsImageUrl(news.getNewsUrl()) + .headLine(news.getHeadLine()) + .noun(news.getKeyword().getKeyword()) + .verb(VerbType.getStringVerbType(news.getKeyword().getVerbType())) + .createdAt(news.getCreatedAt()) + .build()) + .toList(); + } + // User 정보 검증 private User validateUser(UserPrincipal userPrincipal) { diff --git a/src/main/java/com/movelog/domain/news/domain/repository/NewsRepository.java b/src/main/java/com/movelog/domain/news/domain/repository/NewsRepository.java index 644fcf0..4a38445 100644 --- a/src/main/java/com/movelog/domain/news/domain/repository/NewsRepository.java +++ b/src/main/java/com/movelog/domain/news/domain/repository/NewsRepository.java @@ -1,9 +1,27 @@ package com.movelog.domain.news.domain.repository; import com.movelog.domain.news.domain.News; +import com.movelog.domain.user.domain.User; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; + @Repository public interface NewsRepository extends JpaRepository { + + @Query("SELECT n FROM News n " + + "JOIN n.keyword k " + + "WHERE k.user = :user " + + "AND n.createdAt > :createdAt " + + "ORDER BY n.createdAt DESC") + List findRecentNewsByUser( + @Param("user") User user, + @Param("createdAt") LocalDateTime createdAt, + Pageable pageable + ); } diff --git a/src/main/java/com/movelog/domain/news/dto/response/RecentNewsRes.java b/src/main/java/com/movelog/domain/news/dto/response/RecentNewsRes.java new file mode 100644 index 0000000..c5b85b2 --- /dev/null +++ b/src/main/java/com/movelog/domain/news/dto/response/RecentNewsRes.java @@ -0,0 +1,36 @@ +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; + +import java.time.LocalDateTime; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class RecentNewsRes { + + @Schema( type = "int", example ="1", description="뉴스 ID") + private Long newsId; + + @Schema( type = "String", example ="https://movelog.s3.ap-northeast-2.amazonaws.com/record/2021-08-01/1.jpg", description="뉴스 이미지 url") + private String newsImageUrl; + + @Schema( type = "String", example ="5년 만의 첫 도전, 무엇이 그를 움직이게 했나?", description="뉴스 헤드라인 추천 내용입니다.") + private String headLine; + + @Schema( type = "String", example = "헬스", description="명사") + private String noun; + + @Schema( type = "String", example = "했어요", description="동사") + private String verb; + + @Schema( type = "LocalDateTime", example ="2021-08-01T00:00:00", description="뉴스 생성 시간") + private LocalDateTime createdAt; + + +} 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 40fdeaa..497e633 100644 --- a/src/main/java/com/movelog/domain/news/presentation/NewsController.java +++ b/src/main/java/com/movelog/domain/news/presentation/NewsController.java @@ -5,6 +5,7 @@ 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.news.dto.response.RecentNewsRes; import com.movelog.global.config.security.token.CurrentUser; import com.movelog.global.config.security.token.UserPrincipal; import com.movelog.global.payload.Message; @@ -90,6 +91,25 @@ public ResponseEntity getRecentKeywords( return ResponseEntity.ok(ApiResponseUtil.success(response)); } + @Operation(summary = "최근 뉴스 목록 조회 API", description = "최근 일주일간 생성한 뉴스 목록을 1페이지 당 15개씩 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "최근 뉴스 목록 조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "array", implementation = RecentNewsRes.class))), + @ApiResponse(responseCode = "400", description = "최근 뉴스 목록 조회 실패", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/week") + public ResponseEntity getRecentNews( + @Parameter(description = "Access Token을 입력해주세요.", required = true) + @AuthenticationPrincipal UserPrincipal userPrincipal, + @Parameter(description = "뉴스 목록의 페이지 번호를 입력해주세요. **Page는 1부터 시작됩니다!**", required = true) + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page + ) { + List response = newsService.getRecentNews(userPrincipal, page); + return ResponseEntity.ok(ApiResponseUtil.success(response)); + } + From 2f773c24a46b86c970cd9718b3c9f3f58566bb8e Mon Sep 17 00:00:00 2001 From: EunbeenDev Date: Wed, 15 Jan 2025 23:33:16 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[FEAT]=20=EC=B5=9C=EA=B7=BC=20=EC=9D=BC?= =?UTF-8?q?=EC=A3=BC=EC=9D=BC=20=EA=B0=84=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=83=9D=EC=84=B1=ED=95=9C=20=EB=89=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/movelog/domain/news/application/NewsService.java | 3 +++ 1 file changed, 3 insertions(+) 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 262ca58..bd4a2d2 100644 --- a/src/main/java/com/movelog/domain/news/application/NewsService.java +++ b/src/main/java/com/movelog/domain/news/application/NewsService.java @@ -96,6 +96,9 @@ public List getRecentNews(UserPrincipal userPrincipal, Integer pa LocalDateTime createdAt = LocalDateTime.now().minusDays(7); List recentNews = newsRepository.findRecentNewsByUser(user, createdAt, pageable); + // 최신순 정렬 + recentNews.sort((n1, n2) -> n2.getCreatedAt().compareTo(n1.getCreatedAt())); + return recentNews.stream() .map(news -> RecentNewsRes.builder() .newsId(news.getNewsId())