From 9ccaef943f88267ca1c70a35ba76866d8bb07851 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:12:01 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Feat=20:=20=EA=B0=9C=EB=85=90=20=ED=95=99?= =?UTF-8?q?=EC=8A=B5=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20api=20=EB=AA=85=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ConceptController.java | 20 +++++++++++++++---- .../conceptScrap/ConceptScrapRepository.java | 3 +++ .../service/concept/ConceptService.java | 10 ++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ripple/BE/learning/controller/ConceptController.java b/src/main/java/com/ripple/BE/learning/controller/ConceptController.java index f5b9ee2..b3e9634 100644 --- a/src/main/java/com/ripple/BE/learning/controller/ConceptController.java +++ b/src/main/java/com/ripple/BE/learning/controller/ConceptController.java @@ -14,6 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -24,7 +25,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/learning") -@Tag(name = "Learning", description = "학습 API") +@Tag(name = "Concept", description = "개념 학습 API") public class ConceptController { private final ConceptService conceptService; @@ -52,8 +53,8 @@ public ResponseEntity> completeConcept( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } - @Operation(summary = "개념 학습 저장", description = "개념 학습을 저장합니다.") - @PostMapping("/learning/{conceptId}/scrap") + @Operation(summary = "개념 학습 스크랩", description = "개념 학습을 스크랩 처리합니다.") + @PostMapping("/concept/{conceptId}/scrap") public ResponseEntity> scrapConcept( final @AuthenticationPrincipal CustomUserDetails currentUser, final @PathVariable("conceptId") long conceptId) { @@ -62,8 +63,19 @@ public ResponseEntity> scrapConcept( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } + @Operation(summary = "개념 학습 스크랩 취소", description = "개념 학습 스크랩을 취소합니다.") + @DeleteMapping("/concept/{conceptId}/scrap") + public ResponseEntity> unscrapConcept( + final @AuthenticationPrincipal CustomUserDetails currentUser, + final @PathVariable("conceptId") long id) { + + conceptService.removeScrapFromConcept(id, currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); + } + @Operation(summary = "개별 개념 학습 조회", description = "개별 개념 학습을 조회합니다.") - @GetMapping("/learning/{conceptId}") + @GetMapping("/concept/{conceptId}") public ResponseEntity> getConcept( final @PathVariable("conceptId") long conceptId) { diff --git a/src/main/java/com/ripple/BE/learning/repository/conceptScrap/ConceptScrapRepository.java b/src/main/java/com/ripple/BE/learning/repository/conceptScrap/ConceptScrapRepository.java index b627431..ecda074 100644 --- a/src/main/java/com/ripple/BE/learning/repository/conceptScrap/ConceptScrapRepository.java +++ b/src/main/java/com/ripple/BE/learning/repository/conceptScrap/ConceptScrapRepository.java @@ -1,6 +1,7 @@ package com.ripple.BE.learning.repository.conceptScrap; import com.ripple.BE.learning.domain.concept.ConceptScrap; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,4 +9,6 @@ public interface ConceptScrapRepository extends JpaRepository, ConceptScrapRepositoryCustom { Boolean existsByConcept_ConceptIdAndUserId(Long conceptId, Long userId); + + Optional findByConcept_ConceptIdAndUserId(Long conceptId, Long userId); } diff --git a/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java b/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java index 9b3d306..6163fc1 100644 --- a/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java +++ b/src/main/java/com/ripple/BE/learning/service/concept/ConceptService.java @@ -94,6 +94,16 @@ public void scrapConcept(final long userId, final long conceptId) { conceptScrapRepository.save(ConceptScrap.builder().user(user).concept(concept).build()); } + @Transactional + public void removeScrapFromConcept(final long conceptId, final long userId) { + ConceptScrap conceptScrap = + conceptScrapRepository + .findByConcept_ConceptIdAndUserId(conceptId, userId) + .orElseThrow(() -> new LearningException(LearningErrorCode.CONCEPT_SCRAP_NOT_FOUND)); + + conceptScrapRepository.delete(conceptScrap); + } + /** * 개별 개념 조회 * From 5b49adfea1429d4e4d2d0a01feafc343ed98dca3 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:14:55 +0900 Subject: [PATCH 2/9] =?UTF-8?q?Feat=20:=20=ED=80=B4=EC=A6=88=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=9E=A9=20=EC=B7=A8=EC=86=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20api=20=EB=AA=85=20=EC=88=98=EC=A0=95=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../learning/controller/QuizController.java | 20 +++++++++++++++---- .../exception/errorcode/QuizErrorCode.java | 3 ++- .../quizScrap/QuizScrapRepository.java | 3 +++ .../BE/learning/service/quiz/QuizService.java | 10 ++++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ripple/BE/learning/controller/QuizController.java b/src/main/java/com/ripple/BE/learning/controller/QuizController.java index 680606d..383d01a 100644 --- a/src/main/java/com/ripple/BE/learning/controller/QuizController.java +++ b/src/main/java/com/ripple/BE/learning/controller/QuizController.java @@ -19,6 +19,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -29,7 +30,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/learning") -@Tag(name = "Learning", description = "학습 API") +@Tag(name = "Quiz", description = "퀴즈 API") public class QuizController { private final QuizService quizService; @@ -74,8 +75,8 @@ public ResponseEntity> finishQuiz( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); } - @Operation(summary = "퀴즈 저장", description = "퀴즈를 저장합니다.") - @PostMapping("/learning/quiz/{quizId}/scrap") + @Operation(summary = "퀴즈 스크랩", description = "퀴즈를 스크랩 처리합니다.") + @PostMapping("/quiz/{quizId}/scrap") public ResponseEntity> scrapQuiz( final @AuthenticationPrincipal CustomUserDetails currentUser, final @PathVariable("quizId") long quizId) { @@ -84,8 +85,19 @@ public ResponseEntity> scrapQuiz( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } + @Operation(summary = "퀴즈 스크랩 취소", description = "퀴즈 스크랩을 취소합니다.") + @DeleteMapping("/quiz/{quizId}/scrap") + public ResponseEntity> unscrapQuiz( + final @AuthenticationPrincipal CustomUserDetails currentUser, + final @PathVariable("quizId") long id) { + + quizService.removeScrapFromQuiz(id, currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(ApiResponse.EMPTY_RESPONSE)); + } + @Operation(summary = "개별 퀴즈 조회", description = "개별 퀴즈를 조회합니다.") - @GetMapping("/learning/quiz/{quizId}") + @GetMapping("/quiz/{quizId}") public ResponseEntity> getSingleQuiz( final @PathVariable("quizId") long quizId) { diff --git a/src/main/java/com/ripple/BE/learning/exception/errorcode/QuizErrorCode.java b/src/main/java/com/ripple/BE/learning/exception/errorcode/QuizErrorCode.java index 179fca4..9b46973 100644 --- a/src/main/java/com/ripple/BE/learning/exception/errorcode/QuizErrorCode.java +++ b/src/main/java/com/ripple/BE/learning/exception/errorcode/QuizErrorCode.java @@ -9,7 +9,8 @@ @RequiredArgsConstructor public enum QuizErrorCode implements ErrorCode { QUIZ_NOT_FOUND(HttpStatus.NOT_FOUND, "Quiz not found"), - QUIZ_ALREADY_SCRAP(HttpStatus.BAD_REQUEST, "Quiz already scrap"); + QUIZ_ALREADY_SCRAP(HttpStatus.BAD_REQUEST, "Quiz already scrap"), + QUIZ_SCRAP_NOT_FOUND(HttpStatus.NOT_FOUND, "Quiz scrap not found"); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/ripple/BE/learning/repository/quizScrap/QuizScrapRepository.java b/src/main/java/com/ripple/BE/learning/repository/quizScrap/QuizScrapRepository.java index a3cc37d..61ee290 100644 --- a/src/main/java/com/ripple/BE/learning/repository/quizScrap/QuizScrapRepository.java +++ b/src/main/java/com/ripple/BE/learning/repository/quizScrap/QuizScrapRepository.java @@ -1,6 +1,7 @@ package com.ripple.BE.learning.repository.quizScrap; import com.ripple.BE.learning.domain.quiz.QuizScrap; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,4 +9,6 @@ public interface QuizScrapRepository extends JpaRepository, QuizScrapRepositoryCustom { Boolean existsByQuizIdAndUserId(Long quizId, Long userId); + + Optional findByQuizIdAndUserId(Long quizId, Long userId); } diff --git a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java index facd54f..d1db4ea 100644 --- a/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java +++ b/src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java @@ -180,6 +180,16 @@ public void scrapQuiz(final long userId, final long quizId) { quizScrapRepository.save(QuizScrap.builder().user(user).quiz(quiz).build()); } + @Transactional + public void removeScrapFromQuiz(final long quizId, final long userId) { + QuizScrap quizScrap = + quizScrapRepository + .findByQuizIdAndUserId(quizId, userId) + .orElseThrow(() -> new QuizException(QuizErrorCode.QUIZ_SCRAP_NOT_FOUND)); + + quizScrapRepository.delete(quizScrap); + } + /** * 개별 퀴즈 조회 * From 4f92d5c169dc6277d3cff93da47e60abf111078b Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:17:24 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Feat=20:=20=EB=82=B4=EA=B0=80=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=9E=A9=ED=95=9C=20=EC=9A=A9=EC=96=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#49)=20-?= =?UTF-8?q?=20=EC=B4=88=EC=84=B1=EB=B3=84=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=B3=84?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20=EC=9A=A9=EC=96=B4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ripple/BE/term/dto/TermListDTO.java | 4 ++ .../term/repository/TermScrapRepository.java | 3 +- .../repository/TermScrapRepositoryCustom.java | 11 ++++ .../TermScrapRepositoryCustomImpl.java | 57 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustom.java create mode 100644 src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustomImpl.java diff --git a/src/main/java/com/ripple/BE/term/dto/TermListDTO.java b/src/main/java/com/ripple/BE/term/dto/TermListDTO.java index d30aeee..8e5e896 100644 --- a/src/main/java/com/ripple/BE/term/dto/TermListDTO.java +++ b/src/main/java/com/ripple/BE/term/dto/TermListDTO.java @@ -13,4 +13,8 @@ public static TermListDTO toTermListDTO(Page termPage) { termPage.getTotalPages(), termPage.getNumber()); } + + public static TermListDTO toTermListDTO(List termList) { + return new TermListDTO(termList.stream().map(TermDTO::toTermDTO).toList(), 1, 0); + } } diff --git a/src/main/java/com/ripple/BE/term/repository/TermScrapRepository.java b/src/main/java/com/ripple/BE/term/repository/TermScrapRepository.java index ae6623f..b50367b 100644 --- a/src/main/java/com/ripple/BE/term/repository/TermScrapRepository.java +++ b/src/main/java/com/ripple/BE/term/repository/TermScrapRepository.java @@ -4,7 +4,8 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface TermScrapRepository extends JpaRepository { +public interface TermScrapRepository + extends JpaRepository, TermScrapRepositoryCustom { Optional findByTermIdAndUserId(long termId, long userId); diff --git a/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustom.java b/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustom.java new file mode 100644 index 0000000..e1e9fe2 --- /dev/null +++ b/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.ripple.BE.term.repository; + +import com.ripple.BE.term.domain.Term; +import java.util.List; + +public interface TermScrapRepositoryCustom { + + List findTermsScrappedByUserAndInitial(Long userId, String initial); + + List findTermsScrappedByUserAndKeyword(Long userId, String keyword); +} diff --git a/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustomImpl.java b/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustomImpl.java new file mode 100644 index 0000000..cda2d17 --- /dev/null +++ b/src/main/java/com/ripple/BE/term/repository/TermScrapRepositoryCustomImpl.java @@ -0,0 +1,57 @@ +package com.ripple.BE.term.repository; + +import static com.ripple.BE.term.domain.QTerm.*; +import static com.ripple.BE.term.domain.QTermScrap.*; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.ripple.BE.term.domain.Term; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TermScrapRepositoryCustomImpl implements TermScrapRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findTermsScrappedByUserAndInitial(Long userId, String initial) { + if (initial == null || initial.trim().isEmpty()) { + return queryFactory + .select(term) + .from(termScrap) + .join(termScrap.term, term) + .where(termScrap.user.id.eq(userId)) + .orderBy(termScrap.createdDate.desc()) + .fetch(); + } + + return queryFactory + .select(term) + .from(termScrap) + .join(termScrap.term, term) + .where(termScrap.user.id.eq(userId).and(term.initial.startsWith(initial))) + .orderBy(termScrap.createdDate.desc()) + .fetch(); + } + + @Override + public List findTermsScrappedByUserAndKeyword(Long userId, String keyword) { + if (keyword == null || keyword.trim().isEmpty()) { + return queryFactory + .select(term) + .from(termScrap) + .join(termScrap.term, term) + .where(termScrap.user.id.eq(userId)) + .orderBy(termScrap.createdDate.desc()) + .fetch(); + } + + return queryFactory + .select(term) + .from(termScrap) + .join(termScrap.term, term) + .where(termScrap.user.id.eq(userId).and(term.title.containsIgnoreCase(keyword))) + .orderBy(termScrap.createdDate.desc()) + .fetch(); + } +} From 0d9d89770977d69d32a2186ab8e52defc2441a6a Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:18:19 +0900 Subject: [PATCH 4/9] =?UTF-8?q?Feat=20:=20=EB=82=B4=EA=B0=80=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=9E=A9=ED=95=9C=20=EB=89=B4=EC=8A=A4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ripple/BE/news/dto/NewsListDTO.java | 4 +++ .../newscrap/NewsScrapRepository.java | 3 ++- .../newscrap/NewsScrapRepositoryCustom.java | 9 +++++++ .../NewsScrapRepositoryCustomImpl.java | 26 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustom.java create mode 100644 src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustomImpl.java diff --git a/src/main/java/com/ripple/BE/news/dto/NewsListDTO.java b/src/main/java/com/ripple/BE/news/dto/NewsListDTO.java index adeb364..029a7ba 100644 --- a/src/main/java/com/ripple/BE/news/dto/NewsListDTO.java +++ b/src/main/java/com/ripple/BE/news/dto/NewsListDTO.java @@ -12,4 +12,8 @@ public static NewsListDTO toNewsListDTO(Page newsPage) { newsPage.getTotalPages(), newsPage.getNumber()); } + + public static NewsListDTO toNewsListDTO(List newsList) { + return new NewsListDTO(newsList.stream().map(NewsDTO::toNewsDTO).toList(), 1, 0); + } } diff --git a/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepository.java b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepository.java index 4daee03..201cace 100644 --- a/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepository.java +++ b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepository.java @@ -4,7 +4,8 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface NewsScrapRepository extends JpaRepository { +public interface NewsScrapRepository + extends JpaRepository, NewsScrapRepositoryCustom { Optional findByNewsIdAndUserId(long newsId, long userId); diff --git a/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustom.java b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustom.java new file mode 100644 index 0000000..cc95322 --- /dev/null +++ b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.ripple.BE.news.repository.newscrap; + +import com.ripple.BE.news.domain.News; +import java.util.List; + +public interface NewsScrapRepositoryCustom { + + List findNewsScrappedByUser(Long userId); +} diff --git a/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustomImpl.java b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustomImpl.java new file mode 100644 index 0000000..2d3eb5e --- /dev/null +++ b/src/main/java/com/ripple/BE/news/repository/newscrap/NewsScrapRepositoryCustomImpl.java @@ -0,0 +1,26 @@ +package com.ripple.BE.news.repository.newscrap; + +import static com.ripple.BE.news.domain.QNews.*; +import static com.ripple.BE.news.domain.QNewsScrap.*; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.ripple.BE.news.domain.News; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NewsScrapRepositoryCustomImpl implements NewsScrapRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findNewsScrappedByUser(Long userId) { + return queryFactory + .select(news) + .from(newsScrap) + .join(newsScrap.news, news) + .where(newsScrap.user.id.eq(userId)) + .orderBy(newsScrap.createdDate.desc()) + .fetch(); + } +} From 871b5f3149a221b654f968e93a4868a33cc1fe91 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:20:10 +0900 Subject: [PATCH 5/9] =?UTF-8?q?Feat=20:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20ap?= =?UTF-8?q?i=20=EC=B6=94=EA=B0=80=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BE/user/controller/UserController.java | 52 +++++++++++++++++++ .../ripple/BE/user/service/MyPageService.java | 33 +++++++++--- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ripple/BE/user/controller/UserController.java b/src/main/java/com/ripple/BE/user/controller/UserController.java index eb61e53..ee2ceb2 100644 --- a/src/main/java/com/ripple/BE/user/controller/UserController.java +++ b/src/main/java/com/ripple/BE/user/controller/UserController.java @@ -1,5 +1,7 @@ package com.ripple.BE.user.controller; +import static com.ripple.BE.global.exception.errorcode.GlobalErrorCode.*; + import com.ripple.BE.global.dto.response.ApiResponse; import com.ripple.BE.learning.dto.ConceptListDTO; import com.ripple.BE.learning.dto.FailQuizListDTO; @@ -7,10 +9,15 @@ import com.ripple.BE.learning.dto.response.FailQuizListResponse; import com.ripple.BE.learning.dto.response.ScrapConceptListResponse; import com.ripple.BE.learning.dto.response.ScrapQuizListResponse; +import com.ripple.BE.news.dto.NewsListDTO; +import com.ripple.BE.news.dto.response.NewsListResponse; import com.ripple.BE.post.dto.LikeCommentListDTO; import com.ripple.BE.post.dto.PostListDTO; import com.ripple.BE.post.dto.response.LikeCommentListResponse; import com.ripple.BE.post.dto.response.PostListResponse; +import com.ripple.BE.term.dto.TermListDTO; +import com.ripple.BE.term.dto.response.TermListResponse; +import com.ripple.BE.term.exception.TermException; import com.ripple.BE.user.domain.CustomUserDetails; import com.ripple.BE.user.domain.type.Level; import com.ripple.BE.user.dto.ProgressDTO; @@ -159,4 +166,49 @@ public ResponseEntity> getMyScrapConcepts( .body( ApiResponse.from(ScrapConceptListResponse.toScrapConceptListResponse(conceptListDTO))); } + + @Operation(summary = "내가 스크랩한 뉴스 조회", description = "로그인한 유저가 스크랩한 뉴스를 조회합니다.") + @GetMapping("/scrap-news") + public ResponseEntity> getMyScrapNews( + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + + NewsListDTO newsListDTO = myPageService.getMyScrapNews(customUserDetails.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.from(NewsListResponse.toNewsListResponse(newsListDTO))); + } + + @Operation( + summary = "내가 스크랩한 용어 조회", + description = "로그인한 유저가 스크랩한 용어를 조회합니다. 자음 별로 조회할 수 있으며, 아무것도 입력하지 않으면 모든 용어를 조회합니다.") + @GetMapping("/scrap-terms") + public ResponseEntity> getMyScrapTermsByInitial( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam(value = "initial", required = false) final String initial) { + + if (initial != null && initial.length() != 1) { + throw new TermException(INVALID_PARAMETER); + } + + TermListDTO termListDTO = + myPageService.getMyScrapTermsByInitial(customUserDetails.getId(), initial); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.from(TermListResponse.toTermListResponse(termListDTO))); + } + + @Operation( + summary = "내가 스크랩한 용어 조회", + description = "로그인한 유저가 스크랩한 용어를 조회합니다. 키워드 별로 조회할 수 있으며, 아무것도 입력하지 않으면 모든 용어를 조회합니다.") + @GetMapping("/scrap-terms/search") + public ResponseEntity> getMyScrapTermsByKeyword( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @RequestParam(value = "keyword", required = false) final String keyword) { + + TermListDTO termListDTO = + myPageService.getMyScrapTermsByKeyword(customUserDetails.getId(), keyword); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.from(TermListResponse.toTermListResponse(termListDTO))); + } } diff --git a/src/main/java/com/ripple/BE/user/service/MyPageService.java b/src/main/java/com/ripple/BE/user/service/MyPageService.java index 64a6e00..b4ab586 100644 --- a/src/main/java/com/ripple/BE/user/service/MyPageService.java +++ b/src/main/java/com/ripple/BE/user/service/MyPageService.java @@ -8,6 +8,9 @@ import com.ripple.BE.learning.repository.conceptScrap.ConceptScrapRepository; import com.ripple.BE.learning.repository.quiz.QuizRepository; import com.ripple.BE.learning.repository.quizScrap.QuizScrapRepository; +import com.ripple.BE.news.domain.News; +import com.ripple.BE.news.dto.NewsListDTO; +import com.ripple.BE.news.repository.newscrap.NewsScrapRepository; import com.ripple.BE.post.domain.Comment; import com.ripple.BE.post.domain.Post; import com.ripple.BE.post.dto.LikeCommentListDTO; @@ -17,6 +20,9 @@ import com.ripple.BE.post.repository.post.PostRepository; import com.ripple.BE.post.repository.postlike.PostLikeRepository; import com.ripple.BE.post.repository.postscrap.PostScrapRepository; +import com.ripple.BE.term.domain.Term; +import com.ripple.BE.term.dto.TermListDTO; +import com.ripple.BE.term.repository.TermScrapRepository; import com.ripple.BE.user.domain.type.Level; import java.util.List; import lombok.RequiredArgsConstructor; @@ -38,8 +44,9 @@ public class MyPageService { private final QuizRepository quizRepository; private final QuizScrapRepository quizScrapRepository; private final ConceptScrapRepository conceptScrapRepository; + private final TermScrapRepository termScrapRepository; + private final NewsScrapRepository newsScrapRepository; - @Transactional(readOnly = true) public PostListDTO getMyPosts(final long userId) { List posts = postRepository.findUserNormalPosts(userId); @@ -47,7 +54,6 @@ public PostListDTO getMyPosts(final long userId) { return PostListDTO.toPostListDTO(posts); } - @Transactional(readOnly = true) public PostListDTO getMyLikePosts(final long userId) { List posts = postLikeRepository.findPostsLikedByUser(userId); @@ -55,7 +61,6 @@ public PostListDTO getMyLikePosts(final long userId) { return PostListDTO.toPostListDTO(posts); } - @Transactional(readOnly = true) public PostListDTO getMyCommentPosts(final long userId) { List posts = commentRepository.findPostsCommentedByUser(userId); @@ -63,7 +68,6 @@ public PostListDTO getMyCommentPosts(final long userId) { return PostListDTO.toPostListDTO(posts); } - @Transactional(readOnly = true) public PostListDTO getMyScrapPosts(final long userId) { List posts = postScrapRepository.findPostsScrappedByUser(userId); @@ -71,7 +75,6 @@ public PostListDTO getMyScrapPosts(final long userId) { return PostListDTO.toPostListDTO(posts); } - @Transactional(readOnly = true) public FailQuizListDTO getMyFailQuizzes(final long userId, Level level) { List failedQuizzesByUserAndLevel = @@ -80,7 +83,6 @@ public FailQuizListDTO getMyFailQuizzes(final long userId, Level level) { return FailQuizListDTO.toFailQuizListDTO(failedQuizzesByUserAndLevel); } - @Transactional(readOnly = true) public LikeCommentListDTO getMyLikeComments(final long userId) { List commentsLikedByUser = commentLikeRepository.findCommentsLikedByUser(userId); @@ -102,4 +104,23 @@ public ConceptListDTO getMyConcepts(final long userId, final Level level) { return ConceptListDTO.toScrapConceptListDTO(concepts); } + + public TermListDTO getMyScrapTermsByInitial(final long userId, final String initial) { + + List terms = termScrapRepository.findTermsScrappedByUserAndInitial(userId, initial); + + return TermListDTO.toTermListDTO(terms); + } + + public TermListDTO getMyScrapTermsByKeyword(final long userId, final String keyword) { + + List terms = termScrapRepository.findTermsScrappedByUserAndKeyword(userId, keyword); + + return TermListDTO.toTermListDTO(terms); + } + + public NewsListDTO getMyScrapNews(final long userId) { + List news = newsScrapRepository.findNewsScrappedByUser(userId); + return NewsListDTO.toNewsListDTO(news); + } } From af87156ea0e4a15d32a713de019066a285c8ebb4 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:20:51 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Refactor=20:=20api=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A9=94=EC=86=8C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ripple/BE/learning/controller/LearningController.java | 6 ++++-- .../java/com/ripple/BE/news/controller/NewsController.java | 2 +- .../java/com/ripple/BE/post/controller/PostController.java | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ripple/BE/learning/controller/LearningController.java b/src/main/java/com/ripple/BE/learning/controller/LearningController.java index d92b289..9d20fb8 100644 --- a/src/main/java/com/ripple/BE/learning/controller/LearningController.java +++ b/src/main/java/com/ripple/BE/learning/controller/LearningController.java @@ -25,14 +25,16 @@ public class LearningController { private final LearningSetService learningSetService; private final LearningAdminService learningAdminService; - @Operation(summary = "학습 세트 생성", description = "엑셀 파일로부터 학습 세트를 생성합니다.") + @Operation(summary = "학습 세트 생성 (관리자 전용)", description = "엑셀 파일로부터 학습 세트를 생성합니다. (관리자 전용)") @PostMapping("/excel") public ResponseEntity> saveLearningSetsByExcel() { learningAdminService.createLearningSetByExcel(); return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); } - @Operation(summary = "레벨별 학습 세트 조회", description = "레벨별 전체 학습 세트를 조회합니다.") + @Operation( + summary = "레벨별 학습 세트 조회", + description = "레벨별 전체 학습 세트를 조회합니다. 사용자의 현재 레벨에 해당하는 학습 세트 목록을 반환합니다.") @PostMapping public ResponseEntity> getLearningSets( final @AuthenticationPrincipal CustomUserDetails currentUser) { diff --git a/src/main/java/com/ripple/BE/news/controller/NewsController.java b/src/main/java/com/ripple/BE/news/controller/NewsController.java index 525a800..8e652c8 100644 --- a/src/main/java/com/ripple/BE/news/controller/NewsController.java +++ b/src/main/java/com/ripple/BE/news/controller/NewsController.java @@ -67,7 +67,7 @@ public ResponseEntity> scrapNews( @Operation(summary = "뉴스 스크랩 취소", description = "뉴스 스크랩을 취소합니다.") @DeleteMapping("/{id}/scrap") - public ResponseEntity> unscrapPost( + public ResponseEntity> unscrapNews( final @AuthenticationPrincipal CustomUserDetails currentUser, final @PathVariable("id") long id) { diff --git a/src/main/java/com/ripple/BE/post/controller/PostController.java b/src/main/java/com/ripple/BE/post/controller/PostController.java index 0903936..e8301b6 100644 --- a/src/main/java/com/ripple/BE/post/controller/PostController.java +++ b/src/main/java/com/ripple/BE/post/controller/PostController.java @@ -40,7 +40,11 @@ public class PostController { private final PostService postService; - @Operation(summary = "게시물 작성", description = "게시물을 작성합니다. 게시물을 등록하기 전 이미지 등록을 완료해주세요") + @Operation( + summary = "게시물 작성", + description = + "게시물을 작성합니다. 게시물을 등록하기 전 이미지 등록을 완료해주세요. 이미지 등록 후 반한 된 이미지 ID를 입력해주세요." + + "게시물 타입은 FREE ,QUESTION, INFORMATION, BOOK_RECOMMENDATION 중 하나여야 합니다.") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> createPost( final @AuthenticationPrincipal CustomUserDetails currentUser, From 4ce693ba099b4f80625c057dfbf7b327343fc905 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:21:08 +0900 Subject: [PATCH 7/9] =?UTF-8?q?Refactor=20:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BE/learning/exception/errorcode/LearningErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/ripple/BE/learning/exception/errorcode/LearningErrorCode.java b/src/main/java/com/ripple/BE/learning/exception/errorcode/LearningErrorCode.java index ab40a55..51beb68 100644 --- a/src/main/java/com/ripple/BE/learning/exception/errorcode/LearningErrorCode.java +++ b/src/main/java/com/ripple/BE/learning/exception/errorcode/LearningErrorCode.java @@ -14,6 +14,7 @@ public enum LearningErrorCode implements ErrorCode { CONCEPT_NOT_FOUND(HttpStatus.NOT_FOUND, "Concept not found"), CONCEPT_ALREADY_SCRAP(HttpStatus.BAD_REQUEST, "Concept already scrap"), + CONCEPT_SCRAP_NOT_FOUND(HttpStatus.NOT_FOUND, "Concept scrap not found"), QUIZ_PROGRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "Quiz progress not found"), QUIZ_NOT_FOUND(HttpStatus.NOT_FOUND, "Quiz not found"); From 54b758756f25878aa477e3ff2a74dd66265a84c0 Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:21:25 +0900 Subject: [PATCH 8/9] =?UTF-8?q?Refactor=20:=20=EC=8A=A4=ED=81=AC=EB=9E=A9?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/repository/postscrap/PostScrapRepositoryCustomImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/ripple/BE/post/repository/postscrap/PostScrapRepositoryCustomImpl.java b/src/main/java/com/ripple/BE/post/repository/postscrap/PostScrapRepositoryCustomImpl.java index 49857d2..111300d 100644 --- a/src/main/java/com/ripple/BE/post/repository/postscrap/PostScrapRepositoryCustomImpl.java +++ b/src/main/java/com/ripple/BE/post/repository/postscrap/PostScrapRepositoryCustomImpl.java @@ -20,6 +20,7 @@ public List findPostsScrappedByUser(Long userId) { .from(postScrap) .join(postScrap.post, post) .where(postScrap.user.id.eq(userId)) + .orderBy(postScrap.createdDate.desc()) .fetch(); } } From c6ff9fef4d7b5f1a5c63cbe4b895e4ab195dfeff Mon Sep 17 00:00:00 2001 From: jeongchanmin Date: Sun, 2 Feb 2025 15:21:46 +0900 Subject: [PATCH 9/9] =?UTF-8?q?Refactor=20:=20=EB=A0=88=EB=B2=A8=EB=B3=84?= =?UTF-8?q?=20=EC=A7=84=EB=8F=84=EC=9C=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=EC=BA=90=EC=8B=B1=20=20=EC=B6=94=EA=B0=80=20(#?= =?UTF-8?q?49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ripple/BE/user/dto/ProgressResponse.java | 11 ++- .../BE/user/service/UserProgressService.java | 90 ++++++++++++++----- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/ripple/BE/user/dto/ProgressResponse.java b/src/main/java/com/ripple/BE/user/dto/ProgressResponse.java index 2aad047..3eb82c5 100644 --- a/src/main/java/com/ripple/BE/user/dto/ProgressResponse.java +++ b/src/main/java/com/ripple/BE/user/dto/ProgressResponse.java @@ -3,11 +3,18 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.ripple.BE.user.domain.type.Level; import java.util.Map; +import java.util.stream.Collectors; @JsonInclude(JsonInclude.Include.NON_NULL) -public record ProgressResponse(Map progress) { +public record ProgressResponse(Map progress) { public static ProgressResponse toProgressResponse(final ProgressDTO levelDTO) { - return new ProgressResponse(levelDTO.progress()); + + Map progressMap = + levelDTO.progress().entrySet().stream() + .collect( + Collectors.toMap(Map.Entry::getKey, entry -> (int) Math.round(entry.getValue()))); + + return new ProgressResponse(progressMap); } } diff --git a/src/main/java/com/ripple/BE/user/service/UserProgressService.java b/src/main/java/com/ripple/BE/user/service/UserProgressService.java index c96a134..425c940 100644 --- a/src/main/java/com/ripple/BE/user/service/UserProgressService.java +++ b/src/main/java/com/ripple/BE/user/service/UserProgressService.java @@ -7,44 +7,94 @@ import com.ripple.BE.user.dto.ProgressDTO; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @Slf4j +@Transactional(readOnly = true) public class UserProgressService { + private final RedisTemplate redisTemplate; + private static final String PROGRESS_KEY_PREFIX = "progress:"; + private static final String TOTAL_SETS_KEY = "totalSets"; + + private static final int PROGRESS_CACHE_EXPIRE_MINUTES = 10; + private static final int TOTAL_SETS_CACHE_EXPIRE_DAYS = 1; + private final UserService userService; private final ConceptRepository conceptRepository; private final QuizRepository quizRepository; - @Transactional(readOnly = true) - public ProgressDTO getLearningSetCompletionRate(final long userId) { + /** + * 레벨별 총 학습 세트 수를 조회한다. 개념 + 퀴즈 이 데이터는 캐시에 저장되며, 1일간 유효하다. + * + * @return 레벨별 총 학습 세트 수 + */ + public Map getTotalLearningSetsCount() { + Map totalSets = + (Map) redisTemplate.opsForValue().get(TOTAL_SETS_KEY); + + if (totalSets == null) { + totalSets = + Arrays.stream(Level.values()) + .collect( + Collectors.toMap( + Enum::name, + level -> + conceptRepository.countByLevel(level) + + quizRepository.countByLevel(level))); + + redisTemplate + .opsForValue() + .set(TOTAL_SETS_KEY, totalSets, TOTAL_SETS_CACHE_EXPIRE_DAYS, TimeUnit.DAYS); + } + + return totalSets; + } + /** + * 유저의 레벨별 학습 완료율을 조회한다. 이 데이터는 캐시에 저장되며, 10분간 유효하다. + * + * @param userId 유저 ID + * @return 레벨별 학습 완료율 + */ + public ProgressDTO getLearningSetCompletionRate(final long userId) { User user = userService.findUserById(userId); + String cacheKey = PROGRESS_KEY_PREFIX + userId; + + Map totalSets = getTotalLearningSetsCount(); + ProgressDTO cachedProgress = (ProgressDTO) redisTemplate.opsForValue().get(cacheKey); + + // 캐시된 데이터가 있으면 바로 반환 + if (cachedProgress != null) { + return cachedProgress; + } + + // 레벨별 완료율 계산, 퍼센트 단위로 변환 + ProgressDTO progressDTO = + ProgressDTO.toProgressDTO( + Arrays.stream(Level.values()) + .collect( + Collectors.toMap( + level -> level, + level -> + (double) user.getCompletedCountByLevel(level) + / totalSets.get(level.name()) + * 100))); + + // 캐싱 (10분간 저장) + redisTemplate + .opsForValue() + .set(cacheKey, progressDTO, PROGRESS_CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES); - // 레벨별 총 개념과 퀴즈 개수 계산 - Map totalSets = - Arrays.stream(Level.values()) - .collect( - Collectors.toMap( - level -> level, - level -> - conceptRepository.countByLevel(level) - + quizRepository.countByLevel(level))); - - // 레벨별 완료율 계산 - return ProgressDTO.toProgressDTO( - Arrays.stream(Level.values()) - .collect( - Collectors.toMap( - level -> level, - level -> - (double) user.getCompletedCountByLevel(level) / totalSets.get(level)))); + return progressDTO; } }