-
Notifications
You must be signed in to change notification settings - Fork 1
feat: increase question content length/#701 #702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.NotBlank; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.NotNull; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.Size; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public class CreateQuestionV2Dto { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -14,6 +15,7 @@ public record Request( | |||||||||||||||||||||||
| Long chapterId, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @NotBlank(message = "content는 필수 입력값입니다.") | ||||||||||||||||||||||||
| @Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.") | ||||||||||||||||||||||||
| String content, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| String explanation, | ||||||||||||||||||||||||
|
Comment on lines
17
to
21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DB와 엔티티의 변경 사항에 맞춰 DTO에서도 explanation 필드에 대한 @SiZe 검증을 추가하는 것이 안전합니다. 검증이 없으면 DB 저장 시점에 예외가 발생할 수 있습니다.
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,12 +5,14 @@ | |||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.NotBlank; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.NotNull; | ||||||||||||||||||||||||
| import jakarta.validation.constraints.Size; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public class UpdateQuestionV2Dto { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @Schema(name = "UpdateQuestionV2DtoRequest") | ||||||||||||||||||||||||
| public record Request( | ||||||||||||||||||||||||
| @NotBlank(message = "content는 필수 입력값입니다.") | ||||||||||||||||||||||||
| @Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.") | ||||||||||||||||||||||||
| String content, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| String explanation, | ||||||||||||||||||||||||
|
Comment on lines
14
to
18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정 DTO에서도 explanation 필드에 대한 최대 길이 검증을 추가하여 런타임 에러를 방지하는 것이 좋습니다.
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ALTER TABLE questions_v2 | ||
| ALTER COLUMN content TYPE VARCHAR(2000); | ||
|
Comment on lines
+1
to
+2
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package com.process.clash.adapter.web.roadmap.v2.question.controller; | ||
|
|
||
| import static org.mockito.Mockito.verifyNoInteractions; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.process.clash.adapter.web.common.GlobalExceptionHandler; | ||
| import com.process.clash.adapter.web.security.AuthenticatedActor; | ||
| import com.process.clash.application.common.actor.Actor; | ||
| import com.process.clash.application.roadmap.v2.question.port.in.CreateQuestionV2UseCase; | ||
| import com.process.clash.application.roadmap.v2.question.port.in.DeleteQuestionV2UseCase; | ||
| import com.process.clash.application.roadmap.v2.question.port.in.UpdateQuestionV2UseCase; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.extension.ExtendWith; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.jupiter.MockitoExtension; | ||
| import org.springframework.core.MethodParameter; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.test.web.servlet.MockMvc; | ||
| import org.springframework.test.web.servlet.setup.MockMvcBuilders; | ||
| import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; | ||
| import org.springframework.web.bind.support.WebDataBinderFactory; | ||
| import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
| import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
|
||
| @ExtendWith(MockitoExtension.class) | ||
| class AdminQuestionV2ControllerTest { | ||
|
|
||
| @Mock | ||
| private CreateQuestionV2UseCase createQuestionV2UseCase; | ||
|
|
||
| @Mock | ||
| private UpdateQuestionV2UseCase updateQuestionV2UseCase; | ||
|
|
||
| @Mock | ||
| private DeleteQuestionV2UseCase deleteQuestionV2UseCase; | ||
|
|
||
| private final ObjectMapper objectMapper = new ObjectMapper(); | ||
| private MockMvc mockMvc; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| AdminQuestionV2Controller controller = new AdminQuestionV2Controller( | ||
| createQuestionV2UseCase, | ||
| updateQuestionV2UseCase, | ||
| deleteQuestionV2UseCase | ||
| ); | ||
|
|
||
| LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); | ||
| validator.afterPropertiesSet(); | ||
|
|
||
| mockMvc = MockMvcBuilders.standaloneSetup(controller) | ||
| .setControllerAdvice(new GlobalExceptionHandler()) | ||
| .setCustomArgumentResolvers(authenticatedActorResolver()) | ||
| .setValidator(validator) | ||
| .build(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("POST /api/v2/admin/questions 는 content가 2000자를 초과하면 400을 반환한다") | ||
| void createQuestion_whenContentExceeds2000Characters_returnsBadRequest() throws Exception { | ||
| mockMvc.perform(post("/api/v2/admin/questions") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(Map.of( | ||
| "chapterId", 1L, | ||
| "content", "a".repeat(2001), | ||
| "explanation", "해설", | ||
| "orderIndex", 0, | ||
| "difficulty", 1 | ||
| )))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.success").value(false)) | ||
| .andExpect(jsonPath("$.error.code").value("INVALID_ARGUMENT")) | ||
| .andExpect(jsonPath("$.error.details.content").value("content는 최대 2000자까지 입력할 수 있습니다.")); | ||
|
|
||
| verifyNoInteractions(createQuestionV2UseCase); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("PATCH /api/v2/admin/questions/{questionId} 는 content가 2000자를 초과하면 400을 반환한다") | ||
| void updateQuestion_whenContentExceeds2000Characters_returnsBadRequest() throws Exception { | ||
| mockMvc.perform(patch("/api/v2/admin/questions/{questionId}", 1L) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(Map.of( | ||
| "content", "a".repeat(2001), | ||
| "explanation", "해설", | ||
| "orderIndex", 0, | ||
| "difficulty", 1 | ||
| )))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.success").value(false)) | ||
| .andExpect(jsonPath("$.error.code").value("INVALID_ARGUMENT")) | ||
| .andExpect(jsonPath("$.error.details.content").value("content는 최대 2000자까지 입력할 수 있습니다.")); | ||
|
|
||
| verifyNoInteractions(updateQuestionV2UseCase); | ||
| } | ||
|
|
||
| private HandlerMethodArgumentResolver authenticatedActorResolver() { | ||
| return new HandlerMethodArgumentResolver() { | ||
| @Override | ||
| public boolean supportsParameter(MethodParameter parameter) { | ||
| return parameter.hasParameterAnnotation(AuthenticatedActor.class); | ||
| } | ||
|
|
||
| @Override | ||
| public Object resolveArgument( | ||
| MethodParameter parameter, | ||
| ModelAndViewContainer mavContainer, | ||
| org.springframework.web.context.request.NativeWebRequest webRequest, | ||
| WebDataBinderFactory binderFactory | ||
| ) { | ||
| return new Actor(1L); | ||
| } | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
질문 내용(content)의 최대 길이를 2000자로 늘린 것과 대조적으로, 해설(explanation) 필드는 별도의 길이 설정이 없어 기본값인 255자로 제한될 가능성이 큽니다. 질문 내용이 길어지면 그에 따른 해설도 길어질 확률이 높으므로, explanation 필드의 길이도 함께 늘리는 것을 권장합니다.