Merge pull request 'feat(screening): Risk & Compliance 다국어 지원 및 사용자 편의성 개선 (#134)' (#136) from feature/ISSUE-134-risk-compliance-ux into develop
This commit is contained in:
커밋
06f82df33a
@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 추가
|
||||||
|
- Risk & Compliance 사용자 편의성 개선 (#134)
|
||||||
|
- UI 고정 텍스트 다국어 지원 (EN/KO 언어 토글 연동)
|
||||||
|
- -999/null 값 'No Data'/'데이터 없음' 표시 처리
|
||||||
|
- Screening Guide 탭 분리 (Ship Compliance / Company Compliance)
|
||||||
|
- Change History ↔ Screening Guide 간 언어 설정 공유 (localStorage)
|
||||||
|
- 섹션 헤더에 Screening Guide 연결 링크 추가
|
||||||
|
|
||||||
## [2026-04-01]
|
## [2026-04-01]
|
||||||
|
|
||||||
### 추가
|
### 추가
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import {
|
|||||||
type ComplianceCategoryResponse,
|
type ComplianceCategoryResponse,
|
||||||
type ComplianceIndicatorResponse,
|
type ComplianceIndicatorResponse,
|
||||||
} from '../../api/screeningGuideApi';
|
} from '../../api/screeningGuideApi';
|
||||||
|
import { t } from '../../constants/screeningTexts';
|
||||||
|
|
||||||
interface ComplianceTabProps {
|
interface ComplianceTabProps {
|
||||||
lang: string;
|
lang: string;
|
||||||
|
indicatorType?: 'SHIP' | 'COMPANY';
|
||||||
}
|
}
|
||||||
|
|
||||||
type 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' };
|
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 [categories, setCategories] = useState<ComplianceCategoryResponse[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||||
const cache = useRef<Map<CacheKey, ComplianceCategoryResponse[]>>(new Map());
|
const cache = useRef<Map<CacheKey, ComplianceCategoryResponse[]>>(new Map());
|
||||||
|
|
||||||
@ -93,35 +95,37 @@ export default function ComplianceTab({ lang }: ComplianceTabProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Ship / Company 토글 */}
|
{/* Ship / Company 토글 (fixedType이 없을 때만 표시) */}
|
||||||
<div className="flex gap-2">
|
{!fixedType && (
|
||||||
{(['SHIP', 'COMPANY'] as const).map((type) => (
|
<div className="flex gap-2">
|
||||||
<button
|
{(['SHIP', 'COMPANY'] as const).map((type) => (
|
||||||
key={type}
|
<button
|
||||||
onClick={() => setIndicatorType(type)}
|
key={type}
|
||||||
className={`px-5 py-2 rounded-full text-sm font-bold transition-all ${
|
onClick={() => setIndicatorType(type)}
|
||||||
indicatorType === type
|
className={`px-5 py-2 rounded-full text-sm font-bold transition-all ${
|
||||||
? 'bg-wing-text text-wing-bg shadow-sm'
|
indicatorType === type
|
||||||
: 'bg-wing-card text-wing-muted border border-wing-border hover:text-wing-text'
|
? '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>
|
{type === 'SHIP' ? 'Ship' : 'Company'}
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center py-20 text-wing-muted">
|
<div className="flex items-center justify-center py-20 text-wing-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl mb-2">⏳</div>
|
<div className="text-2xl mb-2">⏳</div>
|
||||||
<div className="text-sm">데이터를 불러오는 중...</div>
|
<div className="text-sm">{t(lang, 'loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { useState, useMemo } from 'react';
|
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 { 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';
|
type HistoryType = 'ship-risk' | 'ship-compliance' | 'company-compliance';
|
||||||
|
|
||||||
@ -7,32 +9,34 @@ interface HistoryTabProps {
|
|||||||
lang: string;
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HISTORY_TYPES: {
|
interface HistoryTypeConfig {
|
||||||
key: HistoryType;
|
key: HistoryType;
|
||||||
label: string;
|
labelKey: string;
|
||||||
searchLabel: string;
|
searchLabelKey: string;
|
||||||
searchPlaceholder: string;
|
searchPlaceholderKey: string;
|
||||||
overallColumn: string | null;
|
overallColumn: string | null;
|
||||||
}[] = [
|
}
|
||||||
|
|
||||||
|
const HISTORY_TYPES: HistoryTypeConfig[] = [
|
||||||
{
|
{
|
||||||
key: 'ship-risk',
|
key: 'ship-risk',
|
||||||
label: '선박 위험지표',
|
labelKey: 'histTypeShipRisk',
|
||||||
searchLabel: 'IMO 번호',
|
searchLabelKey: 'searchLabelImo',
|
||||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
searchPlaceholderKey: 'searchPlaceholderImo',
|
||||||
overallColumn: null,
|
overallColumn: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ship-compliance',
|
key: 'ship-compliance',
|
||||||
label: '선박 제재',
|
labelKey: 'histTypeShipCompliance',
|
||||||
searchLabel: 'IMO 번호',
|
searchLabelKey: 'searchLabelImo',
|
||||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
searchPlaceholderKey: 'searchPlaceholderImo',
|
||||||
overallColumn: 'lgl_snths_sanction',
|
overallColumn: 'lgl_snths_sanction',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'company-compliance',
|
key: 'company-compliance',
|
||||||
label: '회사 제재',
|
labelKey: 'histTypeCompanyCompliance',
|
||||||
searchLabel: '회사 코드',
|
searchLabelKey: 'searchLabelCompany',
|
||||||
searchPlaceholder: '회사 코드 : 1288896',
|
searchPlaceholderKey: 'searchPlaceholderCompany',
|
||||||
overallColumn: 'company_snths_compliance_status',
|
overallColumn: 'company_snths_compliance_status',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -62,8 +66,19 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
'2': 'Severe',
|
'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 (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];
|
const status = STATUS_MAP[value];
|
||||||
if (!status) return <span className="text-xs text-wing-muted">{value}</span>;
|
if (!status) return <span className="text-xs text-wing-muted">{value}</span>;
|
||||||
return (
|
return (
|
||||||
@ -80,8 +95,16 @@ function countryFlag(code: string | null | undefined): string {
|
|||||||
return String.fromCodePoint(...codePoints);
|
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 (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 color = STATUS_COLORS[value] ?? '#6b7280';
|
||||||
const label = narrative || STATUS_LABELS[value] || value;
|
const label = narrative || STATUS_LABELS[value] || value;
|
||||||
return (
|
return (
|
||||||
@ -98,7 +121,8 @@ const COMPLIANCE_STATUS: Record<string, { label: string; className: string }> =
|
|||||||
'2': { label: 'Yes', className: 'bg-red-100 text-red-800' },
|
'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
|
// IUU Fishing: All Clear -> None recorded
|
||||||
if (item.columnName === 'ilgl_fshr_viol' && item.value === '0') return 'None recorded';
|
if (item.columnName === 'ilgl_fshr_viol' && item.value === '0') return 'None recorded';
|
||||||
// Risk Data Maintained: 0 -> Yes, 1 -> Not Maintained
|
// 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 || '-';
|
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)이면 해당 지표만 표시
|
// Risk Data Maintained 체크: Not Maintained(1)이면 해당 지표만 표시
|
||||||
const riskDataMaintained = items.find((i) => i.columnName === 'risk_data_maint');
|
const riskDataMaintained = items.find((i) => i.columnName === 'risk_data_maint');
|
||||||
const isNotMaintained = riskDataMaintained?.value === '1';
|
const isNotMaintained = riskDataMaintained?.value === '1';
|
||||||
@ -132,7 +156,7 @@ function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
|
|||||||
<div>
|
<div>
|
||||||
{isNotMaintained && (
|
{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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<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">
|
<div className="space-y-1.5">
|
||||||
{catItems.map((item) => {
|
{catItems.map((item) => {
|
||||||
const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280';
|
const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280';
|
||||||
const label = getRiskLabel(item);
|
const label = getRiskLabel(item, lang);
|
||||||
return (
|
return (
|
||||||
<div key={item.columnName} className="flex items-center gap-2 text-xs">
|
<div key={item.columnName} className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-wing-text truncate flex-1">{item.fieldName}</span>
|
<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: 'portcalls', label: 'Port Calls', match: (code) => code === 'PORT_CALLS' },
|
||||||
{ key: 'sts', label: 'STS Activity', match: (code) => code === 'STS_ACTIVITY' },
|
{ key: 'sts', label: 'STS Activity', match: (code) => code === 'STS_ACTIVITY' },
|
||||||
{ key: 'suspicious', label: 'Suspicious Behavior', match: (code) => code === 'SUSPICIOUS_BEHAVIOR' },
|
{ key: 'suspicious', label: 'Suspicious Behavior', match: (code) => code === 'SUSPICIOUS_BEHAVIOR' },
|
||||||
{ key: 'ownership', label: 'Ownership Screening', match: (code) => code === 'OWNERSHIP_SCREENING' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Compliance 예외 처리
|
// Compliance 예외 처리
|
||||||
@ -189,13 +212,16 @@ function getComplianceLabel(item: IndicatorStatusResponse, isCompany: boolean):
|
|||||||
const SHIP_COMPLIANCE_EXCLUDE = ['lgl_snths_sanction']; // Overall은 토글 헤더에 표시
|
const SHIP_COMPLIANCE_EXCLUDE = ['lgl_snths_sanction']; // Overall은 토글 헤더에 표시
|
||||||
const COMPANY_COMPLIANCE_EXCLUDE = ['company_snths_compliance_status']; // Overall은 토글 헤더에 표시
|
const COMPANY_COMPLIANCE_EXCLUDE = ['company_snths_compliance_status']; // Overall은 토글 헤더에 표시
|
||||||
|
|
||||||
function ComplianceStatusItem({ item, isCompany }: { item: IndicatorStatusResponse; isCompany: boolean }) {
|
function ComplianceStatusItem({ item, isCompany, lang = 'EN' }: { item: IndicatorStatusResponse; isCompany: boolean; lang?: string }) {
|
||||||
const overrideLabel = getComplianceLabel(item, isCompany);
|
const isNoData = isNoDataValue(item.value);
|
||||||
|
const overrideLabel = isNoData ? t(lang, 'noData') : getComplianceLabel(item, isCompany);
|
||||||
const status = overrideLabel ? null : COMPLIANCE_STATUS[item.value ?? ''];
|
const status = overrideLabel ? null : COMPLIANCE_STATUS[item.value ?? ''];
|
||||||
const displayLabel = overrideLabel || (status ? status.label : (item.value ?? '-'));
|
const displayLabel = overrideLabel || (status ? status.label : (item.value ?? '-'));
|
||||||
const displayClass = overrideLabel
|
const displayClass = isNoData
|
||||||
? 'bg-wing-surface text-wing-muted border border-wing-border'
|
? 'bg-gray-100 text-gray-500 border border-gray-300'
|
||||||
: status ? status.className : 'bg-wing-surface text-wing-muted border border-wing-border';
|
: 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 (
|
return (
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<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');
|
const [activeTab, setActiveTab] = useState('sanctions');
|
||||||
|
|
||||||
// 제외 항목 필터링
|
// 제외 항목 필터링
|
||||||
@ -240,7 +266,7 @@ function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResp
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{catItems.map((item) => (
|
{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>
|
||||||
@ -302,14 +328,14 @@ function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResp
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{catItems.map((item) => (
|
{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>
|
||||||
))}
|
))}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
@ -329,7 +355,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
const [complianceStatusCache, setComplianceStatusCache] = useState<Record<string, IndicatorStatusResponse[]>>({});
|
const [complianceStatusCache, setComplianceStatusCache] = useState<Record<string, IndicatorStatusResponse[]>>({});
|
||||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['info', 'risk', 'compliance', 'history']));
|
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 isRisk = historyType === 'ship-risk';
|
||||||
const data = cache[lang] ?? [];
|
const data = cache[lang] ?? [];
|
||||||
const riskStatus = riskStatusCache[lang] ?? [];
|
const riskStatus = riskStatusCache[lang] ?? [];
|
||||||
@ -465,17 +491,17 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
{/* 이력 유형 선택 (언더라인 탭) */}
|
{/* 이력 유형 선택 (언더라인 탭) */}
|
||||||
<div className="border-b border-wing-border">
|
<div className="border-b border-wing-border">
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
{HISTORY_TYPES.map((t) => (
|
{HISTORY_TYPES.map((ht) => (
|
||||||
<button
|
<button
|
||||||
key={t.key}
|
key={ht.key}
|
||||||
onClick={() => handleTypeChange(t.key)}
|
onClick={() => handleTypeChange(ht.key)}
|
||||||
className={`pb-2.5 text-sm font-semibold transition-colors border-b-2 -mb-px ${
|
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-blue-600 border-blue-600'
|
||||||
: 'text-wing-muted border-transparent hover:text-wing-text'
|
: 'text-wing-muted border-transparent hover:text-wing-text'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t(lang, ht.labelKey)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -499,7 +525,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
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
|
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"
|
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}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 에러 */}
|
{/* 에러 */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-300 rounded-xl p-4 text-red-800 text-sm">
|
<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>
|
</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-xs transition-transform ${expandedSections.has('info') ? 'rotate-90' : ''}`}>▶</span>
|
||||||
<span className="text-sm font-semibold text-wing-text">
|
<span className="text-sm font-semibold text-wing-text">
|
||||||
{shipInfo ? '선박 기본 정보' : '회사 기본 정보'}
|
{shipInfo ? t(lang, 'shipBasicInfo') : t(lang, 'companyBasicInfo')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{expandedSections.has('info') && (
|
{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-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<span className="font-medium text-wing-text">
|
||||||
{countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
|
{countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{shipInfo.shipType || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{shipInfo.gt || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{shipInfo.buildYear || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -621,7 +647,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
{companyInfo.parentCompanyName && (
|
{companyInfo.parentCompanyName && (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{companyInfo.parentCompanyName}</span>
|
||||||
</div>
|
</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-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<span className="font-medium text-wing-text">
|
||||||
{countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
|
{countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{companyInfo.controlCountry && (
|
{companyInfo.controlCountry && (
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<span className="font-medium text-wing-text">
|
||||||
{countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
|
{countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
|
||||||
</span>
|
</span>
|
||||||
@ -649,25 +675,25 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
)}
|
)}
|
||||||
{companyInfo.foundedDate && (
|
{companyInfo.foundedDate && (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{companyInfo.foundedDate}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{companyInfo.email && (
|
{companyInfo.email && (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text truncate">{companyInfo.email}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{companyInfo.phone && (
|
{companyInfo.phone && (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-medium text-wing-text">{companyInfo.phone}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{companyInfo.website && (
|
{companyInfo.website && (
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<a
|
||||||
href={companyInfo.website.startsWith('http') ? companyInfo.website : `https://${companyInfo.website}`}
|
href={companyInfo.website.startsWith('http') ? companyInfo.website : `https://${companyInfo.website}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -689,16 +715,24 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
{/* Section 2: Current Risk Indicators (선박 탭만) */}
|
{/* Section 2: Current Risk Indicators (선박 탭만) */}
|
||||||
{riskStatus.length > 0 && (
|
{riskStatus.length > 0 && (
|
||||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||||
<button
|
<div className="flex items-center">
|
||||||
onClick={() => toggleSection('risk')}
|
<button
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
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">Current Risk Indicators</span>
|
<span className={`text-xs transition-transform ${expandedSections.has('risk') ? 'rotate-90' : ''}`}>▶</span>
|
||||||
</button>
|
<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') && (
|
{expandedSections.has('risk') && (
|
||||||
<div className="border-t border-wing-border px-4 py-4">
|
<div className="border-t border-wing-border px-4 py-4">
|
||||||
<RiskStatusGrid items={riskStatus} />
|
<RiskStatusGrid items={riskStatus} lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -712,19 +746,27 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||||
<button
|
<div className="flex items-center">
|
||||||
onClick={() => toggleSection('compliance')}
|
<button
|
||||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
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">Current Compliance</span>
|
<span className={`text-xs transition-transform ${expandedSections.has('compliance') ? 'rotate-90' : ''}`}>▶</span>
|
||||||
{overallItem && (
|
<span className="text-sm font-semibold text-wing-text">{t(lang, 'currentCompliance')}</span>
|
||||||
<StatusBadge value={overallItem.value} />
|
{overallItem && (
|
||||||
)}
|
<StatusBadge value={overallItem.value} lang={lang} />
|
||||||
</button>
|
)}
|
||||||
|
</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') && (
|
{expandedSections.has('compliance') && (
|
||||||
<div className="border-t border-wing-border px-4 py-4">
|
<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>
|
||||||
)}
|
)}
|
||||||
</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"
|
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-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">
|
<span className="text-xs text-wing-muted">
|
||||||
{grouped.length}개 일시, {data.length}건 변동
|
{grouped.length}{t(lang, 'dateCount')}, {data.length}{t(lang, 'changeCount')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{expandedSections.has('history') && (
|
{expandedSections.has('history') && (
|
||||||
@ -779,19 +821,19 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
{hasOverall ? (
|
{hasOverall ? (
|
||||||
<div className="flex items-center gap-1.5">
|
<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>
|
<span className="text-xs text-wing-muted">→</span>
|
||||||
<StatusBadge value={group.overallAfter} />
|
<StatusBadge value={group.overallAfter} lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-wing-muted">
|
<span className="text-xs text-wing-muted">
|
||||||
{displayItems.length}건 변동
|
{displayItems.length}{t(lang, 'changeCount')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasOverall && (
|
{hasOverall && (
|
||||||
<span className="text-xs text-wing-muted">
|
<span className="text-xs text-wing-muted">
|
||||||
{displayItems.length}건 변동
|
{displayItems.length}{t(lang, 'changeCount')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@ -804,19 +846,19 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
style={{ width: '50%' }}
|
style={{ width: '50%' }}
|
||||||
className="px-4 py-2 text-center font-semibold"
|
className="px-4 py-2 text-center font-semibold"
|
||||||
>
|
>
|
||||||
필드명
|
{t(lang, 'colFieldName')}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
className="px-4 py-2 text-center font-semibold"
|
className="px-4 py-2 text-center font-semibold"
|
||||||
>
|
>
|
||||||
이전값
|
{t(lang, 'colBefore')}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
className="px-4 py-2 text-center font-semibold"
|
className="px-4 py-2 text-center font-semibold"
|
||||||
>
|
>
|
||||||
이후값
|
{t(lang, 'colAfter')}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -845,7 +887,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
className="px-4 py-2.5 text-center"
|
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>
|
||||||
<td
|
<td
|
||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
@ -854,6 +896,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
<RiskValueCell
|
<RiskValueCell
|
||||||
value={row.afterValue}
|
value={row.afterValue}
|
||||||
narrative={row.narrative ?? undefined}
|
narrative={row.narrative ?? undefined}
|
||||||
|
lang={lang}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</>
|
</>
|
||||||
@ -863,13 +906,13 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
|
|||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
className="px-4 py-2.5 text-center"
|
className="px-4 py-2.5 text-center"
|
||||||
>
|
>
|
||||||
<StatusBadge value={row.beforeValue} />
|
<StatusBadge value={row.beforeValue} lang={lang} />
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
className="px-4 py-2.5 text-center"
|
className="px-4 py-2.5 text-center"
|
||||||
>
|
>
|
||||||
<StatusBadge value={row.afterValue} />
|
<StatusBadge value={row.afterValue} lang={lang} />
|
||||||
</td>
|
</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="flex items-center justify-center py-8 text-wing-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xl mb-1">📭</div>
|
<div className="text-xl mb-1">📭</div>
|
||||||
<div className="text-sm">변경 이력이 없습니다.</div>
|
<div className="text-sm">{t(lang, 'noChangeHistory')}</div>
|
||||||
</div>
|
</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="flex items-center justify-center py-16 text-wing-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl mb-3">🔍</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { screeningGuideApi, type MethodologyHistoryResponse } from '../../api/screeningGuideApi';
|
import { screeningGuideApi, type MethodologyHistoryResponse } from '../../api/screeningGuideApi';
|
||||||
|
import { t } from '../../constants/screeningTexts';
|
||||||
|
|
||||||
interface MethodologyTabProps {
|
interface MethodologyTabProps {
|
||||||
lang: string;
|
lang: string;
|
||||||
@ -30,7 +31,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
const [banner, setBanner] = useState<string>('');
|
const [banner, setBanner] = useState<string>('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 cache = useRef<Map<LangKey, LangCache>>(new Map());
|
||||||
|
|
||||||
const fetchData = useCallback((fetchLang: string) => {
|
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 uniqueTypes = Array.from(new Set(history.map((h) => h.changeType)));
|
||||||
|
|
||||||
const filtered =
|
const filtered =
|
||||||
selectedType === '전체'
|
selectedType === 'ALL'
|
||||||
? sortedHistory
|
? sortedHistory
|
||||||
: sortedHistory.filter((h) => h.changeType === selectedType);
|
: 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="flex items-center justify-center py-20 text-wing-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl mb-2">⏳</div>
|
<div className="text-2xl mb-2">⏳</div>
|
||||||
<div className="text-sm">데이터를 불러오는 중...</div>
|
<div className="text-sm">{t(lang, 'loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -99,7 +100,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -116,14 +117,14 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
{/* 변경 유형 필터 */}
|
{/* 변경 유형 필터 */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedType('전체')}
|
onClick={() => setSelectedType('ALL')}
|
||||||
className={`px-3 py-1 rounded-full text-[11px] font-bold transition-colors border ${
|
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-slate-900 text-white border-slate-900'
|
||||||
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
전체 ({history.length})
|
{t(lang, 'all')} ({history.length})
|
||||||
</button>
|
</button>
|
||||||
{uniqueTypes.map((type) => {
|
{uniqueTypes.map((type) => {
|
||||||
const count = history.filter((h) => h.changeType === type).length;
|
const count = history.filter((h) => h.changeType === type).length;
|
||||||
@ -132,7 +133,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={type}
|
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"
|
className="px-3 py-1 rounded-full text-[11px] font-bold transition-all border"
|
||||||
style={{
|
style={{
|
||||||
background: isActive ? hex : undefined,
|
background: isActive ? hex : undefined,
|
||||||
@ -147,7 +148,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-wing-muted">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* 타임라인 목록 */}
|
{/* 타임라인 목록 */}
|
||||||
@ -156,7 +157,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
|||||||
<table className="w-full text-xs border-collapse">
|
<table className="w-full text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-slate-900 text-white">
|
<tr className="bg-slate-900 text-white">
|
||||||
{['날짜', '변경 유형', '설명'].map((h) => (
|
{[t(lang, 'colDate'), t(lang, 'colChangeType'), t(lang, 'colDescription')].map((h) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
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 && (
|
{filtered.length === 0 && (
|
||||||
<div className="text-center py-12 text-wing-muted text-sm">
|
<div className="text-center py-12 text-wing-muted text-sm">
|
||||||
해당 유형의 변경 이력이 없습니다.
|
{t(lang, 'noMethodologyHistory')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { screeningGuideApi, type RiskCategoryResponse, type RiskIndicatorResponse } from '../../api/screeningGuideApi';
|
import { screeningGuideApi, type RiskCategoryResponse, type RiskIndicatorResponse } from '../../api/screeningGuideApi';
|
||||||
|
import { t } from '../../constants/screeningTexts';
|
||||||
|
|
||||||
interface RiskTabProps {
|
interface RiskTabProps {
|
||||||
lang: string;
|
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="flex items-center justify-center py-20 text-wing-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl mb-2">⏳</div>
|
<div className="text-2xl mb-2">⏳</div>
|
||||||
<div className="text-sm">데이터를 불러오는 중...</div>
|
<div className="text-sm">{t(lang, 'loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -84,7 +85,7 @@ export default function RiskTab({ lang }: RiskTabProps) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
<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>
|
</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 HistoryTab from '../components/screening/HistoryTab';
|
||||||
|
import { t } from '../constants/screeningTexts';
|
||||||
|
|
||||||
|
const LANG_KEY = 'screening-lang';
|
||||||
|
|
||||||
export default function RiskComplianceHistory() {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 헤더 + 언어 토글 */}
|
{/* 헤더 + 언어 토글 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="mt-1 text-sm text-wing-muted">
|
||||||
S&P 위험 지표 및 규정 준수 값 변경 이력
|
{t(lang, 'changeHistorySubtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex border border-wing-border rounded-lg overflow-hidden shrink-0">
|
<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 RiskTab from '../components/screening/RiskTab';
|
||||||
import ComplianceTab from '../components/screening/ComplianceTab';
|
import ComplianceTab from '../components/screening/ComplianceTab';
|
||||||
import MethodologyTab from '../components/screening/MethodologyTab';
|
import MethodologyTab from '../components/screening/MethodologyTab';
|
||||||
|
import { t } from '../constants/screeningTexts';
|
||||||
|
|
||||||
type ActiveTab = 'compliance' | 'risk' | 'methodology';
|
type ActiveTab = 'risk' | 'ship-compliance' | 'company-compliance' | 'methodology';
|
||||||
|
const VALID_TABS: ActiveTab[] = ['risk', 'ship-compliance', 'company-compliance', 'methodology'];
|
||||||
const TABS: { key: ActiveTab; label: string }[] = [
|
const LANG_KEY = 'screening-lang';
|
||||||
{ key: 'compliance', label: 'Compliance' },
|
|
||||||
{ key: 'risk', label: 'Risk Indicators' },
|
|
||||||
{ key: 'methodology', label: 'Methodology History' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ScreeningGuide() {
|
export default function ScreeningGuide() {
|
||||||
const [activeTab, setActiveTab] = useState<ActiveTab>('compliance');
|
const [searchParams] = useSearchParams();
|
||||||
const [lang, setLang] = useState('EN');
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -21,10 +34,10 @@ export default function ScreeningGuide() {
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-wing-text">
|
<h1 className="text-2xl font-bold text-wing-text">
|
||||||
Risk & Compliance Screening Guide
|
{t(lang, 'screeningGuideTitle')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-wing-muted">
|
<p className="mt-1 text-sm text-wing-muted">
|
||||||
S&P Risk Indicators and Regulatory Compliance Screening Guide
|
{t(lang, 'screeningGuideSubtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* 언어 토글 */}
|
{/* 언어 토글 */}
|
||||||
@ -48,7 +61,7 @@ export default function ScreeningGuide() {
|
|||||||
{/* 언더라인 탭 */}
|
{/* 언더라인 탭 */}
|
||||||
<div className="border-b border-wing-border">
|
<div className="border-b border-wing-border">
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
{TABS.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
onClick={() => setActiveTab(tab.key)}
|
onClick={() => setActiveTab(tab.key)}
|
||||||
@ -65,8 +78,9 @@ export default function ScreeningGuide() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 탭 내용 */}
|
{/* 탭 내용 */}
|
||||||
{activeTab === 'compliance' && <ComplianceTab lang={lang} />}
|
|
||||||
{activeTab === 'risk' && <RiskTab 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} />}
|
{activeTab === 'methodology' && <MethodologyTab lang={lang} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user