diff --git a/frontend/src/app/styles/tokens.css b/frontend/src/app/styles/tokens.css index 447633ee..bb6ddb40 100644 --- a/frontend/src/app/styles/tokens.css +++ b/frontend/src/app/styles/tokens.css @@ -45,7 +45,9 @@ --color-fg-strong: var(--color-sage-800); /* 본문 위 보조 텍스트가 밝은 표면에서 충분히 읽히도록 한 단계씩 진하게 (WCAG AA). */ --color-fg-muted: var(--color-sage-500); - --color-fg-subtle: var(--color-sage-400); + /* sage-400(#6e7f9f)는 white 4.04:1 로 AA(4.5:1) 미달 — 흐린 텍스트 전용으로 한 톤 진하게. + * #586a8e: white 5.43:1 / surface 4.80:1, fg-muted(#4a5a7e)보다 밝아 위계 유지. */ + --color-fg-subtle: #586a8e; --color-fg-disabled: var(--color-sage-200); --color-fg-on-primary: var(--color-white); @@ -68,7 +70,7 @@ --color-warning-50: #f4e8d4; --color-warning-500: #b88840; - --color-warning-700: #8a6529; + --color-warning-700: #7e5a22; --color-warning: var(--color-warning-500); --color-danger-50: #f4e0d8; diff --git a/frontend/src/domain/rag/index.ts b/frontend/src/domain/rag/index.ts index e69de29b..fd7310da 100644 --- a/frontend/src/domain/rag/index.ts +++ b/frontend/src/domain/rag/index.ts @@ -0,0 +1,14 @@ +// 분석 문서(RAG 소스)의 출처 유형 — 이력서/레포/웹/자소서. features 간 공유(라벨 일관성). +export type DocumentSourceType = 'RESUME' | 'REPOSITORY' | 'WEB' | 'COVER_LETTER' + +export const DOCUMENT_SOURCE_LABEL: Record = { + RESUME: '이력서', + REPOSITORY: '레포지토리', + WEB: '웹', + COVER_LETTER: '자소서', +} + +// 임의 문자열(서버 ENUM)도 안전하게 한국어 라벨로. 미매핑이면 원문 반환. +export function documentSourceLabel(source: string): string { + return DOCUMENT_SOURCE_LABEL[source as DocumentSourceType] ?? source +} diff --git a/frontend/src/features/analysis/ui/DocumentList.tsx b/frontend/src/features/analysis/ui/DocumentList.tsx index 18f11317..5baf8030 100644 --- a/frontend/src/features/analysis/ui/DocumentList.tsx +++ b/frontend/src/features/analysis/ui/DocumentList.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { isApiError } from '@/shared/api' import { EmptyState, Modal, StatusBadge, type StatusTone } from '@/shared/ui' +import { DOCUMENT_SOURCE_LABEL as SOURCE_LABEL } from '@/domain/rag' import type { DocumentFilter } from '../api/analysis' import { useDocuments } from '../model/useDocuments' import type { @@ -16,13 +17,6 @@ const STATUS_META: Record = FAILED: { tone: 'danger', label: '분석 실패' }, } -const SOURCE_LABEL: Record = { - RESUME: '이력서', - REPOSITORY: '레포지토리', - WEB: '웹', - COVER_LETTER: '자소서', -} - const TECH_PREVIEW_COUNT = 4 type Props = { diff --git a/frontend/src/features/cover-letter/ui/CoverLetterForm.tsx b/frontend/src/features/cover-letter/ui/CoverLetterForm.tsx index 82a16e24..e8da1147 100644 --- a/frontend/src/features/cover-letter/ui/CoverLetterForm.tsx +++ b/frontend/src/features/cover-letter/ui/CoverLetterForm.tsx @@ -1,13 +1,16 @@ import { useState } from 'react' -import { Spinner } from '@/shared/ui/Spinner' +import { Button } from '@/shared/ui/Button' import { useCreateCoverLetter } from '../model/useCoverLetters' const TITLE_MAX = 200 const ANSWER_MAX = 5000 -type DraftItem = { question: string; answer: string } +type DraftItem = { id: string; question: string; answer: string } -const emptyItem = (): DraftItem => ({ question: '', answer: '' }) +const emptyItem = (): DraftItem => ({ id: crypto.randomUUID(), question: '', answer: '' }) + +const FIELD_FOCUS = + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-primary)] focus-visible:border-primary' // 공채 자소서 문항별 입력 — 질문(문항) + 답변을 여러 개 추가. 텍스트 전용. export function CoverLetterForm() { @@ -15,18 +18,23 @@ export function CoverLetterForm() { const [items, setItems] = useState([emptyItem()]) const create = useCreateCoverLetter() - const updateItem = (idx: number, patch: Partial) => - setItems((prev) => prev.map((it, i) => (i === idx ? { ...it, ...patch } : it))) + const updateItem = (id: string, patch: Partial) => + setItems((prev) => prev.map((it) => (it.id === id ? { ...it, ...patch } : it))) const addItem = () => setItems((prev) => [...prev, emptyItem()]) - const removeItem = (idx: number) => - setItems((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== idx))) + const removeItem = (id: string) => + setItems((prev) => (prev.length <= 1 ? prev : prev.filter((it) => it.id !== id))) // 답변이 하나라도 채워져 있어야 제출 가능. const hasAnswer = items.some((it) => it.answer.trim().length > 0) + // 질문은 적었는데 답변이 빈 문항 — 무음 drop 대신 경고 + 제출 차단. + const isIncomplete = (it: DraftItem) => + it.question.trim().length > 0 && it.answer.trim().length === 0 + const hasIncomplete = items.some(isIncomplete) const submitting = create.isPending + const canSubmit = hasAnswer && !hasIncomplete && !submitting const handleSubmit = () => { - if (!hasAnswer || submitting) return + if (!canSubmit) return const payloadItems = items .filter((it) => it.answer.trim().length > 0) .map((it) => ({ question: it.question.trim(), answer: it.answer.trim() })) @@ -54,19 +62,23 @@ export function CoverLetterForm() { maxLength={TITLE_MAX} placeholder="예: OO기업 2026 상반기 공채 자소서" onChange={(e) => setTitle(e.target.value)} - className="rounded-lg border border-border bg-surface px-3 py-2 text-body text-fg outline-none focus:border-primary" + className={`rounded-lg border border-border bg-surface px-3 py-2 text-body text-fg ${FIELD_FOCUS}`} />
{items.map((item, idx) => ( -
+
문항 {idx + 1} {items.length > 1 ? (