Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
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.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;
Expand All @@ -35,4 +44,36 @@ public ResponseEntity<QuizCreateResponse> saveQuiz(

return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@DeleteMapping("/{quizId}")
public ResponseEntity<Void> deleteQuiz(@PathVariable Long quizId) {

quizService.deleteQuiz(quizId);
return ResponseEntity.noContent().build();
}

@PutMapping("/{quizId}")
public ResponseEntity<Void> 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<QuizListPageResponse> 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);
QuizListPageResponse quizzes = quizService.getQuizzes(title, creator, pageable);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 지금 퀴즈리스트에 대한 정렬이 적용이 된건가요?.?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

퀴즈 리스트에 대한 정렬은 적용되지 않았습니다 !
따로 정한 기준은 없었던 것 같은데,,, 제가 기억이 안 나는 걸까요..? 혹시 어떤 정렬을 해야 할까요?

Copy link
Collaborator

@sehee123 sehee123 Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L4-변경제안]
최신순, 인기순 정렬을 변경하는 기능은 정의하지 않았지만, 디폴트 정렬은 있어야 사용자에게 일관적인 리스트를 보여줄 수 있다고 생각합니다!(명시하지않아서 디폴트 정렬로 나오는 것 같지만, 안정적으로 명시하는게 좋다는 의견입니다) 현재 1번부터 나오는 asc라면 desc로 정렬하면(최신순) 어떨까하는 의견입니다. 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오..! 그렇군요. 페이징을 처음해봐서 정렬까지 같이 처리해주는 기능이 있는지 몰랐는데 디폴트로 정렬기준이 있다면 아주 유용하고 좋을 것 같습니다. 의견 반영해서 커밋 다시 올렸습니다. 좋은 의견 감사합니다 !!


return ResponseEntity.ok().body(quizzes);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
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 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;
Expand All @@ -23,6 +32,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;

@Service
Expand Down Expand Up @@ -91,4 +101,88 @@ private String convertToThumbnailPath(MultipartFile thumbnailFile) throws IOExce
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("존재하지 않는 퀴즈입니다."));

if (1L != quiz.getCreator().getId()) {
throw new RuntimeException("권한이 없습니다.");
}

deleteOldThumbnailFileIfNeeded(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);

deleteOldThumbnailFileIfNeeded(quiz.getThumbnailUrl());
quiz.changeThumbnailUrl(newThumbnailPath);
}
}

private void deleteOldThumbnailFileIfNeeded(String oldFilename) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L5-참고의견]
메서드 명을 deleteFile로 간단하게 정의해도 괜찮을 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 그렇군요 ! 흠 그래도 일반 파일을 지우는 메서드가 아니라 필요없는 썸네일 파일을 지우는 거라.. deleteOldThumbnailFile이나 deleteThumbnailFile로 가보겠습니다 !! 좋은 의견 감사합니다 :)

if (oldFilename.contains("default")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L5-참고의견]
"default"를 상수로 만들어 사용하면 안전할 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다 ! 이 부분도 수정하겠습니다 !

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) {
System.out.println("기존 썸네일 삭제 완료 : " + filePath);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L4-변경제안]
출력문을 log로 대체하는 것이 좋아보입니다 !
[참고 링크] https://hudi.blog/do-not-use-system-out-println-for-logging/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 @Slf4j 라는 추가적인 어노테이션을 달지 않아도 System.out.println로 로그를 찍어볼 수 있는 게 아닌가? 하고 생각했었는데,,, 생각해보니 제공되는 정보의 양이 차이가 많이나는군요 !!

좋은 인사이트 감사합니다. 수정하겠습니다 !

} else {
System.out.println("기존 썸네일 존재 X : " + filePath);
}
} catch (IOException e) {
System.err.println("기존 썸네일 삭제 중 오류 : " + filePath);
throw new RuntimeException(e);
}
}

@Transactional(readOnly = true)
public QuizListPageResponse getQuizzes(String title, String creator, Pageable pageable) {

Page<Quiz> quizzes;

// 검색어가 있을 때
if (title != null && !title.isBlank()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L4-변경제안]
string 조건 비교 시 Apache Commons에서 제공하는 유틸 메서드를 사용하면 조건문을 간단하게 표현할 수 있습니다 !
StringUtils.isBlack() 하나로 처리 가능합니다 !
[참고링크] https://hyeri0903.tistory.com/235

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우 저도 하나 얻어갑니다.. 💨

[L4-변경제안] string 조건 비교 시 Apache Commons에서 제공하는 유틸 메서드를 사용하면 조건문을 간단하게 표현할 수 있습니다 ! StringUtils.isBlack() 하나로 처리 가능합니다 ! [참고링크] https://hyeri0903.tistory.com/235

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 .. ! 저도 저 코드가 좀 안 예뻐서 마음에 안 들었는데 해결방법이 있었군요.. 굉장합니다.. 굉장해..

감사합니다 ! 이 부분도 수정하도록 하겠습니다.

quizzes = quizRepository.findQuizzesByTitleContaining(title, pageable);
} else if (creator != null && !creator.isBlank()) {
quizzes = quizRepository.findQuizzesByCreator_NicknameContaining(creator, pageable);
} else { // 검색어가 없을 때 혹은 빈 문자열일 때
quizzes = quizRepository.findAll(pageable);
}

Page<QuizListResponse> quizListResponses = pageQuizToPageQuizListResponse(quizzes);

return toQuizListPageResponse(quizListResponses);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Quiz, Long> {}
public interface QuizRepository extends JpaRepository<Quiz, Long> {

Page<Quiz> findQuizzesByTitleContaining(String title, Pageable pageable);

Page<Quiz> findQuizzesByCreator_NicknameContaining(String creator, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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<QuizListResponse> quiz) {}
Original file line number Diff line number Diff line change
@@ -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) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.quiz.dto;

public record QuizUpdateRequest(String title, String description) {}
12 changes: 12 additions & 0 deletions backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 삭제하기
Expand All @@ -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<QuizListResponse> quizzes) {
return new QuizListPageResponse(
quizzes.getTotalPages(),
quizzes.getNumber() + 1,
quizzes.getTotalElements(),
quizzes.getContent());
}

public static Page<QuizListResponse> pageQuizToPageQuizListResponse(Page<Quiz> quizzes) {
return quizzes.map(QuizMapper::quizToQuizListResponse);
}
}