From d461e7a13c3a0e70f1085f94dfe0118dc3a2ae3f Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 24 Apr 2026 23:04:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20mission=EC=9D=98=20content=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EB=A5=BC=202000=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/roadmap/v2/question/QuestionV2JpaEntity.java | 2 +- .../question/docs/request/CreateQuestionV2RequestDocument.java | 2 +- .../question/docs/request/UpdateQuestionV2RequestDocument.java | 2 +- .../web/roadmap/v2/question/dto/CreateQuestionV2Dto.java | 2 ++ .../web/roadmap/v2/question/dto/UpdateQuestionV2Dto.java | 2 ++ .../db/migration/V39__increase_question_v2_content_length.sql | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/migration/V39__increase_question_v2_content_length.sql diff --git a/src/main/java/com/process/clash/adapter/persistence/roadmap/v2/question/QuestionV2JpaEntity.java b/src/main/java/com/process/clash/adapter/persistence/roadmap/v2/question/QuestionV2JpaEntity.java index 189f9d8f3..182b4eccf 100644 --- a/src/main/java/com/process/clash/adapter/persistence/roadmap/v2/question/QuestionV2JpaEntity.java +++ b/src/main/java/com/process/clash/adapter/persistence/roadmap/v2/question/QuestionV2JpaEntity.java @@ -32,7 +32,7 @@ public class QuestionV2JpaEntity { @JoinColumn(name = "fk_chapter_id", nullable = false) private ChapterV2JpaEntity chapter; - @Column(nullable = false, length = 1000) + @Column(nullable = false, length = 2000) private String content; private String explanation; diff --git a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/CreateQuestionV2RequestDocument.java b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/CreateQuestionV2RequestDocument.java index 3ac6314ef..9622ac34b 100644 --- a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/CreateQuestionV2RequestDocument.java +++ b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/CreateQuestionV2RequestDocument.java @@ -8,7 +8,7 @@ public class CreateQuestionV2RequestDocument { @Schema(description = "챕터 ID", example = "1") public Long chapterId; - @Schema(description = "문제 내용", example = "자바의 특징이 아닌 것은?") + @Schema(description = "문제 내용", example = "자바의 특징이 아닌 것은?", maxLength = 2000) public String content; @Schema(description = "해설", example = "자바는 객체지향 언어입니다.") diff --git a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/UpdateQuestionV2RequestDocument.java b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/UpdateQuestionV2RequestDocument.java index 6ddcb1a92..5d6549cf9 100644 --- a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/UpdateQuestionV2RequestDocument.java +++ b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/docs/request/UpdateQuestionV2RequestDocument.java @@ -5,7 +5,7 @@ @Schema(description = "문제 수정 요청") public class UpdateQuestionV2RequestDocument { - @Schema(description = "문제 내용", example = "자바의 주요 특징은?") + @Schema(description = "문제 내용", example = "자바의 주요 특징은?", maxLength = 2000) public String content; @Schema(description = "해설", example = "자바는 객체지향, 플랫폼 독립적, 멀티스레드를 지원합니다.") diff --git a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/CreateQuestionV2Dto.java b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/CreateQuestionV2Dto.java index 53615495e..9ba491115 100644 --- a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/CreateQuestionV2Dto.java +++ b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/CreateQuestionV2Dto.java @@ -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, diff --git a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/UpdateQuestionV2Dto.java b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/UpdateQuestionV2Dto.java index 1b6c67cc8..f0ed8f53b 100644 --- a/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/UpdateQuestionV2Dto.java +++ b/src/main/java/com/process/clash/adapter/web/roadmap/v2/question/dto/UpdateQuestionV2Dto.java @@ -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, diff --git a/src/main/resources/db/migration/V39__increase_question_v2_content_length.sql b/src/main/resources/db/migration/V39__increase_question_v2_content_length.sql new file mode 100644 index 000000000..94a4e36c6 --- /dev/null +++ b/src/main/resources/db/migration/V39__increase_question_v2_content_length.sql @@ -0,0 +1,2 @@ +ALTER TABLE questions_v2 + ALTER COLUMN content TYPE VARCHAR(2000); \ No newline at end of file From 98577561fcd0caf35eee42b2746ee963cc2cbbdd Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 24 Apr 2026 23:04:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20mission=EC=9D=98=20content=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminQuestionV2ControllerTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/test/java/com/process/clash/adapter/web/roadmap/v2/question/controller/AdminQuestionV2ControllerTest.java diff --git a/src/test/java/com/process/clash/adapter/web/roadmap/v2/question/controller/AdminQuestionV2ControllerTest.java b/src/test/java/com/process/clash/adapter/web/roadmap/v2/question/controller/AdminQuestionV2ControllerTest.java new file mode 100644 index 000000000..c00ac2fe9 --- /dev/null +++ b/src/test/java/com/process/clash/adapter/web/roadmap/v2/question/controller/AdminQuestionV2ControllerTest.java @@ -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); + } + }; + } +}