From 5f7708962d2e596b2528ccaf07f69b79ecefa636 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Thu, 2 Apr 2026 11:14:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(screening):=20Risk=20&=20Compliance=20?= =?UTF-8?q?=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=A7=80=EC=9B=90=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=8E=B8=EC=9D=98=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UI 고정 텍스트 다국어 메타 파일(screeningTexts.ts) 추가 - -999/null 값 'No Data'/'데이터 없음' 표시 처리 - Screening Guide 탭 분리 (Ship/Company Compliance) - Change History ↔ Screening Guide 간 언어 설정 공유 - 섹션 헤더에 Screening Guide 연결 링크 추가 --- .../components/screening/ComplianceTab.tsx | 44 ++-- .../src/components/screening/HistoryTab.tsx | 209 +++++++++++------- .../components/screening/MethodologyTab.tsx | 23 +- frontend/src/components/screening/RiskTab.tsx | 5 +- frontend/src/constants/screeningTexts.ts | 170 ++++++++++++++ frontend/src/pages/RiskComplianceHistory.tsx | 15 +- frontend/src/pages/ScreeningGuide.tsx | 42 ++-- 7 files changed, 374 insertions(+), 134 deletions(-) create mode 100644 frontend/src/constants/screeningTexts.ts diff --git a/frontend/src/components/screening/ComplianceTab.tsx b/frontend/src/components/screening/ComplianceTab.tsx index 2fa8635..f443d90 100644 --- a/frontend/src/components/screening/ComplianceTab.tsx +++ b/frontend/src/components/screening/ComplianceTab.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [indicatorType, setIndicatorType] = useState('SHIP'); + const [indicatorType, setIndicatorType] = useState(fixedType ?? 'SHIP'); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const cache = useRef>(new Map()); @@ -93,35 +95,37 @@ export default function ComplianceTab({ lang }: ComplianceTabProps) { return (
- {/* Ship / Company 토글 */} -
- {(['SHIP', 'COMPANY'] as const).map((type) => ( - - ))} -
+ {/* Ship / Company 토글 (fixedType이 없을 때만 표시) */} + {!fixedType && ( +
+ {(['SHIP', 'COMPANY'] as const).map((type) => ( + + ))} +
+ )} {loading && (
-
데이터를 불러오는 중...
+
{t(lang, 'loading')}
)} {error && (
- 데이터 로딩 실패: {error} + {t(lang, 'loadError')} {error}
)} diff --git a/frontend/src/components/screening/HistoryTab.tsx b/frontend/src/components/screening/HistoryTab.tsx index dcbfbd8..1e2a3df 100644 --- a/frontend/src/components/screening/HistoryTab.tsx +++ b/frontend/src/components/screening/HistoryTab.tsx @@ -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 = { '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 ( + + {t(lang, 'noData')} + + ); + } const status = STATUS_MAP[value]; if (!status) return {value}; 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 ( +
+ + {t(lang, 'noData')} +
+ ); + } const color = STATUS_COLORS[value] ?? '#6b7280'; const label = narrative || STATUS_LABELS[value] || value; return ( @@ -98,7 +121,8 @@ const COMPLIANCE_STATUS: Record = '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[] }) {
{isNotMaintained && (
- Risk Data is not maintained for this vessel. Only the maintenance status is shown. + {t(lang, 'riskNotMaintained')}
)}
@@ -144,7 +168,7 @@ function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
{catItems.map((item) => { const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280'; - const label = getRiskLabel(item); + const label = getRiskLabel(item, lang); return (
{item.fieldName} @@ -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 (
@@ -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 )}
{catItems.map((item) => ( - + ))}
@@ -302,14 +328,14 @@ function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResp )}
{catItems.map((item) => ( - + ))}
))}
) : ( -
해당 항목이 없습니다.
+
{t(lang, 'noItems')}
)}
); @@ -329,7 +355,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) { const [complianceStatusCache, setComplianceStatusCache] = useState>({}); const [expandedSections, setExpandedSections] = useState>(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) { {/* 이력 유형 선택 (언더라인 탭) */}
- {HISTORY_TYPES.map((t) => ( + {HISTORY_TYPES.map((ht) => ( ))}
@@ -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')}
{/* 에러 */} {error && (
- 조회 실패: {error} + {t(lang, 'loadError')} {error}
)} @@ -542,7 +568,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) { > - {shipInfo ? '선박 기본 정보' : '회사 기본 정보'} + {shipInfo ? t(lang, 'shipBasicInfo') : t(lang, 'companyBasicInfo')} {expandedSections.has('info') && ( @@ -576,13 +602,13 @@ export default function HistoryTab({ lang }: HistoryTabProps) { {/* 우측: 스펙 정보 */}
- 국적 + {t(lang, 'nationality')} {countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
- 선종 + {t(lang, 'shipType')} {shipInfo.shipType || '-'}
@@ -594,7 +620,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) { {shipInfo.gt || '-'}
- 건조연도 + {t(lang, 'buildYear')} {shipInfo.buildYear || '-'}
@@ -621,7 +647,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
{companyInfo.parentCompanyName && (
- 모회사 + {t(lang, 'parentCompany')} {companyInfo.parentCompanyName}
)} @@ -634,14 +660,14 @@ export default function HistoryTab({ lang }: HistoryTabProps) { {/* 우측: 상세 정보 */}
- 등록국가 + {t(lang, 'regCountry')} {countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
{companyInfo.controlCountry && (
- 관리국가 + {t(lang, 'ctrlCountry')} {countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry} @@ -649,25 +675,25 @@ export default function HistoryTab({ lang }: HistoryTabProps) { )} {companyInfo.foundedDate && (
- 설립일 + {t(lang, 'foundedDate')} {companyInfo.foundedDate}
)} {companyInfo.email && (
- 이메일 + {t(lang, 'email')} {companyInfo.email}
)} {companyInfo.phone && (
- 전화 + {t(lang, 'phone')} {companyInfo.phone}
)} {companyInfo.website && (
- 웹사이트 + {t(lang, 'website')} 0 && (
- +
+ + + {t(lang, 'viewGuide')} → + +
{expandedSections.has('risk') && (
- +
)}
@@ -712,19 +746,27 @@ export default function HistoryTab({ lang }: HistoryTabProps) { : null; return (
- +
+ + + {t(lang, 'viewGuide')} → + +
{expandedSections.has('compliance') && (
- +
)}
@@ -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" > - 값 변경 이력 + {t(lang, 'valueChangeHistory')} - {grouped.length}개 일시, {data.length}건 변동 + {grouped.length}{t(lang, 'dateCount')}, {data.length}{t(lang, 'changeCount')} {expandedSections.has('history') && ( @@ -779,19 +821,19 @@ export default function HistoryTab({ lang }: HistoryTabProps) { {hasOverall ? (
- + - +
) : ( - {displayItems.length}건 변동 + {displayItems.length}{t(lang, 'changeCount')} )}
{hasOverall && ( - {displayItems.length}건 변동 + {displayItems.length}{t(lang, 'changeCount')} )} @@ -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')} - 이전값 + {t(lang, 'colBefore')} - 이후값 + {t(lang, 'colAfter')} @@ -845,7 +887,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) { style={{ width: '25%' }} className="px-4 py-2.5 text-center" > - + @@ -863,13 +906,13 @@ export default function HistoryTab({ lang }: HistoryTabProps) { style={{ width: '25%' }} className="px-4 py-2.5 text-center" > - + - + )} @@ -887,7 +930,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
📭
-
변경 이력이 없습니다.
+
{t(lang, 'noChangeHistory')}
)} @@ -902,7 +945,7 @@ export default function HistoryTab({ lang }: HistoryTabProps) {
🔍
-
{currentType.searchLabel}을(를) 입력하고 조회하세요.
+
{t(lang, 'enterSearchKey').replace('{label}', t(lang, currentType.searchLabelKey))}
)} diff --git a/frontend/src/components/screening/MethodologyTab.tsx b/frontend/src/components/screening/MethodologyTab.tsx index cca8945..c99db88 100644 --- a/frontend/src/components/screening/MethodologyTab.tsx +++ b/frontend/src/components/screening/MethodologyTab.tsx @@ -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(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [selectedType, setSelectedType] = useState('전체'); + const [selectedType, setSelectedType] = useState('ALL'); const cache = useRef>(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) {
-
데이터를 불러오는 중...
+
{t(lang, 'loading')}
); @@ -99,7 +100,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) { if (error) { return (
- 데이터 로딩 실패: {error} + {t(lang, 'loadError')} {error}
); } @@ -116,14 +117,14 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) { {/* 변경 유형 필터 */}
{uniqueTypes.map((type) => { const count = history.filter((h) => h.changeType === type).length; @@ -132,7 +133,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) { return (
- 표시: {filtered.length}건 | 최신순 정렬 + {t(lang, 'showing')} {filtered.length}{t(lang, 'unit')} {t(lang, 'sortLatest')}
{/* 타임라인 목록 */} @@ -156,7 +157,7 @@ export default function MethodologyTab({ lang }: MethodologyTabProps) { - {['날짜', '변경 유형', '설명'].map((h) => ( + {[t(lang, 'colDate'), t(lang, 'colChangeType'), t(lang, 'colDescription')].map((h) => (
- 해당 유형의 변경 이력이 없습니다. + {t(lang, 'noMethodologyHistory')} )} diff --git a/frontend/src/components/screening/RiskTab.tsx b/frontend/src/components/screening/RiskTab.tsx index d8b6c13..f3b336b 100644 --- a/frontend/src/components/screening/RiskTab.tsx +++ b/frontend/src/components/screening/RiskTab.tsx @@ -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) {
-
데이터를 불러오는 중...
+
{t(lang, 'loading')}
); @@ -84,7 +85,7 @@ export default function RiskTab({ lang }: RiskTabProps) { if (error) { return (
- 데이터 로딩 실패: {error} + {t(lang, 'loadError')} {error}
); } diff --git a/frontend/src/constants/screeningTexts.ts b/frontend/src/constants/screeningTexts.ts new file mode 100644 index 0000000..fcd4e7c --- /dev/null +++ b/frontend/src/constants/screeningTexts.ts @@ -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 = { 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; +} diff --git a/frontend/src/pages/RiskComplianceHistory.tsx b/frontend/src/pages/RiskComplianceHistory.tsx index 9f72f15..08d32ee 100644 --- a/frontend/src/pages/RiskComplianceHistory.tsx +++ b/frontend/src/pages/RiskComplianceHistory.tsx @@ -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 (
{/* 헤더 + 언어 토글 */}
-

Risk & Compliance Change History

+

{t(lang, 'changeHistoryTitle')}

- S&P 위험 지표 및 규정 준수 값 변경 이력 + {t(lang, 'changeHistorySubtitle')}

diff --git a/frontend/src/pages/ScreeningGuide.tsx b/frontend/src/pages/ScreeningGuide.tsx index 66b9a46..cc7a802 100644 --- a/frontend/src/pages/ScreeningGuide.tsx +++ b/frontend/src/pages/ScreeningGuide.tsx @@ -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('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(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 (
@@ -21,10 +34,10 @@ export default function ScreeningGuide() {

- Risk & Compliance Screening Guide + {t(lang, 'screeningGuideTitle')}

- S&P Risk Indicators and Regulatory Compliance Screening Guide + {t(lang, 'screeningGuideSubtitle')}

{/* 언어 토글 */} @@ -48,7 +61,7 @@ export default function ScreeningGuide() { {/* 언더라인 탭 */}
- {TABS.map((tab) => ( + {tabs.map((tab) => (
{/* 탭 내용 */} - {activeTab === 'compliance' && } {activeTab === 'risk' && } + {activeTab === 'ship-compliance' && } + {activeTab === 'company-compliance' && } {activeTab === 'methodology' && }
);