Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.f1.backend.domain.question.api;

import io.f1.backend.domain.question.app.QuestionService;
import io.f1.backend.domain.question.dto.QuestionUpdateRequest;

import lombok.RequiredArgsConstructor;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/questions")
@RequiredArgsConstructor
public class QuestionController {

private final QuestionService questionService;

@PutMapping("/{questionId}")
public ResponseEntity<Void> updateQuestion(
@PathVariable Long questionId, @RequestBody QuestionUpdateRequest request) {

if (request.content() != null) {
questionService.updateQuestionContent(questionId, request.content());
}

if (request.content() != null) {
questionService.updateQuestionAnswer(questionId, request.answer());
}

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

@DeleteMapping("/{questionId}")
public ResponseEntity<Void> deleteQuestion(@PathVariable Long questionId) {
questionService.deleteQuestion(questionId);

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class QuestionService {
Expand All @@ -33,4 +35,54 @@ public void saveQuestion(Quiz quiz, QuestionRequest request) {
textQuestionRepository.save(textQuestion);
question.addTextQuestion(textQuestion);
}

@Transactional
public void updateQuestionContent(Long questionId, String content) {

validateContent(content);

Question question =
questionRepository
.findById(questionId)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));

TextQuestion textQuestion = question.getTextQuestion();
textQuestion.changeContent(content);
}

@Transactional
public void updateQuestionAnswer(Long questionId, String answer) {

validateAnswer(answer);

Question question =
questionRepository
.findById(questionId)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));

question.changeAnswer(answer);
}

@Transactional
public void deleteQuestion(Long questionId) {

Question question =
questionRepository
.findById(questionId)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 문제입니다."));

questionRepository.delete(question);
}

private void validateAnswer(String answer) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

validate 분리되어있는거 편--안하네요 👍

if (answer.trim().length() < 5 || answer.trim().length() > 30) {
throw new IllegalArgumentException("정답은 1자 이상 30자 이하로 입력해주세요.");
}
}

private void validateContent(String content) {
if (content.trim().length() < 5 || content.trim().length() > 30) {
throw new IllegalArgumentException("문제는 5자 이상 30자 이하로 입력해주세요.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.f1.backend.domain.question.dto;

import io.f1.backend.global.validation.TrimmedSize;

import jakarta.validation.constraints.NotBlank;

import lombok.AccessLevel;
Expand All @@ -10,9 +12,11 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class QuestionRequest {

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

@TrimmedSize(min = 1, max = 30)
@NotBlank(message = "정답을 입력해주세요.")
private String answer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.question.dto;

public record QuestionUpdateRequest(String content, String answer) {}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public Question(Quiz quiz, String answer) {
public void addTextQuestion(TextQuestion textQuestion) {
this.textQuestion = textQuestion;
}

public void changeAnswer(String answer) {
this.answer = answer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public TextQuestion(Question question, String content) {
this.question = question;
this.content = content;
}

public void changeContent(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ public void updateQuiz(Long quizId, MultipartFile thumbnailFile, QuizUpdateReque
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다."));

if (request.title() != null) {
validateTitle(request.title());
quiz.changeTitle(request.title());
}

if (request.description() != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[L4-변경제안]

이 부분도 위의 리뷰와 같은 내용입니다 !

validateDesc(request.description());
quiz.changeDescription(request.description());
}

Expand All @@ -148,6 +150,18 @@ public void updateQuiz(Long quizId, MultipartFile thumbnailFile, QuizUpdateReque
}
}

private void validateDesc(String desc) {
if (desc.trim().length() < 10 || desc.trim().length() > 50) {
throw new IllegalArgumentException("설명은 10자 이상 50자 이하로 입력해주세요.");
}
}

private void validateTitle(String title) {
if (title.trim().length() < 2 || title.trim().length() > 30) {
throw new IllegalArgumentException("제목은 2자 이상 30자 이하로 입력해주세요.");
}
}

private void deleteThumbnailFile(String oldFilename) {
if (oldFilename.contains(DEFAULT)) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.f1.backend.domain.question.dto.QuestionRequest;
import io.f1.backend.domain.quiz.entity.QuizType;
import io.f1.backend.global.validation.TrimmedSize;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand All @@ -17,12 +18,14 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class QuizCreateRequest {

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

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.f1.backend.global.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

@Documented
Copy link
Collaborator

@LimKangHyun LimKangHyun Jul 16, 2025

Choose a reason for hiding this comment

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

[L5-참고의견]
@TrimmedSized 어노테이션을 닉네임에도 적용하면 좋을 것 같네요!
배우고 갑니다!

@Constraint(validatedBy = TrimmedSizeValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrimmedSize {

String message() default "공백 제외 길이가 {min}자 이상 {min}자 이하여야 합니다.";

int min() default 0;

int max() default 50;

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.f1.backend.global.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class TrimmedSizeValidator implements ConstraintValidator<TrimmedSize, String> {

private int min;
private int max;

@Override
public void initialize(TrimmedSize constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

[L5-참고의견]
value가 null일 경우 true return이 정상 흐름인지 확인해 주시면 감사하겠습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 맞습니다 ! null 검사는 @NotNull 이 따로 있기 때문에 @TrimmedSize 어노테이션은 공백 제외 글자수를 확인해주는 용도로 사용하기 위해 분리해두었습니다!

현재 유효성 검사를 할 때, @TrimmedSize@NotBlank를 해주고 있는데,, 유효성 검사를 처리하는 순서가 @NotBlank -> @TrimmedSize와 같은 커스텀 어노테이션 이렇게 된다고 합니다!

그래서 완전한 빈 문자열이나 null이 들어왔을 땐 @NotBlank에 걸려서 -> 제목을 설정해주세요.
그 이후 문자열이 있긴 있는데, 글자 수가 충족이 되지 않을 땐 @TrimmedSize -> 몇 자 이상 ~ 으로 설정해주세요.

이렇게 역할이 분리되는 것이 맞다고 생각하여 따로 null 처리는 하지 않았습니다 !

Copy link
Collaborator

Choose a reason for hiding this comment

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

TrimmedSize라는 어노테이션이 개발자의 실수 등의 이유로 단독으로 사용되는 경우를 생각했었는데,
말씀하신 것처럼 Null 처리에 대한 역할은 NotNull NotBlank에 위임하고 TrimmedSize는 글자 수 검증 용도로만 사용하는 것이 맞는 것 같습니다.
감사합니다!


String trimmed = value.trim();
int length = trimmed.length();

return length >= min && length <= max;
}
}