diff --git a/backend/build.gradle b/backend/build.gradle index 1b77cc8b..caf92a70 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -44,6 +44,7 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' /* ETC */ + implementation 'org.apache.commons:commons-lang3:3.12.0' annotationProcessor 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok' diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java b/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java index b25897c2..8439f4e3 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java @@ -3,16 +3,26 @@ import io.f1.backend.domain.quiz.app.QuizService; import io.f1.backend.domain.quiz.dto.QuizCreateRequest; import io.f1.backend.domain.quiz.dto.QuizCreateResponse; +import io.f1.backend.domain.quiz.dto.QuizListPageResponse; +import io.f1.backend.domain.quiz.dto.QuizUpdateRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +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; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -35,4 +45,37 @@ public ResponseEntity saveQuiz( return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + @DeleteMapping("/{quizId}") + public ResponseEntity deleteQuiz(@PathVariable Long quizId) { + + quizService.deleteQuiz(quizId); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/{quizId}") + public ResponseEntity updateQuiz( + @PathVariable Long quizId, + @RequestPart(required = false) MultipartFile thumbnailFile, + @RequestPart QuizUpdateRequest request) + throws IOException { + + quizService.updateQuiz(quizId, thumbnailFile, request); + + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity getQuizzes( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String title, + @RequestParam(required = false) String creator) { + + Pageable pageable = + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt")); + QuizListPageResponse quizzes = quizService.getQuizzes(title, creator, pageable); + + return ResponseEntity.ok().body(quizzes); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java index dde9f6f3..94a8dba4 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java @@ -1,20 +1,31 @@ package io.f1.backend.domain.quiz.app; +import static io.f1.backend.domain.quiz.mapper.QuizMapper.pageQuizToPageQuizListResponse; import static io.f1.backend.domain.quiz.mapper.QuizMapper.quizCreateRequestToQuiz; import static io.f1.backend.domain.quiz.mapper.QuizMapper.quizToQuizCreateResponse; +import static io.f1.backend.domain.quiz.mapper.QuizMapper.toQuizListPageResponse; + +import static java.nio.file.Files.deleteIfExists; import io.f1.backend.domain.question.app.QuestionService; import io.f1.backend.domain.question.dto.QuestionRequest; import io.f1.backend.domain.quiz.dao.QuizRepository; import io.f1.backend.domain.quiz.dto.QuizCreateRequest; import io.f1.backend.domain.quiz.dto.QuizCreateResponse; +import io.f1.backend.domain.quiz.dto.QuizListPageResponse; +import io.f1.backend.domain.quiz.dto.QuizListResponse; +import io.f1.backend.domain.quiz.dto.QuizUpdateRequest; import io.f1.backend.domain.quiz.entity.Quiz; import io.f1.backend.domain.user.dao.UserRepository; import io.f1.backend.domain.user.entity.User; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -23,8 +34,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.NoSuchElementException; import java.util.UUID; +@Slf4j @Service @RequiredArgsConstructor public class QuizService { @@ -35,6 +48,8 @@ public class QuizService { @Value("${file.default-thumbnail-url}") private String defaultThumbnailPath; + private final String DEFAULT = "default"; + // TODO : 시큐리티 구현 이후 삭제해도 되는 의존성 주입 private final UserRepository userRepository; private final QuestionService questionService; @@ -92,6 +107,92 @@ private String getExtension(String filename) { return filename.substring(filename.lastIndexOf(".") + 1); } + @Transactional + public void deleteQuiz(Long quizId) { + + Quiz quiz = + quizRepository + .findById(quizId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다.")); + + // TODO : util 메서드에서 사용자 ID 꺼내쓰는 식으로 수정하기 + if (1L != quiz.getCreator().getId()) { + throw new RuntimeException("권한이 없습니다."); + } + + deleteThumbnailFile(quiz.getThumbnailUrl()); + quizRepository.deleteById(quizId); + } + + @Transactional + public void updateQuiz(Long quizId, MultipartFile thumbnailFile, QuizUpdateRequest request) + throws IOException { + + Quiz quiz = + quizRepository + .findById(quizId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다.")); + + if (request.title() != null) { + quiz.changeTitle(request.title()); + } + + if (request.description() != null) { + quiz.changeDescription(request.description()); + } + + if (thumbnailFile != null && !thumbnailFile.isEmpty()) { + validateImageFile(thumbnailFile); + String newThumbnailPath = convertToThumbnailPath(thumbnailFile); + + deleteThumbnailFile(quiz.getThumbnailUrl()); + quiz.changeThumbnailUrl(newThumbnailPath); + } + } + + private void deleteThumbnailFile(String oldFilename) { + if (oldFilename.contains(DEFAULT)) { + return; + } + + // oldFilename : /images/thumbnail/123asd.jpg + // filename : 123asd.jpg + String filename = oldFilename.substring(oldFilename.lastIndexOf("/") + 1); + Path filePath = Paths.get(uploadPath, filename).toAbsolutePath(); + + try { + boolean deleted = deleteIfExists(filePath); + if (deleted) { + log.info("기존 썸네일 삭제 완료 : {}", filePath); + } else { + log.info("기존 썸네일 존재 X : {}", filePath); + } + } catch (IOException e) { + log.error("기존 썸네일 삭제 중 오류 : {}", filePath); + throw new RuntimeException(e); + } + } + + @Transactional(readOnly = true) + public QuizListPageResponse getQuizzes(String title, String creator, Pageable pageable) { + + Page quizzes; + + // 검색어가 있을 때 + if (StringUtils.isBlank(title)) { + quizzes = quizRepository.findQuizzesByTitleContaining(title, pageable); + } else if (StringUtils.isBlank(creator)) { + quizzes = quizRepository.findQuizzesByCreator_NicknameContaining(creator, pageable); + } else { // 검색어가 없을 때 혹은 빈 문자열일 때 + quizzes = quizRepository.findAll(pageable); + } + + Page quizListResponses = pageQuizToPageQuizListResponse(quizzes); + + return toQuizListPageResponse(quizListResponses); + } + + @Transactional(readOnly = true) public Quiz getQuizById(Long quizId) { return quizRepository .findById(quizId) diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java b/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java index a88c98e7..82b6bd16 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java @@ -2,6 +2,13 @@ import io.f1.backend.domain.quiz.entity.Quiz; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -public interface QuizRepository extends JpaRepository {} +public interface QuizRepository extends JpaRepository { + + Page findQuizzesByTitleContaining(String title, Pageable pageable); + + Page findQuizzesByCreator_NicknameContaining(String creator, Pageable pageable); +} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListPageResponse.java b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListPageResponse.java new file mode 100644 index 00000000..f10457f2 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListPageResponse.java @@ -0,0 +1,6 @@ +package io.f1.backend.domain.quiz.dto; + +import java.util.List; + +public record QuizListPageResponse( + int totalPages, int currentPage, long totalElements, List quiz) {} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListResponse.java b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListResponse.java new file mode 100644 index 00000000..8a443fab --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizListResponse.java @@ -0,0 +1,9 @@ +package io.f1.backend.domain.quiz.dto; + +public record QuizListResponse( + Long quizId, + String title, + String description, + String creatorNickname, + int numberOfQuestion, + String thumbnailUrl) {} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java new file mode 100644 index 00000000..30065d4f --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.quiz.dto; + +public record QuizUpdateRequest(String title, String description) {} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java b/backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java index 5168a1e1..fde07c7e 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java @@ -69,4 +69,16 @@ public Quiz( public void addQuestion(Question question) { this.questions.add(question); } + + public void changeTitle(String title) { + this.title = title; + } + + public void changeDescription(String description) { + this.description = description; + } + + public void changeThumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java b/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java index 762c6ead..e6bf88e8 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java @@ -2,9 +2,13 @@ import io.f1.backend.domain.quiz.dto.QuizCreateRequest; import io.f1.backend.domain.quiz.dto.QuizCreateResponse; +import io.f1.backend.domain.quiz.dto.QuizListPageResponse; +import io.f1.backend.domain.quiz.dto.QuizListResponse; import io.f1.backend.domain.quiz.entity.Quiz; import io.f1.backend.domain.user.entity.User; +import org.springframework.data.domain.Page; + public class QuizMapper { // TODO : 이후 파라미터에서 user 삭제하기 @@ -30,4 +34,26 @@ public static QuizCreateResponse quizToQuizCreateResponse(Quiz quiz) { quiz.getThumbnailUrl(), quiz.getCreator().getId()); } + + public static QuizListResponse quizToQuizListResponse(Quiz quiz) { + return new QuizListResponse( + quiz.getId(), + quiz.getTitle(), + quiz.getDescription(), + quiz.getCreator().getNickname(), + quiz.getQuestions().size(), + quiz.getThumbnailUrl()); + } + + public static QuizListPageResponse toQuizListPageResponse(Page quizzes) { + return new QuizListPageResponse( + quizzes.getTotalPages(), + quizzes.getNumber() + 1, + quizzes.getTotalElements(), + quizzes.getContent()); + } + + public static Page pageQuizToPageQuizListResponse(Page quizzes) { + return quizzes.map(QuizMapper::quizToQuizListResponse); + } }