import { useState, useMemo } from 'react'; import { screeningGuideApi, type ChangeHistoryResponse, type ShipInfoResponse, type CompanyInfoResponse, type IndicatorStatusResponse } from '../../api/screeningGuideApi'; type HistoryType = 'ship-risk' | 'ship-compliance' | 'company-compliance'; interface HistoryTabProps { lang: string; } const HISTORY_TYPES: { key: HistoryType; label: string; searchLabel: string; searchPlaceholder: string; overallColumn: string | null; }[] = [ { key: 'ship-risk', label: '선박 위험지표', searchLabel: 'IMO 번호', searchPlaceholder: 'IMO 번호 : 9672533', overallColumn: null, }, { key: 'ship-compliance', label: '선박 제재', searchLabel: 'IMO 번호', searchPlaceholder: 'IMO 번호 : 9672533', overallColumn: 'lgl_snths_sanction', }, { key: 'company-compliance', label: '회사 제재', searchLabel: '회사 코드', searchPlaceholder: '회사 코드 : 1288896', overallColumn: 'company_snths_compliance_status', }, ]; interface GroupedHistory { lastModifiedDate: string; items: ChangeHistoryResponse[]; overallBefore: string | null; overallAfter: string | null; } const STATUS_MAP: Record = { '0': { label: 'All Clear', className: 'bg-green-100 text-green-800 border-green-300' }, '1': { label: 'Warning', className: 'bg-yellow-100 text-yellow-800 border-yellow-300' }, '2': { label: 'Severe', className: 'bg-red-100 text-red-800 border-red-300' }, }; const STATUS_COLORS: Record = { '0': '#22c55e', '1': '#eab308', '2': '#ef4444', }; const STATUS_LABELS: Record = { '0': 'All Clear', '1': 'Warning', '2': 'Severe', }; function StatusBadge({ value }: { value: string | null }) { if (value == null || value === '') return null; const status = STATUS_MAP[value]; if (!status) return {value}; return ( {status.label} ); } function countryFlag(code: string | null | undefined): string { if (!code || code.length < 2) return ''; const cc = code.slice(0, 2).toUpperCase(); const codePoints = [...cc].map((c) => 0x1F1E6 + c.charCodeAt(0) - 65); return String.fromCodePoint(...codePoints); } function RiskValueCell({ value, narrative }: { value: string | null; narrative?: string }) { if (value == null || value === '') return null; const color = STATUS_COLORS[value] ?? '#6b7280'; const label = narrative || STATUS_LABELS[value] || value; return (
{label}
); } const COMPLIANCE_STATUS: Record = { '0': { label: 'No', className: 'bg-green-100 text-green-800' }, '1': { label: 'Warning', className: 'bg-yellow-100 text-yellow-800' }, '2': { label: 'Yes', className: 'bg-red-100 text-red-800' }, }; function getRiskLabel(item: IndicatorStatusResponse): string { // IUU Fishing: All Clear -> None recorded if (item.columnName === 'ilgl_fshr_viol' && item.value === '0') return 'None recorded'; // Risk Data Maintained: 0 -> Yes, 1 -> Not Maintained if (item.columnName === 'risk_data_maint') { if (item.value === '0') return 'Yes'; if (item.value === '1') return 'Not Maintained'; } return item.narrative || STATUS_LABELS[item.value ?? ''] || item.value || '-'; } function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) { // Risk Data Maintained 체크: Not Maintained(1)이면 해당 지표만 표시 const riskDataMaintained = items.find((i) => i.columnName === 'risk_data_maint'); const isNotMaintained = riskDataMaintained?.value === '1'; const displayItems = isNotMaintained ? items.filter((i) => i.columnName === 'risk_data_maint') : items; const categories = useMemo(() => { const map = new Map(); for (const item of displayItems) { const cat = item.category || 'Other'; if (!map.has(cat)) map.set(cat, []); map.get(cat)!.push(item); } return Array.from(map.entries()); }, [displayItems]); return (
{isNotMaintained && (
Risk Data is not maintained for this vessel. Only the maintenance status is shown.
)}
{categories.map(([category, catItems]) => (
{category}
{catItems.map((item) => { const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280'; const label = getRiskLabel(item); return (
{item.fieldName} {label}
); })}
))}
); } // 선박 Compliance 탭 분류 (categoryCode 기반) const SHIP_COMPLIANCE_TABS: { key: string; label: string; match: (categoryCode: string) => boolean }[] = [ { key: 'sanctions', label: 'Sanctions', match: (code) => code.startsWith('SANCTIONS_') }, { key: 'portcalls', label: 'Port Calls', match: (code) => code === 'PORT_CALLS' }, { key: 'sts', label: 'STS Activity', match: (code) => code === 'STS_ACTIVITY' }, { key: 'suspicious', label: 'Suspicious Behavior', match: (code) => code === 'SUSPICIOUS_BEHAVIOR' }, { key: 'ownership', label: 'Ownership Screening', match: (code) => code === 'OWNERSHIP_SCREENING' }, ]; // Compliance 예외 처리 function getComplianceLabel(item: IndicatorStatusResponse, isCompany: boolean): string | null { // Parent Company 관련: null -> No Parent if (item.value == null || item.value === '') { if (item.fieldName.includes('Parent Company') || item.fieldName.includes('Parent company')) return 'No Parent'; if (isCompany && item.columnName === 'prnt_company_compliance_risk') return 'No Parent'; } return null; } // 제외할 컬럼명 const SHIP_COMPLIANCE_EXCLUDE = ['lgl_snths_sanction']; // Overall은 토글 헤더에 표시 const COMPANY_COMPLIANCE_EXCLUDE = ['company_snths_compliance_status']; // Overall은 토글 헤더에 표시 function ComplianceStatusItem({ item, isCompany }: { item: IndicatorStatusResponse; isCompany: boolean }) { const overrideLabel = getComplianceLabel(item, isCompany); const status = overrideLabel ? null : COMPLIANCE_STATUS[item.value ?? '']; const displayLabel = overrideLabel || (status ? status.label : (item.value ?? '-')); const displayClass = overrideLabel ? 'bg-wing-surface text-wing-muted border border-wing-border' : status ? status.className : 'bg-wing-surface text-wing-muted border border-wing-border'; return (
{item.fieldName} {displayLabel}
); } function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResponse[]; isCompany: boolean }) { const [activeTab, setActiveTab] = useState('sanctions'); // 제외 항목 필터링 const excludeList = isCompany ? COMPANY_COMPLIANCE_EXCLUDE : SHIP_COMPLIANCE_EXCLUDE; const filteredItems = items.filter((i) => !excludeList.includes(i.columnName)); // 회사: 탭 없이 2컬럼 그리드 if (isCompany) { const categories = useMemo(() => { const map = new Map(); for (const item of filteredItems) { const cat = item.category || 'Other'; if (!map.has(cat)) map.set(cat, []); map.get(cat)!.push(item); } return Array.from(map.entries()); }, [filteredItems]); return (
{categories.map(([category, catItems]) => (
{categories.length > 1 && (
{category}
)}
{catItems.map((item) => ( ))}
))}
); } // 선박: 탭 기반 분류 const tabData = useMemo(() => { const result: Record = {}; for (const tab of SHIP_COMPLIANCE_TABS) { result[tab.key] = filteredItems.filter((i) => tab.match(i.categoryCode)); } return result; }, [filteredItems]); const currentItems = tabData[activeTab] ?? []; // 현재 탭 내 카테고리별 그룹핑 const categories = useMemo(() => { const map = new Map(); for (const item of currentItems) { const cat = item.category || 'Other'; if (!map.has(cat)) map.set(cat, []); map.get(cat)!.push(item); } return Array.from(map.entries()); }, [currentItems]); return (
{/* 탭 버튼 */}
{SHIP_COMPLIANCE_TABS.map((tab) => ( ))}
{/* 현재 탭 내용 */} {currentItems.length > 0 ? (
{categories.map(([category, catItems]) => (
{categories.length > 1 && (
{category}
)}
{catItems.map((item) => ( ))}
))}
) : (
해당 항목이 없습니다.
)}
); } export default function HistoryTab({ lang }: HistoryTabProps) { const [historyType, setHistoryType] = useState('ship-risk'); const [searchValue, setSearchValue] = useState(''); const [cache, setCache] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searched, setSearched] = useState(false); const [expandedDates, setExpandedDates] = useState>(new Set()); const [shipInfo, setShipInfo] = useState(null); const [companyInfo, setCompanyInfo] = useState(null); const [riskStatusCache, setRiskStatusCache] = useState>({}); const [complianceStatusCache, setComplianceStatusCache] = useState>({}); const [expandedSections, setExpandedSections] = useState>(new Set(['info', 'risk', 'compliance', 'history'])); const currentType = HISTORY_TYPES.find((t) => t.key === historyType)!; const isRisk = historyType === 'ship-risk'; const data = cache[lang] ?? []; const riskStatus = riskStatusCache[lang] ?? []; const complianceStatus = complianceStatusCache[lang] ?? []; const grouped: GroupedHistory[] = useMemo(() => { const map = new Map(); for (const item of data) { const key = item.lastModifiedDate; if (!map.has(key)) map.set(key, []); map.get(key)!.push(item); } return Array.from(map.entries()).map(([date, items]) => { const overallColumn = currentType.overallColumn; if (overallColumn) { const overallItem = items.find((item) => item.changedColumnName === overallColumn); return { lastModifiedDate: date, items, overallBefore: overallItem?.beforeValue ?? null, overallAfter: overallItem?.afterValue ?? null, }; } return { lastModifiedDate: date, items, overallBefore: null, overallAfter: null }; }); }, [data, currentType.overallColumn]); function handleSearch() { const trimmed = searchValue.trim(); if (!trimmed) return; setLoading(true); setError(null); setSearched(true); setExpandedDates(new Set()); setExpandedSections(new Set(['info', 'risk', 'compliance', 'history'])); const isShip = historyType !== 'company-compliance'; const showRisk = historyType === 'ship-risk'; const getHistoryCall = (l: string) => historyType === 'ship-risk' ? screeningGuideApi.getShipRiskHistory(trimmed, l) : historyType === 'ship-compliance' ? screeningGuideApi.getShipComplianceHistory(trimmed, l) : screeningGuideApi.getCompanyComplianceHistory(trimmed, l); const promises: Promise[] = [ getHistoryCall('KO'), getHistoryCall('EN'), ]; if (isShip) { promises.push( screeningGuideApi.getShipInfo(trimmed), screeningGuideApi.getShipComplianceStatus(trimmed, 'KO'), screeningGuideApi.getShipComplianceStatus(trimmed, 'EN'), ); if (showRisk) { promises.push( screeningGuideApi.getShipRiskStatus(trimmed, 'KO'), screeningGuideApi.getShipRiskStatus(trimmed, 'EN'), ); } } else { promises.push( screeningGuideApi.getCompanyInfo(trimmed), screeningGuideApi.getCompanyComplianceStatus(trimmed, 'KO'), screeningGuideApi.getCompanyComplianceStatus(trimmed, 'EN'), ); } Promise.all(promises) .then((results) => { setCache({ KO: results[0].data ?? [], EN: results[1].data ?? [] }); if (isShip) { setShipInfo(results[2].data); setCompanyInfo(null); setComplianceStatusCache({ KO: results[3].data ?? [], EN: results[4].data ?? [] }); if (showRisk) { setRiskStatusCache({ KO: results[5].data ?? [], EN: results[6].data ?? [] }); } else { setRiskStatusCache({}); } } else { setShipInfo(null); setCompanyInfo(results[2].data); setRiskStatusCache({}); setComplianceStatusCache({ KO: results[3].data ?? [], EN: results[4].data ?? [] }); } }) .catch((err: Error) => setError(err.message)) .finally(() => setLoading(false)); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === 'Enter') handleSearch(); } function handleTypeChange(type: HistoryType) { setHistoryType(type); setSearchValue(''); setCache({}); setError(null); setSearched(false); setExpandedDates(new Set()); setShipInfo(null); setCompanyInfo(null); setRiskStatusCache({}); setComplianceStatusCache({}); setExpandedSections(new Set(['info', 'risk', 'compliance', 'history'])); } function toggleSection(section: string) { setExpandedSections((prev) => { const next = new Set(prev); if (next.has(section)) next.delete(section); else next.add(section); return next; }); } function toggleDate(date: string) { setExpandedDates((prev) => { const next = new Set(prev); if (next.has(date)) next.delete(date); else next.add(date); return next; }); } return (
{/* 이력 유형 선택 (언더라인 탭) */}
{HISTORY_TYPES.map((t) => ( ))}
{/* 검색 */}
setSearchValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={currentType.searchPlaceholder} className="w-full pl-10 pr-8 py-2 border border-wing-border rounded-lg text-sm focus:ring-2 focus:ring-wing-accent focus:border-wing-accent outline-none" /> {searchValue && ( )}
{/* 에러 */} {error && (
조회 실패: {error}
)} {/* 결과: 3개 섹션 */} {searched && !loading && !error && (
{/* Section 1: 기본 정보 */} {(shipInfo || companyInfo) && (
{expandedSections.has('info') && (
{shipInfo && (
{/* 좌측: 핵심 식별 정보 */}
{shipInfo.shipName || '-'}
IMO {shipInfo.imoNo}
MMSI {shipInfo.mmsiNo || '-'}
Status {shipInfo.shipStatus || '-'}
{/* 구분선 */}
{/* 우측: 스펙 정보 */}
국적 {countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
선종 {shipInfo.shipType || '-'}
DWT {shipInfo.dwt || '-'}
GT {shipInfo.gt || '-'}
건조연도 {shipInfo.buildYear || '-'}
)} {companyInfo && (
{/* 좌측: 핵심 식별 정보 */}
{companyInfo.fullName || '-'}
{companyInfo.abbreviation && (
{companyInfo.abbreviation}
)}
Code {companyInfo.companyCode}
Status {companyInfo.status || '-'}
{companyInfo.parentCompanyName && (
모회사 {companyInfo.parentCompanyName}
)}
{/* 구분선 */}
{/* 우측: 상세 정보 */}
등록국가 {countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
{companyInfo.controlCountry && (
관리국가 {countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
)} {companyInfo.foundedDate && (
설립일 {companyInfo.foundedDate}
)} {companyInfo.email && (
이메일 {companyInfo.email}
)} {companyInfo.phone && (
전화 {companyInfo.phone}
)} {companyInfo.website && ( )}
)}
)}
)} {/* Section 2: Current Risk Indicators (선박 탭만) */} {riskStatus.length > 0 && (
{expandedSections.has('risk') && (
)}
)} {/* Section 3: Current Compliance (선박 제재/회사 제재 탭만) */} {complianceStatus.length > 0 && !isRisk && (() => { const isCompany = historyType === 'company-compliance'; const overallItem = isCompany ? complianceStatus.find((i) => i.columnName === 'company_snths_compliance_status') : null; return (
{expandedSections.has('compliance') && (
)}
); })()} {/* Section 4: 값 변경 이력 */}
{expandedSections.has('history') && (
{grouped.length > 0 ? (
{grouped.map((group) => { const isExpanded = expandedDates.has(group.lastModifiedDate); const hasOverall = group.overallBefore != null || group.overallAfter != null; const displayItems = (currentType.overallColumn ? group.items.filter( (item) => item.changedColumnName !== currentType.overallColumn, ) : [...group.items] ).sort((a, b) => (a.sortOrder ?? Infinity) - (b.sortOrder ?? Infinity)); return (
{isExpanded && displayItems.length > 0 && (
{displayItems.map((row) => ( {isRisk ? ( <> ) : ( <> )} ))}
필드명 이전값 이후값
{row.fieldName || row.changedColumnName}
{row.fieldName && (
{row.changedColumnName}
)}
)}
); })}
) : (
📭
변경 이력이 없습니다.
)}
)}
)} {/* 초기 상태 */} {!searched && (
🔍
{currentType.searchLabel}을(를) 입력하고 조회하세요.
)}
); }