Skip to content

Commit

Permalink
Merge pull request #50 from IT-Cotato/feat/scrap-etc-#49
Browse files Browse the repository at this point in the history
[Feat] 스크랩 기능 추가 및 레벨별 진도율 조회 메소드 기능 수정
  • Loading branch information
chanmin-00 authored Feb 3, 2025
2 parents 7e5ad1c + 1290a3e commit 5aafd42
Show file tree
Hide file tree
Showing 24 changed files with 346 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -52,8 +53,8 @@ public ResponseEntity<ApiResponse<?>> 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<ApiResponse<?>> scrapConcept(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("conceptId") long conceptId) {
Expand All @@ -62,8 +63,19 @@ public ResponseEntity<ApiResponse<?>> scrapConcept(
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}

@Operation(summary = "개념 학습 스크랩 취소", description = "개념 학습 스크랩을 취소합니다.")
@DeleteMapping("/concept/{conceptId}/scrap")
public ResponseEntity<ApiResponse<Object>> 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<ApiResponse<Object>> getConcept(
final @PathVariable("conceptId") long conceptId) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ public class LearningController {
private final LearningSetService learningSetService;
private final LearningAdminService learningAdminService;

@Operation(summary = "학습 세트 생성", description = "엑셀 파일로부터 학습 세트를 생성합니다.")
@Operation(summary = "학습 세트 생성 (관리자 전용)", description = "엑셀 파일로부터 학습 세트를 생성합니다. (관리자 전용)")
@PostMapping("/excel")
public ResponseEntity<ApiResponse<?>> saveLearningSetsByExcel() {
learningAdminService.createLearningSetByExcel();
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}

@Operation(summary = "레벨별 학습 세트 조회", description = "레벨별 전체 학습 세트를 조회합니다.")

@Operation(
summary = "레벨별 학습 세트 조회",
description = "레벨별 전체 학습 세트를 조회합니다. 사용자의 현재 레벨에 해당하는 학습 세트 목록을 반환합니다.")
@GetMapping
public ResponseEntity<ApiResponse<Object>> getLearningSets(
final @AuthenticationPrincipal CustomUserDetails currentUser) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -74,8 +75,8 @@ public ResponseEntity<ApiResponse<Object>> 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<ApiResponse<?>> scrapQuiz(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("quizId") long quizId) {
Expand All @@ -84,8 +85,19 @@ public ResponseEntity<ApiResponse<?>> scrapQuiz(
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}

@Operation(summary = "퀴즈 스크랩 취소", description = "퀴즈 스크랩을 취소합니다.")
@DeleteMapping("/quiz/{quizId}/scrap")
public ResponseEntity<ApiResponse<Object>> 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<ApiResponse<Object>> getSingleQuiz(
final @PathVariable("quizId") long quizId) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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;

@Repository
public interface ConceptScrapRepository
extends JpaRepository<ConceptScrap, Long>, ConceptScrapRepositoryCustom {
Boolean existsByConcept_ConceptIdAndUserId(Long conceptId, Long userId);

Optional<ConceptScrap> findByConcept_ConceptIdAndUserId(Long conceptId, Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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;

@Repository
public interface QuizScrapRepository
extends JpaRepository<QuizScrap, Long>, QuizScrapRepositoryCustom {
Boolean existsByQuizIdAndUserId(Long quizId, Long userId);

Optional<QuizScrap> findByQuizIdAndUserId(Long quizId, Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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);
}

/**
* 개별 개념 조회
*
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/ripple/BE/learning/service/quiz/QuizService.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,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);
}

/**
* 개별 퀴즈 조회
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public ResponseEntity<ApiResponse<Object>> scrapNews(

@Operation(summary = "뉴스 스크랩 취소", description = "뉴스 스크랩을 취소합니다.")
@DeleteMapping("/{id}/scrap")
public ResponseEntity<ApiResponse<Object>> unscrapPost(
public ResponseEntity<ApiResponse<Object>> unscrapNews(
final @AuthenticationPrincipal CustomUserDetails currentUser,
final @PathVariable("id") long id) {

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/ripple/BE/news/dto/NewsListDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ public static NewsListDTO toNewsListDTO(Page<News> newsPage) {
newsPage.getTotalPages(),
newsPage.getNumber());
}

public static NewsListDTO toNewsListDTO(List<News> newsList) {
return new NewsListDTO(newsList.stream().map(NewsDTO::toNewsDTO).toList(), 1, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NewsScrapRepository extends JpaRepository<NewsScrap, Long> {
public interface NewsScrapRepository
extends JpaRepository<NewsScrap, Long>, NewsScrapRepositoryCustom {

Optional<NewsScrap> findByNewsIdAndUserId(long newsId, long userId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<News> findNewsScrappedByUser(Long userId);
}
Original file line number Diff line number Diff line change
@@ -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<News> findNewsScrappedByUser(Long userId) {
return queryFactory
.select(news)
.from(newsScrap)
.join(newsScrap.news, news)
.where(newsScrap.user.id.eq(userId))
.orderBy(newsScrap.createdDate.desc())
.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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<ApiResponse<Object>> createPost(
final @AuthenticationPrincipal CustomUserDetails currentUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public List<Post> findPostsScrappedByUser(Long userId) {
.from(postScrap)
.join(postScrap.post, post)
.where(postScrap.user.id.eq(userId))
.orderBy(postScrap.createdDate.desc())
.fetch();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/ripple/BE/term/dto/TermListDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public static TermListDTO toTermListDTO(Page<Term> termPage) {
termPage.getTotalPages(),
termPage.getNumber());
}

public static TermListDTO toTermListDTO(List<Term> termList) {
return new TermListDTO(termList.stream().map(TermDTO::toTermDTO).toList(), 1, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TermScrapRepository extends JpaRepository<TermScrap, Long> {
public interface TermScrapRepository
extends JpaRepository<TermScrap, Long>, TermScrapRepositoryCustom {

Optional<TermScrap> findByTermIdAndUserId(long termId, long userId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Term> findTermsScrappedByUserAndInitial(Long userId, String initial);

List<Term> findTermsScrappedByUserAndKeyword(Long userId, String keyword);
}
Original file line number Diff line number Diff line change
@@ -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<Term> 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<Term> 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();
}
}
Loading

0 comments on commit 5aafd42

Please sign in to comment.