검증게시판 기능 추가

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,118 @@
/**
* 검증게시판 — 좌측 이슈 목록 (단계 필터 + 검색)
*/
import React, { useMemo } from 'react';
import type { VerificationIssueDto } from '../../utils/verificationBoardApi';
import {
VERIFICATION_ISSUE_STAGES,
stageColor,
stageLabel,
type VerificationIssueStage,
} from '../../utils/verificationBoardStages';
import VerificationStageIcon from './VerificationStageIcon';
interface Props {
issues: VerificationIssueDto[];
selectedId: number | null;
stageFilter: VerificationIssueStage | '';
searchQuery: string;
onStageFilterChange: (stage: VerificationIssueStage | '') => void;
onSearchChange: (q: string) => void;
onSelect: (issue: VerificationIssueDto) => void;
}
function formatDate(iso?: string): string {
if (!iso) return '';
try {
const d = new Date(iso);
return d.toLocaleString('ko-KR', {
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
});
} catch { return ''; }
}
const VerificationIssueListPanel: React.FC<Props> = ({
issues,
selectedId,
stageFilter,
searchQuery,
onStageFilterChange,
onSearchChange,
onSelect,
}) => {
const filtered = useMemo(() => {
const q = searchQuery.trim().toLowerCase();
if (!q) return issues;
return issues.filter(i =>
i.title.toLowerCase().includes(q)
|| i.content.toLowerCase().includes(q)
|| (i.reproductionPath?.toLowerCase().includes(q) ?? false),
);
}, [issues, searchQuery]);
return (
<div className="vbd-list-panel">
<div className="vbd-list-filters">
<select
className="vbd-stage-select"
value={stageFilter}
onChange={e => onStageFilterChange(e.target.value as VerificationIssueStage | '')}
aria-label="이슈 단계 필터"
>
<option value=""> </option>
{VERIFICATION_ISSUE_STAGES.map(s => (
<option key={s.value} value={s.value}>{s.label}</option>
))}
</select>
<input
type="search"
className="vbd-search-input"
placeholder="제목·내용·재현경로 검색…"
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
aria-label="검색"
/>
</div>
<div className="vbd-list-count">
{filtered.length}
{searchQuery.trim() && issues.length !== filtered.length && (
<span className="vbd-list-count-sub"> / {issues.length} </span>
)}
</div>
<ul className="vbd-list">
{filtered.length === 0 ? (
<li className="vbd-list-empty"> .</li>
) : filtered.map(issue => {
const active = issue.id === selectedId;
return (
<li key={issue.id}>
<button
type="button"
className={`vbd-list-item${active ? ' vbd-list-item--active' : ''}`}
onClick={() => onSelect(issue)}
>
<VerificationStageIcon stage={issue.stage} size={34} />
<span className="vbd-list-item-body">
<span className="vbd-list-item-top">
<span
className="vbd-stage-badge"
style={{ backgroundColor: `${stageColor(issue.stage)}22`, color: stageColor(issue.stage) }}
>
{stageLabel(issue.stage)}
</span>
<span className="vbd-list-meta">{formatDate(issue.updatedAt ?? issue.createdAt)}</span>
</span>
<span className="vbd-list-title">{issue.title}</span>
</span>
</button>
</li>
);
})}
</ul>
</div>
);
};
export default VerificationIssueListPanel;