209 lines
8.3 KiB
TypeScript
209 lines
8.3 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { screeningGuideApi, type MethodologyHistoryResponse } from '../../api/screeningGuideApi';
|
|
|
|
interface MethodologyTabProps {
|
|
lang: string;
|
|
}
|
|
|
|
const CHANGE_TYPE_COLORS: Record<string, string> = {
|
|
Addition: '#065f46',
|
|
Update: '#1d4ed8',
|
|
Expansion: '#6b21a8',
|
|
Change: '#92400e',
|
|
Removal: '#991b1b',
|
|
New: '#0f766e',
|
|
};
|
|
|
|
function getChangeTypeColor(changeType: string): string {
|
|
return CHANGE_TYPE_COLORS[changeType] ?? '#374151';
|
|
}
|
|
|
|
type LangKey = 'KO' | 'EN';
|
|
|
|
interface LangCache {
|
|
history: MethodologyHistoryResponse[];
|
|
banner: string;
|
|
}
|
|
|
|
export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|
const [history, setHistory] = useState<MethodologyHistoryResponse[]>([]);
|
|
const [banner, setBanner] = useState<string>('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedType, setSelectedType] = useState('전체');
|
|
const cache = useRef<Map<LangKey, LangCache>>(new Map());
|
|
|
|
const fetchData = useCallback((fetchLang: string) => {
|
|
return Promise.all([
|
|
screeningGuideApi.getMethodologyHistory(fetchLang),
|
|
screeningGuideApi.getMethodologyBanner(fetchLang).catch(() => ({ data: null })),
|
|
]).then(([historyRes, bannerRes]) => {
|
|
const data: LangCache = {
|
|
history: historyRes.data ?? [],
|
|
banner: bannerRes.data?.description ?? '',
|
|
};
|
|
cache.current.set(fetchLang as LangKey, data);
|
|
return data;
|
|
});
|
|
}, []);
|
|
|
|
// 초기 로드: KO/EN 데이터 모두 가져와서 캐싱
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
setError(null);
|
|
cache.current.clear();
|
|
|
|
Promise.all([fetchData('KO'), fetchData('EN')])
|
|
.then(() => {
|
|
const cached = cache.current.get(lang as LangKey);
|
|
if (cached) {
|
|
setHistory(cached.history);
|
|
setBanner(cached.banner);
|
|
}
|
|
})
|
|
.catch((err: Error) => setError(err.message))
|
|
.finally(() => setLoading(false));
|
|
}, [fetchData]);
|
|
|
|
// 언어 변경: 캐시에서 스위칭
|
|
useEffect(() => {
|
|
const cached = cache.current.get(lang as LangKey);
|
|
if (cached) {
|
|
setHistory(cached.history);
|
|
setBanner(cached.banner);
|
|
}
|
|
}, [lang]);
|
|
|
|
const sortedHistory = [...history].sort((a, b) =>
|
|
b.changeDate.localeCompare(a.changeDate),
|
|
);
|
|
|
|
const uniqueTypes = Array.from(new Set(history.map((h) => h.changeType)));
|
|
|
|
const filtered =
|
|
selectedType === '전체'
|
|
? sortedHistory
|
|
: sortedHistory.filter((h) => h.changeType === selectedType);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20 text-wing-muted">
|
|
<div className="text-center">
|
|
<div className="text-2xl mb-2">⏳</div>
|
|
<div className="text-sm">데이터를 불러오는 중...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
|
<strong>데이터 로딩 실패:</strong> {error}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 주의사항 배너 */}
|
|
{banner && (
|
|
<div className="bg-amber-50 border border-amber-300 rounded-xl px-4 py-3 text-amber-800 text-xs leading-relaxed">
|
|
{banner}
|
|
</div>
|
|
)}
|
|
|
|
{/* 변경 유형 필터 */}
|
|
<div className="flex gap-2 flex-wrap">
|
|
<button
|
|
onClick={() => setSelectedType('전체')}
|
|
className={`px-3 py-1 rounded-full text-[11px] font-bold transition-colors border ${
|
|
selectedType === '전체'
|
|
? 'bg-slate-900 text-white border-slate-900'
|
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
|
}`}
|
|
>
|
|
전체 ({history.length})
|
|
</button>
|
|
{uniqueTypes.map((type) => {
|
|
const count = history.filter((h) => h.changeType === type).length;
|
|
const hex = getChangeTypeColor(type);
|
|
const isActive = selectedType === type;
|
|
return (
|
|
<button
|
|
key={type}
|
|
onClick={() => setSelectedType(isActive ? '전체' : type)}
|
|
className="px-3 py-1 rounded-full text-[11px] font-bold transition-all border"
|
|
style={{
|
|
background: isActive ? hex : undefined,
|
|
borderColor: isActive ? hex : undefined,
|
|
color: isActive ? 'white' : undefined,
|
|
}}
|
|
>
|
|
{type} ({count})
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="text-xs text-wing-muted">
|
|
표시: <strong className="text-wing-text">{filtered.length}</strong>건 | 최신순 정렬
|
|
</div>
|
|
|
|
{/* 타임라인 목록 */}
|
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-xs border-collapse">
|
|
<thead>
|
|
<tr className="bg-slate-900 text-white">
|
|
{['날짜', '변경 유형', '설명'].map((h) => (
|
|
<th
|
|
key={h}
|
|
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
|
>
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map((row, i) => {
|
|
const hex = getChangeTypeColor(row.changeType);
|
|
return (
|
|
<tr
|
|
key={`${row.historyId}-${i}`}
|
|
className="border-b border-wing-border align-top even:bg-wing-card"
|
|
>
|
|
<td className="px-3 py-3 min-w-[110px]">
|
|
<div className="font-bold text-wing-text">
|
|
{row.changeDate}
|
|
</div>
|
|
</td>
|
|
<td className="px-3 py-3 min-w-[110px]">
|
|
<span
|
|
className="inline-block text-white rounded px-2 py-0.5 text-[10px] font-bold"
|
|
style={{ background: hex }}
|
|
>
|
|
{row.changeType}
|
|
</span>
|
|
</td>
|
|
<td className="px-3 py-3 min-w-[280px] leading-relaxed text-wing-text">
|
|
{row.description || '-'}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{filtered.length === 0 && (
|
|
<div className="text-center py-12 text-wing-muted text-sm">
|
|
해당 유형의 변경 이력이 없습니다.
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|