Skip to content

Commit 5501997

Browse files
✨ 퀴즈 조회, 수정, 삭제 API 구현 (#31)
* ✨ 퀴즈 조회, 수정, 삭제 API 구현 * chore: Java 스타일 수정 * ♻️ refactor : StringUtils.isBlank 적용 및 로그 수정 * chore: Java 스타일 수정 * ✨ feat: 퀴즈 목록 조회 최신순으로 정렬 * chore: Java 스타일 수정 * chore: Java 스타일 수정 * ♻️ refactor : default 문자열 상수로 빼기 * ♻️ refactor : StringUtils import문 수정, 메서드명 수정 * chore: Java 스타일 수정 --------- Co-authored-by: github-actions <>
1 parent cf25ffa commit 5501997

File tree

9 files changed

+209
-1
lines changed

9 files changed

+209
-1
lines changed

backend/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies {
4444
testImplementation 'org.springframework.security:spring-security-test'
4545

4646
/* ETC */
47+
implementation 'org.apache.commons:commons-lang3:3.12.0'
4748
annotationProcessor 'org.projectlombok:lombok'
4849
compileOnly 'org.projectlombok:lombok'
4950

backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@
33
import io.f1.backend.domain.quiz.app.QuizService;
44
import io.f1.backend.domain.quiz.dto.QuizCreateRequest;
55
import io.f1.backend.domain.quiz.dto.QuizCreateResponse;
6+
import io.f1.backend.domain.quiz.dto.QuizListPageResponse;
7+
import io.f1.backend.domain.quiz.dto.QuizUpdateRequest;
68

79
import jakarta.validation.Valid;
810

911
import lombok.RequiredArgsConstructor;
1012

13+
import org.springframework.data.domain.PageRequest;
14+
import org.springframework.data.domain.Pageable;
15+
import org.springframework.data.domain.Sort;
1116
import org.springframework.http.HttpStatus;
1217
import org.springframework.http.MediaType;
1318
import org.springframework.http.ResponseEntity;
19+
import org.springframework.web.bind.annotation.DeleteMapping;
20+
import org.springframework.web.bind.annotation.GetMapping;
21+
import org.springframework.web.bind.annotation.PathVariable;
1422
import org.springframework.web.bind.annotation.PostMapping;
23+
import org.springframework.web.bind.annotation.PutMapping;
1524
import org.springframework.web.bind.annotation.RequestMapping;
25+
import org.springframework.web.bind.annotation.RequestParam;
1626
import org.springframework.web.bind.annotation.RequestPart;
1727
import org.springframework.web.bind.annotation.RestController;
1828
import org.springframework.web.multipart.MultipartFile;
@@ -35,4 +45,37 @@ public ResponseEntity<QuizCreateResponse> saveQuiz(
3545

3646
return ResponseEntity.status(HttpStatus.CREATED).body(response);
3747
}
48+
49+
@DeleteMapping("/{quizId}")
50+
public ResponseEntity<Void> deleteQuiz(@PathVariable Long quizId) {
51+
52+
quizService.deleteQuiz(quizId);
53+
return ResponseEntity.noContent().build();
54+
}
55+
56+
@PutMapping("/{quizId}")
57+
public ResponseEntity<Void> updateQuiz(
58+
@PathVariable Long quizId,
59+
@RequestPart(required = false) MultipartFile thumbnailFile,
60+
@RequestPart QuizUpdateRequest request)
61+
throws IOException {
62+
63+
quizService.updateQuiz(quizId, thumbnailFile, request);
64+
65+
return ResponseEntity.noContent().build();
66+
}
67+
68+
@GetMapping
69+
public ResponseEntity<QuizListPageResponse> getQuizzes(
70+
@RequestParam(defaultValue = "1") int page,
71+
@RequestParam(defaultValue = "10") int size,
72+
@RequestParam(required = false) String title,
73+
@RequestParam(required = false) String creator) {
74+
75+
Pageable pageable =
76+
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
77+
QuizListPageResponse quizzes = quizService.getQuizzes(title, creator, pageable);
78+
79+
return ResponseEntity.ok().body(quizzes);
80+
}
3881
}

backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
package io.f1.backend.domain.quiz.app;
22

3+
import static io.f1.backend.domain.quiz.mapper.QuizMapper.pageQuizToPageQuizListResponse;
34
import static io.f1.backend.domain.quiz.mapper.QuizMapper.quizCreateRequestToQuiz;
45
import static io.f1.backend.domain.quiz.mapper.QuizMapper.quizToQuizCreateResponse;
6+
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toQuizListPageResponse;
7+
8+
import static java.nio.file.Files.deleteIfExists;
59

610
import io.f1.backend.domain.question.app.QuestionService;
711
import io.f1.backend.domain.question.dto.QuestionRequest;
812
import io.f1.backend.domain.quiz.dao.QuizRepository;
913
import io.f1.backend.domain.quiz.dto.QuizCreateRequest;
1014
import io.f1.backend.domain.quiz.dto.QuizCreateResponse;
15+
import io.f1.backend.domain.quiz.dto.QuizListPageResponse;
16+
import io.f1.backend.domain.quiz.dto.QuizListResponse;
17+
import io.f1.backend.domain.quiz.dto.QuizUpdateRequest;
1118
import io.f1.backend.domain.quiz.entity.Quiz;
1219
import io.f1.backend.domain.user.dao.UserRepository;
1320
import io.f1.backend.domain.user.entity.User;
1421

1522
import lombok.RequiredArgsConstructor;
23+
import lombok.extern.slf4j.Slf4j;
1624

25+
import org.apache.commons.lang3.StringUtils;
1726
import org.springframework.beans.factory.annotation.Value;
27+
import org.springframework.data.domain.Page;
28+
import org.springframework.data.domain.Pageable;
1829
import org.springframework.stereotype.Service;
1930
import org.springframework.transaction.annotation.Transactional;
2031
import org.springframework.web.multipart.MultipartFile;
@@ -23,8 +34,10 @@
2334
import java.nio.file.Path;
2435
import java.nio.file.Paths;
2536
import java.util.List;
37+
import java.util.NoSuchElementException;
2638
import java.util.UUID;
2739

40+
@Slf4j
2841
@Service
2942
@RequiredArgsConstructor
3043
public class QuizService {
@@ -35,6 +48,8 @@ public class QuizService {
3548
@Value("${file.default-thumbnail-url}")
3649
private String defaultThumbnailPath;
3750

51+
private final String DEFAULT = "default";
52+
3853
// TODO : 시큐리티 구현 이후 삭제해도 되는 의존성 주입
3954
private final UserRepository userRepository;
4055
private final QuestionService questionService;
@@ -92,6 +107,92 @@ private String getExtension(String filename) {
92107
return filename.substring(filename.lastIndexOf(".") + 1);
93108
}
94109

110+
@Transactional
111+
public void deleteQuiz(Long quizId) {
112+
113+
Quiz quiz =
114+
quizRepository
115+
.findById(quizId)
116+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
117+
118+
// TODO : util 메서드에서 사용자 ID 꺼내쓰는 식으로 수정하기
119+
if (1L != quiz.getCreator().getId()) {
120+
throw new RuntimeException("권한이 없습니다.");
121+
}
122+
123+
deleteThumbnailFile(quiz.getThumbnailUrl());
124+
quizRepository.deleteById(quizId);
125+
}
126+
127+
@Transactional
128+
public void updateQuiz(Long quizId, MultipartFile thumbnailFile, QuizUpdateRequest request)
129+
throws IOException {
130+
131+
Quiz quiz =
132+
quizRepository
133+
.findById(quizId)
134+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
135+
136+
if (request.title() != null) {
137+
quiz.changeTitle(request.title());
138+
}
139+
140+
if (request.description() != null) {
141+
quiz.changeDescription(request.description());
142+
}
143+
144+
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
145+
validateImageFile(thumbnailFile);
146+
String newThumbnailPath = convertToThumbnailPath(thumbnailFile);
147+
148+
deleteThumbnailFile(quiz.getThumbnailUrl());
149+
quiz.changeThumbnailUrl(newThumbnailPath);
150+
}
151+
}
152+
153+
private void deleteThumbnailFile(String oldFilename) {
154+
if (oldFilename.contains(DEFAULT)) {
155+
return;
156+
}
157+
158+
// oldFilename : /images/thumbnail/123asd.jpg
159+
// filename : 123asd.jpg
160+
String filename = oldFilename.substring(oldFilename.lastIndexOf("/") + 1);
161+
Path filePath = Paths.get(uploadPath, filename).toAbsolutePath();
162+
163+
try {
164+
boolean deleted = deleteIfExists(filePath);
165+
if (deleted) {
166+
log.info("기존 썸네일 삭제 완료 : {}", filePath);
167+
} else {
168+
log.info("기존 썸네일 존재 X : {}", filePath);
169+
}
170+
} catch (IOException e) {
171+
log.error("기존 썸네일 삭제 중 오류 : {}", filePath);
172+
throw new RuntimeException(e);
173+
}
174+
}
175+
176+
@Transactional(readOnly = true)
177+
public QuizListPageResponse getQuizzes(String title, String creator, Pageable pageable) {
178+
179+
Page<Quiz> quizzes;
180+
181+
// 검색어가 있을 때
182+
if (StringUtils.isBlank(title)) {
183+
quizzes = quizRepository.findQuizzesByTitleContaining(title, pageable);
184+
} else if (StringUtils.isBlank(creator)) {
185+
quizzes = quizRepository.findQuizzesByCreator_NicknameContaining(creator, pageable);
186+
} else { // 검색어가 없을 때 혹은 빈 문자열일 때
187+
quizzes = quizRepository.findAll(pageable);
188+
}
189+
190+
Page<QuizListResponse> quizListResponses = pageQuizToPageQuizListResponse(quizzes);
191+
192+
return toQuizListPageResponse(quizListResponses);
193+
}
194+
195+
@Transactional(readOnly = true)
95196
public Quiz getQuizById(Long quizId) {
96197
return quizRepository
97198
.findById(quizId)

backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
import io.f1.backend.domain.quiz.entity.Quiz;
44

5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
57
import org.springframework.data.jpa.repository.JpaRepository;
68

7-
public interface QuizRepository extends JpaRepository<Quiz, Long> {}
9+
public interface QuizRepository extends JpaRepository<Quiz, Long> {
10+
11+
Page<Quiz> findQuizzesByTitleContaining(String title, Pageable pageable);
12+
13+
Page<Quiz> findQuizzesByCreator_NicknameContaining(String creator, Pageable pageable);
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.f1.backend.domain.quiz.dto;
2+
3+
import java.util.List;
4+
5+
public record QuizListPageResponse(
6+
int totalPages, int currentPage, long totalElements, List<QuizListResponse> quiz) {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.f1.backend.domain.quiz.dto;
2+
3+
public record QuizListResponse(
4+
Long quizId,
5+
String title,
6+
String description,
7+
String creatorNickname,
8+
int numberOfQuestion,
9+
String thumbnailUrl) {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.f1.backend.domain.quiz.dto;
2+
3+
public record QuizUpdateRequest(String title, String description) {}

backend/src/main/java/io/f1/backend/domain/quiz/entity/Quiz.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,16 @@ public Quiz(
6969
public void addQuestion(Question question) {
7070
this.questions.add(question);
7171
}
72+
73+
public void changeTitle(String title) {
74+
this.title = title;
75+
}
76+
77+
public void changeDescription(String description) {
78+
this.description = description;
79+
}
80+
81+
public void changeThumbnailUrl(String thumbnailUrl) {
82+
this.thumbnailUrl = thumbnailUrl;
83+
}
7284
}

backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import io.f1.backend.domain.quiz.dto.QuizCreateRequest;
44
import io.f1.backend.domain.quiz.dto.QuizCreateResponse;
5+
import io.f1.backend.domain.quiz.dto.QuizListPageResponse;
6+
import io.f1.backend.domain.quiz.dto.QuizListResponse;
57
import io.f1.backend.domain.quiz.entity.Quiz;
68
import io.f1.backend.domain.user.entity.User;
79

10+
import org.springframework.data.domain.Page;
11+
812
public class QuizMapper {
913

1014
// TODO : 이후 파라미터에서 user 삭제하기
@@ -30,4 +34,26 @@ public static QuizCreateResponse quizToQuizCreateResponse(Quiz quiz) {
3034
quiz.getThumbnailUrl(),
3135
quiz.getCreator().getId());
3236
}
37+
38+
public static QuizListResponse quizToQuizListResponse(Quiz quiz) {
39+
return new QuizListResponse(
40+
quiz.getId(),
41+
quiz.getTitle(),
42+
quiz.getDescription(),
43+
quiz.getCreator().getNickname(),
44+
quiz.getQuestions().size(),
45+
quiz.getThumbnailUrl());
46+
}
47+
48+
public static QuizListPageResponse toQuizListPageResponse(Page<QuizListResponse> quizzes) {
49+
return new QuizListPageResponse(
50+
quizzes.getTotalPages(),
51+
quizzes.getNumber() + 1,
52+
quizzes.getTotalElements(),
53+
quizzes.getContent());
54+
}
55+
56+
public static Page<QuizListResponse> pageQuizToPageQuizListResponse(Page<Quiz> quizzes) {
57+
return quizzes.map(QuizMapper::quizToQuizListResponse);
58+
}
3359
}

0 commit comments

Comments
 (0)