Skip to content

Commit 26ecfe9

Browse files
✨ feat : 문제 수정, 삭제 API 추가 + TrimmedSize 유효성 검사 어노테이션 정의 (#44)
* ✨ feat : 문제 수정, 삭제 API 추가 + TrimmedSize 유효성 검사 어노테이션 정의 * 🔧 chore : 시큐리티 임시 허용해줬던 거 다시 원래대로 * chore: Java 스타일 수정 * 🔧 chore : 충돌 해결 * chore: Java 스타일 수정 * ♻️ refactor : Quiz 수정시에도 컨트롤러, 서비스 역할 분리 * chore: Java 스타일 수정 --------- Co-authored-by: github-actions <>
1 parent 509026e commit 26ecfe9

File tree

11 files changed

+211
-15
lines changed

11 files changed

+211
-15
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.f1.backend.domain.question.api;
2+
3+
import io.f1.backend.domain.question.app.QuestionService;
4+
import io.f1.backend.domain.question.dto.QuestionUpdateRequest;
5+
6+
import lombok.RequiredArgsConstructor;
7+
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.DeleteMapping;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.PutMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequestMapping("/questions")
18+
@RequiredArgsConstructor
19+
public class QuestionController {
20+
21+
private final QuestionService questionService;
22+
23+
@PutMapping("/{questionId}")
24+
public ResponseEntity<Void> updateQuestion(
25+
@PathVariable Long questionId, @RequestBody QuestionUpdateRequest request) {
26+
27+
if (request.content() != null) {
28+
questionService.updateQuestionContent(questionId, request.content());
29+
}
30+
31+
if (request.content() != null) {
32+
questionService.updateQuestionAnswer(questionId, request.answer());
33+
}
34+
35+
return ResponseEntity.noContent().build();
36+
}
37+
38+
@DeleteMapping("/{questionId}")
39+
public ResponseEntity<Void> deleteQuestion(@PathVariable Long questionId) {
40+
questionService.deleteQuestion(questionId);
41+
42+
return ResponseEntity.noContent().build();
43+
}
44+
}

backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.springframework.stereotype.Service;
1616
import org.springframework.transaction.annotation.Transactional;
1717

18+
import java.util.NoSuchElementException;
19+
1820
@Service
1921
@RequiredArgsConstructor
2022
public class QuestionService {
@@ -33,4 +35,54 @@ public void saveQuestion(Quiz quiz, QuestionRequest request) {
3335
textQuestionRepository.save(textQuestion);
3436
question.addTextQuestion(textQuestion);
3537
}
38+
39+
@Transactional
40+
public void updateQuestionContent(Long questionId, String content) {
41+
42+
validateContent(content);
43+
44+
Question question =
45+
questionRepository
46+
.findById(questionId)
47+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
48+
49+
TextQuestion textQuestion = question.getTextQuestion();
50+
textQuestion.changeContent(content);
51+
}
52+
53+
@Transactional
54+
public void updateQuestionAnswer(Long questionId, String answer) {
55+
56+
validateAnswer(answer);
57+
58+
Question question =
59+
questionRepository
60+
.findById(questionId)
61+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
62+
63+
question.changeAnswer(answer);
64+
}
65+
66+
@Transactional
67+
public void deleteQuestion(Long questionId) {
68+
69+
Question question =
70+
questionRepository
71+
.findById(questionId)
72+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));
73+
74+
questionRepository.delete(question);
75+
}
76+
77+
private void validateAnswer(String answer) {
78+
if (answer.trim().length() < 5 || answer.trim().length() > 30) {
79+
throw new IllegalArgumentException("정답은 1자 이상 30자 이하로 입력해주세요.");
80+
}
81+
}
82+
83+
private void validateContent(String content) {
84+
if (content.trim().length() < 5 || content.trim().length() > 30) {
85+
throw new IllegalArgumentException("문제는 5자 이상 30자 이하로 입력해주세요.");
86+
}
87+
}
3688
}

backend/src/main/java/io/f1/backend/domain/question/dto/QuestionRequest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.f1.backend.domain.question.dto;
22

3+
import io.f1.backend.global.validation.TrimmedSize;
4+
35
import jakarta.validation.constraints.NotBlank;
46

57
import lombok.AccessLevel;
@@ -10,9 +12,11 @@
1012
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1113
public class QuestionRequest {
1214

15+
@TrimmedSize(min = 5, max = 30)
1316
@NotBlank(message = "문제를 입력해주세요.")
1417
private String content;
1518

19+
@TrimmedSize(min = 1, max = 30)
1620
@NotBlank(message = "정답을 입력해주세요.")
1721
private String answer;
1822
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.f1.backend.domain.question.dto;
2+
3+
public record QuestionUpdateRequest(String content, String answer) {}

backend/src/main/java/io/f1/backend/domain/question/entity/Question.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,8 @@ public Question(Quiz quiz, String answer) {
4545
public void addTextQuestion(TextQuestion textQuestion) {
4646
this.textQuestion = textQuestion;
4747
}
48+
49+
public void changeAnswer(String answer) {
50+
this.answer = answer;
51+
}
4852
}

backend/src/main/java/io/f1/backend/domain/question/entity/TextQuestion.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,8 @@ public TextQuestion(Question question, String content) {
3232
this.question = question;
3333
this.content = content;
3434
}
35+
36+
public void changeContent(String content) {
37+
this.content = content;
38+
}
3539
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,17 @@ public ResponseEntity<Void> updateQuiz(
6161
@RequestPart QuizUpdateRequest request)
6262
throws IOException {
6363

64-
quizService.updateQuiz(quizId, thumbnailFile, request);
64+
if (request.title() != null) {
65+
quizService.updateQuizTitle(quizId, request.title());
66+
}
67+
68+
if (request.description() != null) {
69+
quizService.updateQuizDesc(quizId, request.description());
70+
}
71+
72+
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
73+
quizService.updateThumbnail(quizId, thumbnailFile);
74+
}
6575

6676
return ResponseEntity.noContent().build();
6777
}

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

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import io.f1.backend.domain.quiz.dto.QuizListPageResponse;
1313
import io.f1.backend.domain.quiz.dto.QuizListResponse;
1414
import io.f1.backend.domain.quiz.dto.QuizQuestionListResponse;
15-
import io.f1.backend.domain.quiz.dto.QuizUpdateRequest;
1615
import io.f1.backend.domain.quiz.entity.Quiz;
1716
import io.f1.backend.domain.user.dao.UserRepository;
1817
import io.f1.backend.domain.user.entity.User;
@@ -123,28 +122,52 @@ public void deleteQuiz(Long quizId) {
123122
}
124123

125124
@Transactional
126-
public void updateQuiz(Long quizId, MultipartFile thumbnailFile, QuizUpdateRequest request)
127-
throws IOException {
125+
public void updateQuizTitle(Long quizId, String title) {
126+
Quiz quiz =
127+
quizRepository
128+
.findById(quizId)
129+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
130+
131+
validateTitle(title);
132+
quiz.changeTitle(title);
133+
}
134+
135+
@Transactional
136+
public void updateQuizDesc(Long quizId, String description) {
128137

129138
Quiz quiz =
130139
quizRepository
131140
.findById(quizId)
132141
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
133142

134-
if (request.title() != null) {
135-
quiz.changeTitle(request.title());
136-
}
143+
validateDesc(description);
144+
quiz.changeDescription(description);
145+
}
137146

138-
if (request.description() != null) {
139-
quiz.changeDescription(request.description());
140-
}
147+
@Transactional
148+
public void updateThumbnail(Long quizId, MultipartFile thumbnailFile) throws IOException {
141149

142-
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
143-
validateImageFile(thumbnailFile);
144-
String newThumbnailPath = convertToThumbnailPath(thumbnailFile);
150+
Quiz quiz =
151+
quizRepository
152+
.findById(quizId)
153+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));
154+
155+
validateImageFile(thumbnailFile);
156+
String newThumbnailPath = convertToThumbnailPath(thumbnailFile);
157+
158+
deleteThumbnailFile(quiz.getThumbnailUrl());
159+
quiz.changeThumbnailUrl(newThumbnailPath);
160+
}
161+
162+
private void validateDesc(String desc) {
163+
if (desc.trim().length() < 10 || desc.trim().length() > 50) {
164+
throw new IllegalArgumentException("설명은 10자 이상 50자 이하로 입력해주세요.");
165+
}
166+
}
145167

146-
deleteThumbnailFile(quiz.getThumbnailUrl());
147-
quiz.changeThumbnailUrl(newThumbnailPath);
168+
private void validateTitle(String title) {
169+
if (title.trim().length() < 2 || title.trim().length() > 30) {
170+
throw new IllegalArgumentException("제목은 2자 이상 30자 이하로 입력해주세요.");
148171
}
149172
}
150173

backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizCreateRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.f1.backend.domain.question.dto.QuestionRequest;
44
import io.f1.backend.domain.quiz.entity.QuizType;
5+
import io.f1.backend.global.validation.TrimmedSize;
56

67
import jakarta.validation.constraints.NotBlank;
78
import jakarta.validation.constraints.NotNull;
@@ -17,12 +18,14 @@
1718
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1819
public class QuizCreateRequest {
1920

21+
@TrimmedSize(min = 2, max = 30)
2022
@NotBlank(message = "퀴즈 제목을 설정해주세요.")
2123
private String title;
2224

2325
@NotNull(message = "퀴즈 종류를 선택해주세요.")
2426
private QuizType quizType;
2527

28+
@TrimmedSize(min = 10, max = 50)
2629
@NotBlank(message = "퀴즈 설명을 적어주세요.")
2730
private String description;
2831

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.f1.backend.global.validation;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.Payload;
5+
6+
import java.lang.annotation.*;
7+
8+
@Documented
9+
@Constraint(validatedBy = TrimmedSizeValidator.class)
10+
@Target({ElementType.FIELD, ElementType.PARAMETER})
11+
@Retention(RetentionPolicy.RUNTIME)
12+
public @interface TrimmedSize {
13+
14+
String message() default "공백 제외 길이가 {min}자 이상 {min}자 이하여야 합니다.";
15+
16+
int min() default 0;
17+
18+
int max() default 50;
19+
20+
Class<?>[] groups() default {};
21+
22+
Class<? extends Payload>[] payload() default {};
23+
}

0 commit comments

Comments
 (0)