Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -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;
Comment on lines +35 to 38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

질문 내용(content)의 최대 길이를 2000자로 늘린 것과 대조적으로, 해설(explanation) 필드는 별도의 길이 설정이 없어 기본값인 255자로 제한될 가능성이 큽니다. 질문 내용이 길어지면 그에 따른 해설도 길어질 확률이 높으므로, explanation 필드의 길이도 함께 늘리는 것을 권장합니다.

Suggested change
@Column(nullable = false, length = 2000)
private String content;
private String explanation;
@Column(nullable = false, length = 2000)
private String content;
@Column(length = 2000)
private String explanation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "자바는 객체지향 언어입니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@Schema(description = "문제 수정 요청")
public class UpdateQuestionV2RequestDocument {

@Schema(description = "문제 내용", example = "자바의 주요 특징은?")
@Schema(description = "문제 내용", example = "자바의 주요 특징은?", maxLength = 2000)
public String content;

@Schema(description = "해설", example = "자바는 객체지향, 플랫폼 독립적, 멀티스레드를 지원합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

DB와 엔티티의 변경 사항에 맞춰 DTO에서도 explanation 필드에 대한 @SiZe 검증을 추가하는 것이 안전합니다. 검증이 없으면 DB 저장 시점에 예외가 발생할 수 있습니다.

Suggested change
@NotBlank(message = "content는 필수 입력값입니다.")
@Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.")
String content,
String explanation,
@NotBlank(message = "content는 필수 입력값입니다.")
@Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.")
String content,
@Size(max = 2000, message = "explanation은 최대 2000자까지 입력할 수 있습니다.")
String explanation,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

수정 DTO에서도 explanation 필드에 대한 최대 길이 검증을 추가하여 런타임 에러를 방지하는 것이 좋습니다.

Suggested change
@NotBlank(message = "content는 필수 입력값입니다.")
@Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.")
String content,
String explanation,
@NotBlank(message = "content는 필수 입력값입니다.")
@Size(max = 2000, message = "content는 최대 2000자까지 입력할 수 있습니다.")
String content,
@Size(max = 2000, message = "explanation은 최대 2000자까지 입력할 수 있습니다.")
String explanation,

Expand Down
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

content 컬럼의 길이를 늘릴 때 explanation 컬럼의 길이도 함께 늘리는 것을 권장합니다. 질문 내용이 길어지면 해설 또한 기본 VARCHAR 길이(255)를 초과할 가능성이 높기 때문입니다.

ALTER TABLE questions_v2
    ALTER COLUMN content TYPE VARCHAR(2000),
    ALTER COLUMN explanation TYPE VARCHAR(2000);

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);
}
};
}
}
Loading