fix: 업무 내용 필수 검증 추가 + 제목 에러 표기 정리

- 업무 내용(*)이 표기상 필수인데 검증이 없던 문제 → 필수로 강제
  · 프론트: 인라인 검증(에디터 빨강 테두리 + '업무 내용을 입력해 주세요.'),
    입력 시 즉시 해제
  · 백엔드: CreateTaskDto.content 를 @ArrayNotEmpty 필수로(데이터 계약 일치)
- 제목 에러 표기 정리: 1.5rem 큰 placeholder 가 빨갛게 변하며 아래 메시지와
  이중으로 강조되던 것을 제거 → 작은 인라인 메시지만 표시(차분하게)

검증: 백엔드 build/lint 0·8 tests, 프론트 type-check/lint/build 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 11:36:58 +09:00
parent 9f66c6d0ec
commit 612d6c7de8
2 changed files with 26 additions and 11 deletions
@@ -37,17 +37,16 @@ export class CreateTaskDto {
title!: string;
@ApiProperty({
description: '본문 문단 배열(선택)',
description: '본문 문단 배열(필수)',
type: [String],
required: false,
example: ['9월 신제품 캠페인 메인 배너를 제작합니다.'],
})
@IsOptional()
@IsArray()
@ArrayNotEmpty({ message: '업무 내용을 입력해 주세요.' })
@ArrayMaxSize(200, { message: '본문 문단이 너무 많습니다.' })
@IsString({ each: true })
@MaxLength(5000, { each: true })
content?: string[];
content!: string[];
@ApiProperty({
description: '담당자 사용자 ID 배열(프로젝트 멤버만 가능)',
+23 -7
View File
@@ -257,12 +257,16 @@ async function uploadPendingFiles(tid: number): Promise<number> {
// --- 제출 ---
const submitting = ref(false)
// 필드별 인라인 검증 — 제목·담당자 필수. 입력하면 즉시 해제.
// 필드별 인라인 검증 — 제목·내용·담당자 필수. 입력하면 즉시 해제.
const titleError = ref(false)
const contentError = ref(false)
const assigneeError = ref(false)
watch(title, (v) => {
if (v.trim().length > 0) titleError.value = false
})
watch(contentText, (v) => {
if (v.trim().length > 0) contentError.value = false
})
watch(
assignees,
(v) => {
@@ -272,8 +276,9 @@ watch(
)
function validateForm(): boolean {
titleError.value = title.value.trim().length === 0
contentError.value = contentText.value.trim().length === 0
assigneeError.value = assignees.value.length === 0
return !titleError.value && !assigneeError.value
return !titleError.value && !contentError.value && !assigneeError.value
}
async function submitTask() {
@@ -417,7 +422,6 @@ function goBack() {
<input
v-model="title"
class="title-input"
:class="{ 'has-error': titleError }"
placeholder="업무 제목을 입력하세요"
>
<div
@@ -433,7 +437,10 @@ function goBack() {
<div class="field-label">
<span class="req">*</span> 업무 내용
</div>
<div class="editor">
<div
class="editor"
:class="{ 'has-error': contentError }"
>
<div class="editor-toolbar">
<button
class="tb"
@@ -520,6 +527,12 @@ function goBack() {
placeholder="업무 배경과 요구사항을 작성하세요. (줄바꿈은 문단으로 저장됩니다)"
/>
</div>
<div
v-if="contentError"
class="field-error"
>
업무 내용을 입력해 주세요.
</div>
</div>
<!-- 체크리스트 -->
@@ -981,9 +994,6 @@ function goBack() {
.title-input::placeholder {
color: #c4c8cf;
}
.title-input.has-error::placeholder {
color: var(--red);
}
.field-error {
font-size: 0.75rem;
color: var(--red);
@@ -1023,6 +1033,12 @@ function goBack() {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
.editor.has-error {
border-color: var(--red);
}
.editor.has-error:focus-within {
box-shadow: 0 0 0 3px var(--red-weak);
}
.editor-toolbar {
display: flex;
align-items: center;