release: 2026-04-02 (6건 커밋) #139
@ -4,9 +4,11 @@ import {
|
||||
type ComplianceCategoryResponse,
|
||||
type ComplianceIndicatorResponse,
|
||||
} from '../../api/screeningGuideApi';
|
||||
import { t } from '../../constants/screeningTexts';
|
||||
|
||||
interface ComplianceTabProps {
|
||||
lang: string;
|
||||
indicatorType?: 'SHIP' | 'COMPANY';
|
||||
}
|
||||
|
||||
type IndicatorType = 'SHIP' | 'COMPANY';
|
||||
@ -37,11 +39,11 @@ function getBadgeColor(categoryCode: string): { bg: string; text: string } {
|
||||
return CAT_BADGE_COLORS[categoryCode] ?? { bg: '#e5e7eb', text: '#374151' };
|
||||
}
|
||||
|
||||
export default function ComplianceTab({ lang }: ComplianceTabProps) {
|
||||
export default function ComplianceTab({ lang, indicatorType: fixedType }: ComplianceTabProps) {
|
||||
const [categories, setCategories] = useState<ComplianceCategoryResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [indicatorType, setIndicatorType] = useState<IndicatorType>('SHIP');
|
||||
const [indicatorType, setIndicatorType] = useState<IndicatorType>(fixedType ?? 'SHIP');
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const cache = useRef<Map<CacheKey, ComplianceCategoryResponse[]>>(new Map());
|
||||
|
||||
@ -93,35 +95,37 @@ export default function ComplianceTab({ lang }: ComplianceTabProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Ship / Company 토글 */}
|
||||
<div className="flex gap-2">
|
||||
{(['SHIP', 'COMPANY'] as const).map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setIndicatorType(type)}
|
||||
className={`px-5 py-2 rounded-full text-sm font-bold transition-all ${
|
||||
indicatorType === type
|
||||
? 'bg-wing-text text-wing-bg shadow-sm'
|
||||
: 'bg-wing-card text-wing-muted border border-wing-border hover:text-wing-text'
|
||||
}`}
|
||||
>
|
||||
{type === 'SHIP' ? 'Ship' : 'Company'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Ship / Company 토글 (fixedType이 없을 때만 표시) */}
|
||||
{!fixedType && (
|
||||
<div className="flex gap-2">
|
||||
{(['SHIP', 'COMPANY'] as const).map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setIndicatorType(type)}
|
||||
className={`px-5 py-2 rounded-full text-sm font-bold transition-all ${
|
||||
indicatorType === type
|
||||
? 'bg-wing-text text-wing-bg shadow-sm'
|
||||
: 'bg-wing-card text-wing-muted border border-wing-border hover:text-wing-text'
|
||||
}`}
|
||||
>
|
||||
{type === 'SHIP' ? 'Ship' : 'Company'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<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 className="text-sm">{t(lang, 'loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||
<strong>데이터 로딩 실패:</strong> {error}
|
||||
<strong>{t(lang, 'loadError')}</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { screeningGuideApi, type ChangeHistoryResponse, type ShipInfoResponse, type CompanyInfoResponse, type IndicatorStatusResponse } from '../../api/screeningGuideApi';
|
||||
import { t } from '../../constants/screeningTexts';
|
||||
|
||||
type HistoryType = 'ship-risk' | 'ship-compliance' | 'company-compliance';
|
||||
|
||||
@ -7,32 +9,34 @@ interface HistoryTabProps {
|
||||
lang: string;
|
||||
}
|
||||
|
||||
const HISTORY_TYPES: {
|
||||
interface HistoryTypeConfig {
|
||||
key: HistoryType;
|
||||
label: string;
|
||||
searchLabel: string;
|
||||
searchPlaceholder: string;
|
||||
labelKey: string;
|
||||
searchLabelKey: string;
|
||||
searchPlaceholderKey: string;
|
||||
overallColumn: string | null;
|
||||
}[] = [
|
||||
}
|
||||
|
||||
const HISTORY_TYPES: HistoryTypeConfig[] = [
|
||||
{
|
||||
key: 'ship-risk',
|
||||
label: '선박 위험지표',
|
||||
searchLabel: 'IMO 번호',
|
||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
||||
labelKey: 'histTypeShipRisk',
|
||||
searchLabelKey: 'searchLabelImo',
|
||||
searchPlaceholderKey: 'searchPlaceholderImo',
|
||||
overallColumn: null,
|
||||
},
|
||||
{
|
||||
key: 'ship-compliance',
|
||||
label: '선박 제재',
|
||||
searchLabel: 'IMO 번호',
|
||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
||||
labelKey: 'histTypeShipCompliance',
|
||||
searchLabelKey: 'searchLabelImo',
|
||||
searchPlaceholderKey: 'searchPlaceholderImo',
|
||||
overallColumn: 'lgl_snths_sanction',
|
||||
},
|
||||
{
|
||||
key: 'company-compliance',
|
||||
label: '회사 제재',
|
||||
searchLabel: '회사 코드',
|
||||
searchPlaceholder: '회사 코드 : 1288896',
|
||||
labelKey: 'histTypeCompanyCompliance',
|
||||
searchLabelKey: 'searchLabelCompany',
|
||||
searchPlaceholderKey: 'searchPlaceholderCompany',
|
||||
overallColumn: 'company_snths_compliance_status',
|
||||
},
|
||||
];
|
||||
@ -62,8 +66,19 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
'2': 'Severe',
|
||||
};
|
||||
|
||||
function StatusBadge({ value }: { value: string | null }) {
|
||||
function isNoDataValue(v: string | null | undefined): boolean {
|
||||
return v === '-999' || v === '-999.0' || v === 'null';
|
||||
}
|
||||
|
||||
function StatusBadge({ value, lang = 'EN' }: { value: string | null; lang?: string }) {
|
||||
if (value == null || value === '') return null;
|
||||
if (isNoDataValue(value)) {
|
||||
return (
|
||||
<span className="inline-block rounded-full px-3 py-0.5 text-xs font-bold border bg-gray-100 text-gray-500 border-gray-300">
|
||||
{t(lang, 'noData')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const status = STATUS_MAP[value];
|
||||
if (!status) return <span className="text-xs text-wing-muted">{value}</span>;
|
||||
return (
|
||||
@ -80,8 +95,16 @@ function countryFlag(code: string | null | undefined): string {
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
function RiskValueCell({ value, narrative }: { value: string | null; narrative?: string }) {
|
||||
function RiskValueCell({ value, narrative, lang = 'EN' }: { value: string | null; narrative?: string; lang?: string }) {
|
||||
if (value == null || value === '') return null;
|
||||
if (isNoDataValue(value)) {
|
||||
return (
|
||||
<div className="inline-flex items-start gap-1.5">
|
||||
<span style={{ color: '#9ca3af' }} className="text-sm leading-tight">●</span>
|
||||
<span className="text-xs text-gray-500 leading-relaxed text-left">{t(lang, 'noData')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const color = STATUS_COLORS[value] ?? '#6b7280';
|
||||
const label = narrative || STATUS_LABELS[value] || value;
|
||||
return (
|
||||
@ -98,7 +121,8 @@ const COMPLIANCE_STATUS: Record<string, { label: string; className: string }> =
|
||||
'2': { label: 'Yes', className: 'bg-red-100 text-red-800' },
|
||||
};
|
||||
|
||||
function getRiskLabel(item: IndicatorStatusResponse): string {
|
||||
function getRiskLabel(item: IndicatorStatusResponse, lang: string): string {
|
||||
if (isNoDataValue(item.value)) return t(lang, 'noData');
|
||||
// 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
|
||||
@ -109,7 +133,7 @@ function getRiskLabel(item: IndicatorStatusResponse): string {
|
||||
return item.narrative || STATUS_LABELS[item.value ?? ''] || item.value || '-';
|
||||
}
|
||||
|
||||
function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
|
||||
function RiskStatusGrid({ items, lang }: { items: IndicatorStatusResponse[]; lang: string }) {
|
||||
// Risk Data Maintained 체크: Not Maintained(1)이면 해당 지표만 표시
|
||||
const riskDataMaintained = items.find((i) => i.columnName === 'risk_data_maint');
|
||||
const isNotMaintained = riskDataMaintained?.value === '1';
|
||||
@ -132,7 +156,7 @@ function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
|
||||
<div>
|
||||
{isNotMaintained && (
|
||||
<div className="mb-3 px-3 py-2 bg-yellow-50 border border-yellow-300 rounded-lg text-xs text-yellow-800 font-medium">
|
||||
Risk Data is not maintained for this vessel. Only the maintenance status is shown.
|
||||
{t(lang, 'riskNotMaintained')}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
@ -144,7 +168,7 @@ function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => {
|
||||
const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280';
|
||||
const label = getRiskLabel(item);
|
||||
const label = getRiskLabel(item, lang);
|
||||
return (
|
||||
<div key={item.columnName} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-wing-text truncate flex-1">{item.fieldName}</span>
|
||||
@ -172,7 +196,6 @@ const SHIP_COMPLIANCE_TABS: { key: string; label: string; match: (categoryCode:
|
||||
{ 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 예외 처리
|
||||
@ -189,13 +212,16 @@ function getComplianceLabel(item: IndicatorStatusResponse, isCompany: boolean):
|
||||
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);
|
||||
function ComplianceStatusItem({ item, isCompany, lang = 'EN' }: { item: IndicatorStatusResponse; isCompany: boolean; lang?: string }) {
|
||||
const isNoData = isNoDataValue(item.value);
|
||||
const overrideLabel = isNoData ? t(lang, 'noData') : 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';
|
||||
const displayClass = isNoData
|
||||
? 'bg-gray-100 text-gray-500 border border-gray-300'
|
||||
: 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 (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
@ -210,7 +236,7 @@ function ComplianceStatusItem({ item, isCompany }: { item: IndicatorStatusRespon
|
||||
);
|
||||
}
|
||||
|
||||
function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResponse[]; isCompany: boolean }) {
|
||||
function ComplianceStatusGrid({ items, isCompany, lang }: { items: IndicatorStatusResponse[]; isCompany: boolean; lang: string }) {
|
||||
const [activeTab, setActiveTab] = useState('sanctions');
|
||||
|
||||
// 제외 항목 필터링
|
||||
@ -240,7 +266,7 @@ function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResp
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => (
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} />
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -302,14 +328,14 @@ function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResp
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => (
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} />
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-xs text-wing-muted py-4">해당 항목이 없습니다.</div>
|
||||
<div className="text-center text-xs text-wing-muted py-4">{t(lang, 'noItems')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -329,7 +355,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
const [complianceStatusCache, setComplianceStatusCache] = useState<Record<string, IndicatorStatusResponse[]>>({});
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['info', 'risk', 'compliance', 'history']));
|
||||
|
||||
const currentType = HISTORY_TYPES.find((t) => t.key === historyType)!;
|
||||
const currentType = HISTORY_TYPES.find((ht) => ht.key === historyType)!;
|
||||
const isRisk = historyType === 'ship-risk';
|
||||
const data = cache[lang] ?? [];
|
||||
const riskStatus = riskStatusCache[lang] ?? [];
|
||||
@ -465,17 +491,17 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
{/* 이력 유형 선택 (언더라인 탭) */}
|
||||
<div className="border-b border-wing-border">
|
||||
<div className="flex gap-6">
|
||||
{HISTORY_TYPES.map((t) => (
|
||||
{HISTORY_TYPES.map((ht) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => handleTypeChange(t.key)}
|
||||
key={ht.key}
|
||||
onClick={() => handleTypeChange(ht.key)}
|
||||
className={`pb-2.5 text-sm font-semibold transition-colors border-b-2 -mb-px ${
|
||||
historyType === t.key
|
||||
historyType === ht.key
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-wing-muted border-transparent hover:text-wing-text'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
{t(lang, ht.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -499,7 +525,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={currentType.searchPlaceholder}
|
||||
placeholder={t(lang, currentType.searchPlaceholderKey)}
|
||||
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"
|
||||
/>
|
||||
@ -519,14 +545,14 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
disabled={!searchValue.trim() || loading}
|
||||
className="px-5 py-2 rounded-lg bg-slate-900 text-white text-sm font-bold hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? '조회 중...' : '조회'}
|
||||
{loading ? t(lang, 'searching') : t(lang, 'search')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 에러 */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-300 rounded-xl p-4 text-red-800 text-sm">
|
||||
<strong>조회 실패:</strong> {error}
|
||||
<strong>{t(lang, 'loadError')}</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -542,7 +568,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('info') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">
|
||||
{shipInfo ? '선박 기본 정보' : '회사 기본 정보'}
|
||||
{shipInfo ? t(lang, 'shipBasicInfo') : t(lang, 'companyBasicInfo')}
|
||||
</span>
|
||||
</button>
|
||||
{expandedSections.has('info') && (
|
||||
@ -576,13 +602,13 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
{/* 우측: 스펙 정보 */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">국적</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'nationality')}</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">선종</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'shipType')}</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.shipType || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -594,7 +620,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
<span className="font-medium text-wing-text">{shipInfo.gt || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">건조연도</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'buildYear')}</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.buildYear || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -621,7 +647,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
</div>
|
||||
{companyInfo.parentCompanyName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">모회사</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'parentCompany')}</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.parentCompanyName}</span>
|
||||
</div>
|
||||
)}
|
||||
@ -634,14 +660,14 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
{/* 우측: 상세 정보 */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">등록국가</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'regCountry')}</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
|
||||
</span>
|
||||
</div>
|
||||
{companyInfo.controlCountry && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">관리국가</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'ctrlCountry')}</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
|
||||
</span>
|
||||
@ -649,25 +675,25 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
)}
|
||||
{companyInfo.foundedDate && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">설립일</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'foundedDate')}</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.foundedDate}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.email && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">이메일</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'email')}</span>
|
||||
<span className="font-medium text-wing-text truncate">{companyInfo.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.phone && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">전화</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'phone')}</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.website && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">웹사이트</span>
|
||||
<span className="text-wing-muted w-16">{t(lang, 'website')}</span>
|
||||
<a
|
||||
href={companyInfo.website.startsWith('http') ? companyInfo.website : `https://${companyInfo.website}`}
|
||||
target="_blank"
|
||||
@ -689,16 +715,24 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
{/* Section 2: Current Risk Indicators (선박 탭만) */}
|
||||
{riskStatus.length > 0 && (
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('risk')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('risk') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">Current Risk Indicators</span>
|
||||
</button>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => toggleSection('risk')}
|
||||
className="flex-1 flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('risk') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">{t(lang, 'currentRiskIndicators')}</span>
|
||||
</button>
|
||||
<Link
|
||||
to="/screening-guide?tab=risk"
|
||||
className="shrink-0 mr-4 text-[11px] text-wing-muted hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{t(lang, 'viewGuide')} →
|
||||
</Link>
|
||||
</div>
|
||||
{expandedSections.has('risk') && (
|
||||
<div className="border-t border-wing-border px-4 py-4">
|
||||
<RiskStatusGrid items={riskStatus} />
|
||||
<RiskStatusGrid items={riskStatus} lang={lang} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -712,19 +746,27 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
: null;
|
||||
return (
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('compliance')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('compliance') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">Current Compliance</span>
|
||||
{overallItem && (
|
||||
<StatusBadge value={overallItem.value} />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => toggleSection('compliance')}
|
||||
className="flex-1 flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('compliance') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">{t(lang, 'currentCompliance')}</span>
|
||||
{overallItem && (
|
||||
<StatusBadge value={overallItem.value} lang={lang} />
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
to={`/screening-guide?tab=${isCompany ? 'company-compliance' : 'ship-compliance'}`}
|
||||
className="shrink-0 mr-4 text-[11px] text-wing-muted hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{t(lang, 'viewGuide')} →
|
||||
</Link>
|
||||
</div>
|
||||
{expandedSections.has('compliance') && (
|
||||
<div className="border-t border-wing-border px-4 py-4">
|
||||
<ComplianceStatusGrid items={complianceStatus} isCompany={isCompany} />
|
||||
<ComplianceStatusGrid items={complianceStatus} isCompany={isCompany} lang={lang} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -738,9 +780,9 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('history') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">값 변경 이력</span>
|
||||
<span className="text-sm font-semibold text-wing-text">{t(lang, 'valueChangeHistory')}</span>
|
||||
<span className="text-xs text-wing-muted">
|
||||
{grouped.length}개 일시, {data.length}건 변동
|
||||
{grouped.length}{t(lang, 'dateCount')}, {data.length}{t(lang, 'changeCount')}
|
||||
</span>
|
||||
</button>
|
||||
{expandedSections.has('history') && (
|
||||
@ -779,19 +821,19 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
</span>
|
||||
{hasOverall ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusBadge value={group.overallBefore} />
|
||||
<StatusBadge value={group.overallBefore} lang={lang} />
|
||||
<span className="text-xs text-wing-muted">→</span>
|
||||
<StatusBadge value={group.overallAfter} />
|
||||
<StatusBadge value={group.overallAfter} lang={lang} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-wing-muted">
|
||||
{displayItems.length}건 변동
|
||||
{displayItems.length}{t(lang, 'changeCount')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasOverall && (
|
||||
<span className="text-xs text-wing-muted">
|
||||
{displayItems.length}건 변동
|
||||
{displayItems.length}{t(lang, 'changeCount')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@ -804,19 +846,19 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
style={{ width: '50%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
필드명
|
||||
{t(lang, 'colFieldName')}
|
||||
</th>
|
||||
<th
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
이전값
|
||||
{t(lang, 'colBefore')}
|
||||
</th>
|
||||
<th
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
이후값
|
||||
{t(lang, 'colAfter')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -845,7 +887,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<RiskValueCell value={row.beforeValue} narrative={row.prevNarrative ?? undefined} />
|
||||
<RiskValueCell value={row.beforeValue} narrative={row.prevNarrative ?? undefined} lang={lang} />
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
@ -854,6 +896,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
<RiskValueCell
|
||||
value={row.afterValue}
|
||||
narrative={row.narrative ?? undefined}
|
||||
lang={lang}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
@ -863,13 +906,13 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<StatusBadge value={row.beforeValue} />
|
||||
<StatusBadge value={row.beforeValue} lang={lang} />
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<StatusBadge value={row.afterValue} />
|
||||
<StatusBadge value={row.afterValue} lang={lang} />
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
@ -887,7 +930,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
<div className="flex items-center justify-center py-8 text-wing-muted">
|
||||
<div className="text-center">
|
||||
<div className="text-xl mb-1">📭</div>
|
||||
<div className="text-sm">변경 이력이 없습니다.</div>
|
||||
<div className="text-sm">{t(lang, 'noChangeHistory')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -902,7 +945,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
<div className="flex items-center justify-center py-16 text-wing-muted">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-3">🔍</div>
|
||||
<div className="text-sm">{currentType.searchLabel}을(를) 입력하고 조회하세요.</div>
|
||||
<div className="text-sm">{t(lang, 'enterSearchKey').replace('{label}', t(lang, currentType.searchLabelKey))}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { screeningGuideApi, type MethodologyHistoryResponse } from '../../api/screeningGuideApi';
|
||||
import { t } from '../../constants/screeningTexts';
|
||||
|
||||
interface MethodologyTabProps {
|
||||
lang: string;
|
||||
@ -30,7 +31,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
const [banner, setBanner] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedType, setSelectedType] = useState('전체');
|
||||
const [selectedType, setSelectedType] = useState('ALL');
|
||||
const cache = useRef<Map<LangKey, LangCache>>(new Map());
|
||||
|
||||
const fetchData = useCallback((fetchLang: string) => {
|
||||
@ -81,7 +82,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
const uniqueTypes = Array.from(new Set(history.map((h) => h.changeType)));
|
||||
|
||||
const filtered =
|
||||
selectedType === '전체'
|
||||
selectedType === 'ALL'
|
||||
? sortedHistory
|
||||
: sortedHistory.filter((h) => h.changeType === selectedType);
|
||||
|
||||
@ -90,7 +91,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
<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 className="text-sm">{t(lang, 'loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -99,7 +100,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||
<strong>데이터 로딩 실패:</strong> {error}
|
||||
<strong>{t(lang, 'loadError')}</strong> {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -116,14 +117,14 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
{/* 변경 유형 필터 */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setSelectedType('전체')}
|
||||
onClick={() => setSelectedType('ALL')}
|
||||
className={`px-3 py-1 rounded-full text-[11px] font-bold transition-colors border ${
|
||||
selectedType === '전체'
|
||||
selectedType === 'ALL'
|
||||
? 'bg-slate-900 text-white border-slate-900'
|
||||
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||
}`}
|
||||
>
|
||||
전체 ({history.length})
|
||||
{t(lang, 'all')} ({history.length})
|
||||
</button>
|
||||
{uniqueTypes.map((type) => {
|
||||
const count = history.filter((h) => h.changeType === type).length;
|
||||
@ -132,7 +133,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setSelectedType(isActive ? '전체' : type)}
|
||||
onClick={() => setSelectedType(isActive ? 'ALL' : type)}
|
||||
className="px-3 py-1 rounded-full text-[11px] font-bold transition-all border"
|
||||
style={{
|
||||
background: isActive ? hex : undefined,
|
||||
@ -147,7 +148,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-wing-muted">
|
||||
표시: <strong className="text-wing-text">{filtered.length}</strong>건 | 최신순 정렬
|
||||
{t(lang, 'showing')} <strong className="text-wing-text">{filtered.length}</strong>{t(lang, 'unit')} {t(lang, 'sortLatest')}
|
||||
</div>
|
||||
|
||||
{/* 타임라인 목록 */}
|
||||
@ -156,7 +157,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900 text-white">
|
||||
{['날짜', '변경 유형', '설명'].map((h) => (
|
||||
{[t(lang, 'colDate'), t(lang, 'colChangeType'), t(lang, 'colDescription')].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
||||
@ -200,7 +201,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-center py-12 text-wing-muted text-sm">
|
||||
해당 유형의 변경 이력이 없습니다.
|
||||
{t(lang, 'noMethodologyHistory')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { screeningGuideApi, type RiskCategoryResponse, type RiskIndicatorResponse } from '../../api/screeningGuideApi';
|
||||
import { t } from '../../constants/screeningTexts';
|
||||
|
||||
interface RiskTabProps {
|
||||
lang: string;
|
||||
@ -75,7 +76,7 @@ export default function RiskTab({ lang }: RiskTabProps) {
|
||||
<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 className="text-sm">{t(lang, 'loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -84,7 +85,7 @@ export default function RiskTab({ lang }: RiskTabProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||
<strong>데이터 로딩 실패:</strong> {error}
|
||||
<strong>{t(lang, 'loadError')}</strong> {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
170
frontend/src/constants/screeningTexts.ts
Normal file
170
frontend/src/constants/screeningTexts.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Risk & Compliance 섹션 UI 고정 텍스트 다국어 메타
|
||||
*
|
||||
* lang prop('EN' | 'KO')에 따라 텍스트를 반환한다.
|
||||
* 사용: t(lang, 'key') 또는 screeningTexts[lang].key
|
||||
*/
|
||||
|
||||
interface TextMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const EN: TextMap = {
|
||||
// --- 공통 ---
|
||||
loading: 'Loading...',
|
||||
loadError: 'Failed to load data:',
|
||||
noData: 'No Data',
|
||||
|
||||
// --- RiskComplianceHistory (페이지) ---
|
||||
changeHistoryTitle: 'Risk & Compliance Change History',
|
||||
changeHistorySubtitle: 'S&P Risk Indicator and Compliance Value Change History',
|
||||
|
||||
// --- ScreeningGuide (페이지) ---
|
||||
screeningGuideTitle: 'Risk & Compliance Screening Guide',
|
||||
screeningGuideSubtitle: 'S&P Risk Indicators and Regulatory Compliance Screening Guide',
|
||||
tabShipCompliance: 'Ship Compliance',
|
||||
tabCompanyCompliance: 'Company Compliance',
|
||||
tabRiskIndicators: 'Ship Risk Indicator',
|
||||
tabMethodology: 'Methodology History',
|
||||
|
||||
// --- HistoryTab ---
|
||||
histTypeShipRisk: 'Ship Risk Indicator',
|
||||
histTypeShipCompliance: 'Ship Compliance',
|
||||
histTypeCompanyCompliance: 'Company Compliance',
|
||||
searchLabelImo: 'IMO Number',
|
||||
searchLabelCompany: 'Company Code',
|
||||
searchPlaceholderImo: 'IMO Number : 9672533',
|
||||
searchPlaceholderCompany: 'Company Code : 1288896',
|
||||
search: 'Search',
|
||||
searching: 'Searching...',
|
||||
shipBasicInfo: 'Ship Basic Info',
|
||||
companyBasicInfo: 'Company Basic Info',
|
||||
valueChangeHistory: 'Value Change History',
|
||||
colFieldName: 'Field Name',
|
||||
colBefore: 'Before',
|
||||
colAfter: 'After',
|
||||
noChangeHistory: 'No change history.',
|
||||
noItems: 'No items found.',
|
||||
dateCount: 'dates',
|
||||
changeCount: 'changes',
|
||||
enterSearchKey: 'Enter {label} to search.',
|
||||
|
||||
// 선박/회사 기본 정보 라벨
|
||||
nationality: 'Nationality',
|
||||
shipType: 'Ship Type',
|
||||
buildYear: 'Build Year',
|
||||
regCountry: 'Reg. Country',
|
||||
ctrlCountry: 'Ctrl Country',
|
||||
foundedDate: 'Founded',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
website: 'Website',
|
||||
parentCompany: 'Parent',
|
||||
|
||||
// --- MethodologyTab ---
|
||||
all: 'All',
|
||||
showing: 'Showing:',
|
||||
unit: '',
|
||||
sortLatest: '| Latest first',
|
||||
colDate: 'Date',
|
||||
colChangeType: 'Change Type',
|
||||
colDescription: 'Description',
|
||||
noMethodologyHistory: 'No history for this type.',
|
||||
|
||||
// --- HistoryTab 현재 상태 ---
|
||||
currentRiskIndicators: 'Current Risk Indicators',
|
||||
currentCompliance: 'Current Compliance',
|
||||
riskNotMaintained: 'Risk Data is not maintained for this vessel. Only the maintenance status is shown.',
|
||||
|
||||
// --- Screening Guide 링크 ---
|
||||
viewGuide: 'View Guide',
|
||||
};
|
||||
|
||||
const KO: TextMap = {
|
||||
// --- 공통 ---
|
||||
loading: '데이터를 불러오는 중...',
|
||||
loadError: '데이터 로딩 실패:',
|
||||
noData: '데이터 없음',
|
||||
|
||||
// --- RiskComplianceHistory (페이지) ---
|
||||
changeHistoryTitle: 'Risk & Compliance Change History',
|
||||
changeHistorySubtitle: 'S&P 위험 지표 및 규정 준수 값 변경 이력',
|
||||
|
||||
// --- ScreeningGuide (페이지) ---
|
||||
screeningGuideTitle: 'Risk & Compliance Screening Guide',
|
||||
screeningGuideSubtitle: 'S&P 위험 지표 및 규정 준수 Screening Guide',
|
||||
tabShipCompliance: '선박 규정 준수',
|
||||
tabCompanyCompliance: '회사 규정 준수',
|
||||
tabRiskIndicators: '선박 위험 지표',
|
||||
tabMethodology: '방법론 변경 이력',
|
||||
|
||||
// --- HistoryTab ---
|
||||
histTypeShipRisk: '선박 위험 지표',
|
||||
histTypeShipCompliance: '선박 규정 준수',
|
||||
histTypeCompanyCompliance: '회사 규정 준수',
|
||||
searchLabelImo: 'IMO 번호',
|
||||
searchLabelCompany: '회사 코드',
|
||||
searchPlaceholderImo: 'IMO 번호 : 9290933',
|
||||
searchPlaceholderCompany: '회사 코드 : 1288896',
|
||||
search: '조회',
|
||||
searching: '조회 중...',
|
||||
shipBasicInfo: '선박 기본 정보',
|
||||
companyBasicInfo: '회사 기본 정보',
|
||||
valueChangeHistory: '값 변경 이력',
|
||||
colFieldName: '필드명',
|
||||
colBefore: '이전값',
|
||||
colAfter: '이후값',
|
||||
noChangeHistory: '변경 이력이 없습니다.',
|
||||
noItems: '해당 항목이 없습니다.',
|
||||
dateCount: '개 일시',
|
||||
changeCount: '건 변동',
|
||||
enterSearchKey: '{label}을(를) 입력하고 조회하세요.',
|
||||
|
||||
// 선박/회사 기본 정보 라벨
|
||||
nationality: '국적',
|
||||
shipType: '선종',
|
||||
buildYear: '건조연도',
|
||||
regCountry: '등록국가',
|
||||
ctrlCountry: '관리국가',
|
||||
foundedDate: '설립일',
|
||||
email: '이메일',
|
||||
phone: '전화',
|
||||
website: '웹사이트',
|
||||
parentCompany: '모회사',
|
||||
|
||||
// --- MethodologyTab ---
|
||||
all: '전체',
|
||||
showing: '표시:',
|
||||
unit: '건',
|
||||
sortLatest: '| 최신순 정렬',
|
||||
colDate: '날짜',
|
||||
colChangeType: '변경 유형',
|
||||
colDescription: '설명',
|
||||
noMethodologyHistory: '해당 유형의 변경 이력이 없습니다.',
|
||||
|
||||
// --- HistoryTab 현재 상태 ---
|
||||
currentRiskIndicators: '현재 위험 지표 상태',
|
||||
currentCompliance: '현재 규정 준수 상태',
|
||||
riskNotMaintained: '이 선박의 위험 지표 데이터가 관리되지 않습니다. 관리 대상만 위첨 지표가 표시됩니다.',
|
||||
|
||||
// --- Screening Guide 링크 ---
|
||||
viewGuide: '가이드 보기',
|
||||
};
|
||||
|
||||
const TEXTS: Record<string, TextMap> = { EN, KO };
|
||||
|
||||
/**
|
||||
* lang에 맞는 텍스트를 반환. 키가 없으면 키 자체를 반환.
|
||||
*/
|
||||
export function t(lang: string, key: string): string {
|
||||
return TEXTS[lang]?.[key] ?? TEXTS['EN']?.[key] ?? key;
|
||||
}
|
||||
|
||||
/**
|
||||
* -999 값을 lang에 맞는 "No Data" 텍스트로 변환.
|
||||
* -999가 아니면 원래 값을 그대로 반환.
|
||||
*/
|
||||
export function resolveNoData(lang: string, value: string | null | undefined): string | null | undefined {
|
||||
if (value === '-999' || value === '-999.0') return t(lang, 'noData');
|
||||
return value;
|
||||
}
|
||||
@ -1,17 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import HistoryTab from '../components/screening/HistoryTab';
|
||||
import { t } from '../constants/screeningTexts';
|
||||
|
||||
const LANG_KEY = 'screening-lang';
|
||||
|
||||
export default function RiskComplianceHistory() {
|
||||
const [lang, setLang] = useState('KO');
|
||||
const [lang, setLangState] = useState(() => localStorage.getItem(LANG_KEY) || 'KO');
|
||||
const setLang = useCallback((l: string) => {
|
||||
setLangState(l);
|
||||
localStorage.setItem(LANG_KEY, l);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 + 언어 토글 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-wing-text">Risk & Compliance Change History</h1>
|
||||
<h1 className="text-2xl font-bold text-wing-text">{t(lang, 'changeHistoryTitle')}</h1>
|
||||
<p className="mt-1 text-sm text-wing-muted">
|
||||
S&P 위험 지표 및 규정 준수 값 변경 이력
|
||||
{t(lang, 'changeHistorySubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex border border-wing-border rounded-lg overflow-hidden shrink-0">
|
||||
|
||||
@ -1,19 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import RiskTab from '../components/screening/RiskTab';
|
||||
import ComplianceTab from '../components/screening/ComplianceTab';
|
||||
import MethodologyTab from '../components/screening/MethodologyTab';
|
||||
import { t } from '../constants/screeningTexts';
|
||||
|
||||
type ActiveTab = 'compliance' | 'risk' | 'methodology';
|
||||
|
||||
const TABS: { key: ActiveTab; label: string }[] = [
|
||||
{ key: 'compliance', label: 'Compliance' },
|
||||
{ key: 'risk', label: 'Risk Indicators' },
|
||||
{ key: 'methodology', label: 'Methodology History' },
|
||||
];
|
||||
type ActiveTab = 'risk' | 'ship-compliance' | 'company-compliance' | 'methodology';
|
||||
const VALID_TABS: ActiveTab[] = ['risk', 'ship-compliance', 'company-compliance', 'methodology'];
|
||||
const LANG_KEY = 'screening-lang';
|
||||
|
||||
export default function ScreeningGuide() {
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('compliance');
|
||||
const [lang, setLang] = useState('EN');
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialTab = VALID_TABS.includes(searchParams.get('tab') as ActiveTab)
|
||||
? (searchParams.get('tab') as ActiveTab)
|
||||
: 'risk';
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>(initialTab);
|
||||
const [lang, setLangState] = useState(() => localStorage.getItem(LANG_KEY) || 'EN');
|
||||
const setLang = useCallback((l: string) => {
|
||||
setLangState(l);
|
||||
localStorage.setItem(LANG_KEY, l);
|
||||
}, []);
|
||||
|
||||
const tabs: { key: ActiveTab; label: string }[] = [
|
||||
{ key: 'risk', label: t(lang, 'tabRiskIndicators') },
|
||||
{ key: 'ship-compliance', label: t(lang, 'tabShipCompliance') },
|
||||
{ key: 'company-compliance', label: t(lang, 'tabCompanyCompliance') },
|
||||
{ key: 'methodology', label: t(lang, 'tabMethodology') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -21,10 +34,10 @@ export default function ScreeningGuide() {
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-wing-text">
|
||||
Risk & Compliance Screening Guide
|
||||
{t(lang, 'screeningGuideTitle')}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-wing-muted">
|
||||
S&P Risk Indicators and Regulatory Compliance Screening Guide
|
||||
{t(lang, 'screeningGuideSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
{/* 언어 토글 */}
|
||||
@ -48,7 +61,7 @@ export default function ScreeningGuide() {
|
||||
{/* 언더라인 탭 */}
|
||||
<div className="border-b border-wing-border">
|
||||
<div className="flex gap-6">
|
||||
{TABS.map((tab) => (
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
@ -65,8 +78,9 @@ export default function ScreeningGuide() {
|
||||
</div>
|
||||
|
||||
{/* 탭 내용 */}
|
||||
{activeTab === 'compliance' && <ComplianceTab lang={lang} />}
|
||||
{activeTab === 'risk' && <RiskTab lang={lang} />}
|
||||
{activeTab === 'ship-compliance' && <ComplianceTab lang={lang} indicatorType="SHIP" />}
|
||||
{activeTab === 'company-compliance' && <ComplianceTab lang={lang} indicatorType="COMPANY" />}
|
||||
{activeTab === 'methodology' && <MethodologyTab lang={lang} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user