검증게시판 기능 추가

This commit is contained in:
Macbook
2026-05-27 15:41:19 +09:00
parent 7a0af36b9b
commit 63693d47c0
57 changed files with 4480 additions and 14 deletions
@@ -0,0 +1,134 @@
/**
* 검증 이슈 등록·수정 모달
*/
import React, { useEffect, useState } from 'react';
import type { VerificationIssueDto } from '../../utils/verificationBoardApi';
import {
VERIFICATION_ISSUE_STAGES,
type VerificationIssueStage,
} from '../../utils/verificationBoardStages';
import ReproductionPathInput from './ReproductionPathInput';
import VerificationIssueAttachmentsSection from './VerificationIssueAttachmentsSection';
interface Props {
open: boolean;
initial: VerificationIssueDto | null;
saving: boolean;
onSave: (dto: VerificationIssueDto, pendingImages: File[]) => void;
onCancel: () => void;
}
const EMPTY: VerificationIssueDto = {
title: '',
content: '',
reproductionPath: '',
stage: 'REGISTERED',
};
const VerificationIssueFormModal: React.FC<Props> = ({
open, initial, saving, onSave, onCancel,
}) => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [reproductionPath, setReproductionPath] = useState('');
const [stage, setStage] = useState<VerificationIssueStage>('REGISTERED');
const [draftImages, setDraftImages] = useState<File[]>([]);
useEffect(() => {
if (!open) return;
setTitle(initial?.title ?? '');
setContent(initial?.content ?? '');
setReproductionPath(initial?.reproductionPath ?? '');
setStage(initial?.stage ?? 'REGISTERED');
setDraftImages([]);
}, [open, initial]);
if (!open) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = title.trim();
if (!trimmed) {
window.alert('제목을 입력하세요.');
return;
}
const pathTrimmed = reproductionPath.trim();
onSave({
...(initial?.id != null ? { id: initial.id } : {}),
title: trimmed,
content: content.trim(),
reproductionPath: pathTrimmed || undefined,
stage,
}, initial?.id == null ? draftImages : []);
};
return (
<div className="vbd-modal-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onCancel(); }}>
<div className="vbd-modal vbd-modal--wide" role="dialog" aria-modal="true" aria-labelledby="vbd-modal-title">
<div className="vbd-modal-header">
<h3 id="vbd-modal-title">{initial?.id != null ? '검증 이슈 수정' : '검증 이슈 등록'}</h3>
<button type="button" className="vbd-modal-close" onClick={onCancel} aria-label="닫기"></button>
</div>
<form className="vbd-modal-form" onSubmit={handleSubmit}>
<label className="vbd-field">
<span className="vbd-field-label"></span>
<select
value={stage}
onChange={e => setStage(e.target.value as VerificationIssueStage)}
>
{VERIFICATION_ISSUE_STAGES.map(s => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
</label>
<label className="vbd-field">
<span className="vbd-field-label"></span>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
maxLength={300}
autoFocus
placeholder="검증 이슈 제목"
/>
</label>
<label className="vbd-field">
<span className="vbd-field-label"></span>
<ReproductionPathInput
value={reproductionPath}
onChange={setReproductionPath}
disabled={saving}
/>
<span className="vbd-field-hint">
, .
</span>
</label>
<label className="vbd-field vbd-field--grow">
<span className="vbd-field-label"></span>
<textarea
value={content}
onChange={e => setContent(e.target.value)}
rows={10}
placeholder="문제점·재현 방법·기대 결과 등"
/>
</label>
<VerificationIssueAttachmentsSection
issueId={initial?.id ?? null}
draftFiles={draftImages}
onDraftFilesChange={setDraftImages}
disabled={saving}
compact
/>
<div className="vbd-modal-actions">
<button type="button" className="vbd-btn" onClick={onCancel} disabled={saving}></button>
<button type="submit" className="vbd-btn vbd-btn--primary" disabled={saving}>
{saving ? '저장 중…' : '저장'}
</button>
</div>
</form>
</div>
</div>
);
};
export default VerificationIssueFormModal;