feat: Risk&Compliance Screening Guide 페이지 생성 (#109) #113
@ -15,6 +15,7 @@ const RecollectDetail = lazy(() => import('./pages/RecollectDetail'));
|
|||||||
const Schedules = lazy(() => import('./pages/Schedules'));
|
const Schedules = lazy(() => import('./pages/Schedules'));
|
||||||
const Timeline = lazy(() => import('./pages/Timeline'));
|
const Timeline = lazy(() => import('./pages/Timeline'));
|
||||||
const BypassConfig = lazy(() => import('./pages/BypassConfig'));
|
const BypassConfig = lazy(() => import('./pages/BypassConfig'));
|
||||||
|
const ScreeningGuide = lazy(() => import('./pages/ScreeningGuide'));
|
||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
const { toasts, removeToast } = useToastContext();
|
const { toasts, removeToast } = useToastContext();
|
||||||
@ -34,6 +35,7 @@ function AppLayout() {
|
|||||||
<Route path="/schedules" element={<Schedules />} />
|
<Route path="/schedules" element={<Schedules />} />
|
||||||
<Route path="/schedule-timeline" element={<Timeline />} />
|
<Route path="/schedule-timeline" element={<Timeline />} />
|
||||||
<Route path="/bypass-config" element={<BypassConfig />} />
|
<Route path="/bypass-config" element={<BypassConfig />} />
|
||||||
|
<Route path="/screening-guide" element={<ScreeningGuide />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
71
frontend/src/api/screeningGuideApi.ts
Normal file
71
frontend/src/api/screeningGuideApi.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// API 응답 타입
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Risk 지표 타입
|
||||||
|
export interface RiskIndicatorResponse {
|
||||||
|
indicatorId: number;
|
||||||
|
fieldKey: string;
|
||||||
|
fieldName: string;
|
||||||
|
description: string;
|
||||||
|
conditionRed: string;
|
||||||
|
conditionAmber: string;
|
||||||
|
conditionGreen: string;
|
||||||
|
dataType: string;
|
||||||
|
collectionNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskCategoryResponse {
|
||||||
|
categoryCode: string;
|
||||||
|
categoryName: string;
|
||||||
|
indicators: RiskIndicatorResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compliance 지표 타입
|
||||||
|
export interface ComplianceIndicatorResponse {
|
||||||
|
indicatorId: number;
|
||||||
|
fieldKey: string;
|
||||||
|
fieldName: string;
|
||||||
|
description: string;
|
||||||
|
conditionRed: string;
|
||||||
|
conditionAmber: string;
|
||||||
|
conditionGreen: string;
|
||||||
|
dataType: string;
|
||||||
|
collectionNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceCategoryResponse {
|
||||||
|
category: string;
|
||||||
|
indicatorType: string;
|
||||||
|
indicators: ComplianceIndicatorResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방법론 변경 이력 타입
|
||||||
|
export interface MethodologyHistoryResponse {
|
||||||
|
historyId: number;
|
||||||
|
changeDate: string;
|
||||||
|
changeType: string;
|
||||||
|
updateTitle: string;
|
||||||
|
description: string;
|
||||||
|
collectionNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE = '/snp-api/api/screening-guide';
|
||||||
|
|
||||||
|
async function fetchJson<T>(url: string): Promise<T> {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`API Error: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const screeningGuideApi = {
|
||||||
|
getRiskIndicators: (lang = 'KO') =>
|
||||||
|
fetchJson<ApiResponse<RiskCategoryResponse[]>>(`${BASE}/risk-indicators?lang=${lang}`),
|
||||||
|
getComplianceIndicators: (lang = 'KO', type = 'SHIP') =>
|
||||||
|
fetchJson<ApiResponse<ComplianceCategoryResponse[]>>(`${BASE}/compliance-indicators?lang=${lang}&type=${type}`),
|
||||||
|
getMethodologyHistory: (lang = 'KO') =>
|
||||||
|
fetchJson<ApiResponse<MethodologyHistoryResponse[]>>(`${BASE}/methodology-history?lang=${lang}`),
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@ const navItems = [
|
|||||||
{ path: '/schedules', label: '스케줄', icon: '🕐' },
|
{ path: '/schedules', label: '스케줄', icon: '🕐' },
|
||||||
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
||||||
{ path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
|
{ path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
|
||||||
|
{ path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
|||||||
385
frontend/src/components/screening/ComplianceTab.tsx
Normal file
385
frontend/src/components/screening/ComplianceTab.tsx
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
screeningGuideApi,
|
||||||
|
type ComplianceCategoryResponse,
|
||||||
|
type ComplianceIndicatorResponse,
|
||||||
|
} from '../../api/screeningGuideApi';
|
||||||
|
|
||||||
|
interface ComplianceTabProps {
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewMode = 'table' | 'card';
|
||||||
|
type IndicatorType = 'SHIP' | 'COMPANY';
|
||||||
|
|
||||||
|
const SHIP_CAT_COLORS: Record<string, string> = {
|
||||||
|
'Sanctions – Ship (US OFAC)': '#1e3a5f',
|
||||||
|
'Sanctions – Ownership (US OFAC)': '#1d4ed8',
|
||||||
|
'Sanctions – Ship (Non-US)': '#065f46',
|
||||||
|
'Sanctions – Ownership (Non-US)': '#0f766e',
|
||||||
|
'Sanctions – FATF': '#6b21a8',
|
||||||
|
'Sanctions – Other': '#991b1b',
|
||||||
|
'Port Calls': '#065f46',
|
||||||
|
'STS Activity': '#0f766e',
|
||||||
|
'Dark Activity': '#374151',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPANY_CAT_COLORS: Record<string, string> = {
|
||||||
|
'Company Sanctions (US OFAC)': '#1e3a5f',
|
||||||
|
'Company Sanctions (Non-US)': '#065f46',
|
||||||
|
'Company Compliance': '#6b21a8',
|
||||||
|
'Company Risk': '#92400e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCatHex(categoryName: string, type: IndicatorType): string {
|
||||||
|
const map = type === 'SHIP' ? SHIP_CAT_COLORS : COMPANY_CAT_COLORS;
|
||||||
|
return map[categoryName] ?? '#374151';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlatRow {
|
||||||
|
category: string;
|
||||||
|
indicatorType: string;
|
||||||
|
indicator: ComplianceIndicatorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ComplianceTab({ lang }: ComplianceTabProps) {
|
||||||
|
const [categories, setCategories] = useState<ComplianceCategoryResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('전체');
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('table');
|
||||||
|
const [indicatorType, setIndicatorType] = useState<IndicatorType>('SHIP');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSelectedCategory('전체');
|
||||||
|
screeningGuideApi
|
||||||
|
.getComplianceIndicators(lang, indicatorType)
|
||||||
|
.then((res) => setCategories(res.data ?? []))
|
||||||
|
.catch((err: Error) => setError(err.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [lang, indicatorType]);
|
||||||
|
|
||||||
|
const flatRows: FlatRow[] = categories.flatMap((cat) =>
|
||||||
|
cat.indicators.map((ind) => ({
|
||||||
|
category: cat.category,
|
||||||
|
indicatorType: cat.indicatorType,
|
||||||
|
indicator: ind,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered: FlatRow[] =
|
||||||
|
selectedCategory === '전체'
|
||||||
|
? flatRows
|
||||||
|
: flatRows.filter((r) => r.category === selectedCategory);
|
||||||
|
|
||||||
|
const uniqueCategories = Array.from(new Set(flatRows.map((r) => r.category)));
|
||||||
|
|
||||||
|
function downloadCSV() {
|
||||||
|
const bom = '\uFEFF';
|
||||||
|
const headers = [
|
||||||
|
'카테고리',
|
||||||
|
'타입',
|
||||||
|
'필드키',
|
||||||
|
'필드명',
|
||||||
|
'설명',
|
||||||
|
'RED 조건',
|
||||||
|
'AMBER 조건',
|
||||||
|
'GREEN 조건',
|
||||||
|
'데이터 타입',
|
||||||
|
'이력 관리 참고사항',
|
||||||
|
];
|
||||||
|
const rows = flatRows.map((r) =>
|
||||||
|
[
|
||||||
|
r.category,
|
||||||
|
r.indicatorType,
|
||||||
|
r.indicator.fieldKey,
|
||||||
|
r.indicator.fieldName,
|
||||||
|
r.indicator.description,
|
||||||
|
r.indicator.conditionRed,
|
||||||
|
r.indicator.conditionAmber,
|
||||||
|
r.indicator.conditionGreen,
|
||||||
|
r.indicator.dataType,
|
||||||
|
r.indicator.collectionNote,
|
||||||
|
]
|
||||||
|
.map((v) => `"${(v ?? '').replace(/"/g, '""')}"`)
|
||||||
|
.join(','),
|
||||||
|
);
|
||||||
|
const csv = bom + [headers.join(','), ...rows].join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `MIRS_Compliance_${indicatorType}.csv`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* SHIP / COMPANY 토글 */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIndicatorType('SHIP')}
|
||||||
|
className={`px-5 py-2 rounded-lg text-sm font-bold transition-all border ${
|
||||||
|
indicatorType === 'SHIP'
|
||||||
|
? 'bg-slate-900 text-white border-slate-900 shadow-md'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
선박 컴플라이언스 (SHIP)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIndicatorType('COMPANY')}
|
||||||
|
className={`px-5 py-2 rounded-lg text-sm font-bold transition-all border ${
|
||||||
|
indicatorType === 'COMPANY'
|
||||||
|
? 'bg-slate-900 text-white border-slate-900 shadow-md'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
기업 컴플라이언스 (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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||||
|
<strong>데이터 로딩 실패:</strong> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<>
|
||||||
|
{/* 카테고리 요약 카드 */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{uniqueCategories.map((catName) => {
|
||||||
|
const count = flatRows.filter((r) => r.category === catName).length;
|
||||||
|
const isActive = selectedCategory === catName;
|
||||||
|
const hex = getCatHex(catName, indicatorType);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={catName}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedCategory(isActive ? '전체' : catName)
|
||||||
|
}
|
||||||
|
className="rounded-lg p-3 text-center cursor-pointer transition-all border-2 text-left"
|
||||||
|
style={{
|
||||||
|
background: isActive ? hex : undefined,
|
||||||
|
borderColor: isActive ? hex : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-xl font-bold ${isActive ? 'text-white' : 'text-wing-text'}`}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs font-semibold leading-tight mt-1 ${isActive ? 'text-white' : 'text-wing-muted'}`}
|
||||||
|
>
|
||||||
|
{catName}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컨트롤 바 */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCategory('전체')}
|
||||||
|
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-colors border ${
|
||||||
|
selectedCategory === '전체'
|
||||||
|
? 'bg-slate-900 text-white border-slate-900'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
전체 ({flatRows.length})
|
||||||
|
</button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<span className="text-xs text-wing-muted">
|
||||||
|
표시:{' '}
|
||||||
|
<strong className="text-wing-text">{filtered.length}</strong>개 항목
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')}
|
||||||
|
className="px-3 py-1.5 rounded-lg border border-wing-border bg-wing-card text-wing-muted text-xs font-semibold hover:text-wing-text transition-colors"
|
||||||
|
>
|
||||||
|
{viewMode === 'table' ? '📋 카드 보기' : '📊 테이블 보기'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={downloadCSV}
|
||||||
|
className="px-4 py-1.5 rounded-lg bg-green-600 text-white text-xs font-bold hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
⬇ CSV 다운로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 뷰 */}
|
||||||
|
{viewMode === 'table' && (
|
||||||
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-900 text-white">
|
||||||
|
{[
|
||||||
|
'카테고리',
|
||||||
|
'필드명',
|
||||||
|
'설명',
|
||||||
|
'🔴 RED',
|
||||||
|
'🟡 AMBER',
|
||||||
|
'🟢 GREEN',
|
||||||
|
'데이터 타입',
|
||||||
|
'이력 관리 참고',
|
||||||
|
].map((h) => (
|
||||||
|
<th
|
||||||
|
key={h}
|
||||||
|
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const showCat =
|
||||||
|
i === 0 ||
|
||||||
|
filtered[i - 1].category !== row.category;
|
||||||
|
const hex = getCatHex(row.category, indicatorType);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`${row.indicator.indicatorId}-${i}`}
|
||||||
|
className="border-b border-wing-border align-top even:bg-wing-card"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2.5 min-w-[140px]">
|
||||||
|
{showCat && (
|
||||||
|
<span
|
||||||
|
className="inline-block text-white rounded px-2 py-0.5 text-[10px] font-bold"
|
||||||
|
style={{ background: hex }}
|
||||||
|
>
|
||||||
|
{row.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[160px]">
|
||||||
|
<div className="font-bold text-wing-text">
|
||||||
|
{row.indicator.fieldName}
|
||||||
|
</div>
|
||||||
|
<div className="text-wing-muted text-[10px] mt-0.5">
|
||||||
|
{row.indicator.fieldKey}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[220px] leading-relaxed text-wing-text">
|
||||||
|
{row.indicator.description}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-red-50 border border-red-300 rounded px-2 py-1 text-red-800">
|
||||||
|
{row.indicator.conditionRed}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-amber-50 border border-amber-300 rounded px-2 py-1 text-amber-800">
|
||||||
|
{row.indicator.conditionAmber}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-green-50 border border-green-300 rounded px-2 py-1 text-green-800">
|
||||||
|
{row.indicator.conditionGreen}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[110px] text-wing-muted">
|
||||||
|
{row.indicator.dataType}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[200px] text-blue-600 leading-relaxed">
|
||||||
|
{row.indicator.collectionNote &&
|
||||||
|
`💡 ${row.indicator.collectionNote}`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 뷰 */}
|
||||||
|
{viewMode === 'card' && (
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const hex = getCatHex(row.category, indicatorType);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${row.indicator.indicatorId}-${i}`}
|
||||||
|
className="bg-wing-surface rounded-xl shadow-md overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="px-4 py-2.5 flex justify-between items-center"
|
||||||
|
style={{ background: hex }}
|
||||||
|
>
|
||||||
|
<span className="text-white text-[10px] font-bold">
|
||||||
|
{row.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/75 text-[10px]">
|
||||||
|
{row.indicator.dataType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="font-bold text-sm text-wing-text mb-0.5">
|
||||||
|
{row.indicator.fieldName}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-wing-muted mb-3">
|
||||||
|
{row.indicator.fieldKey}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-wing-text leading-relaxed mb-3">
|
||||||
|
{row.indicator.description}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||||
|
<div className="bg-red-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-red-800 mb-1">
|
||||||
|
🔴 RED
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-red-800">
|
||||||
|
{row.indicator.conditionRed}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-amber-800 mb-1">
|
||||||
|
🟡 AMBER
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-amber-800">
|
||||||
|
{row.indicator.conditionAmber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-green-800 mb-1">
|
||||||
|
🟢 GREEN
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-green-800">
|
||||||
|
{row.indicator.conditionGreen}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{row.indicator.collectionNote && (
|
||||||
|
<div className="bg-blue-50 rounded-lg p-3 text-[11px] text-blue-700 leading-relaxed">
|
||||||
|
💡 {row.indicator.collectionNote}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
174
frontend/src/components/screening/MethodologyTab.tsx
Normal file
174
frontend/src/components/screening/MethodologyTab.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { screeningGuideApi, type MethodologyHistoryResponse } from '../../api/screeningGuideApi';
|
||||||
|
|
||||||
|
interface MethodologyTabProps {
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_TYPE_COLORS: Record<string, string> = {
|
||||||
|
Addition: '#065f46',
|
||||||
|
Update: '#1d4ed8',
|
||||||
|
Expansion: '#6b21a8',
|
||||||
|
Change: '#92400e',
|
||||||
|
Removal: '#991b1b',
|
||||||
|
New: '#0f766e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getChangeTypeColor(changeType: string): string {
|
||||||
|
return CHANGE_TYPE_COLORS[changeType] ?? '#374151';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MethodologyTab({ lang }: MethodologyTabProps) {
|
||||||
|
const [history, setHistory] = useState<MethodologyHistoryResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedType, setSelectedType] = useState('전체');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
screeningGuideApi
|
||||||
|
.getMethodologyHistory(lang)
|
||||||
|
.then((res) => setHistory(res.data ?? []))
|
||||||
|
.catch((err: Error) => setError(err.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [lang]);
|
||||||
|
|
||||||
|
const sortedHistory = [...history].sort((a, b) =>
|
||||||
|
b.changeDate.localeCompare(a.changeDate),
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueTypes = Array.from(new Set(history.map((h) => h.changeType)));
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
selectedType === '전체'
|
||||||
|
? sortedHistory
|
||||||
|
: sortedHistory.filter((h) => h.changeType === selectedType);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20 text-wing-muted">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl mb-2">⏳</div>
|
||||||
|
<div className="text-sm">데이터를 불러오는 중...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||||
|
<strong>데이터 로딩 실패:</strong> {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 주의사항 배너 */}
|
||||||
|
<div className="bg-amber-50 border border-amber-300 rounded-xl px-4 py-3 text-amber-800 text-xs leading-relaxed">
|
||||||
|
<strong>이력 관리 주의사항:</strong> 방법론 변경은 선박·기업의 컴플라이언스
|
||||||
|
상태값을 자동으로 변경시킵니다. 상태 변경이 실제 리스크 변화인지, 방법론
|
||||||
|
업데이트 때문인지 반드시 교차 확인해야 합니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 변경 유형 필터 */}
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedType('전체')}
|
||||||
|
className={`px-3 py-1 rounded-full text-[11px] font-bold transition-colors border ${
|
||||||
|
selectedType === '전체'
|
||||||
|
? 'bg-slate-900 text-white border-slate-900'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
전체 ({history.length})
|
||||||
|
</button>
|
||||||
|
{uniqueTypes.map((type) => {
|
||||||
|
const count = history.filter((h) => h.changeType === type).length;
|
||||||
|
const hex = getChangeTypeColor(type);
|
||||||
|
const isActive = selectedType === type;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setSelectedType(isActive ? '전체' : type)}
|
||||||
|
className="px-3 py-1 rounded-full text-[11px] font-bold transition-all border"
|
||||||
|
style={{
|
||||||
|
background: isActive ? hex : undefined,
|
||||||
|
borderColor: isActive ? hex : undefined,
|
||||||
|
color: isActive ? 'white' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type} ({count})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-wing-muted">
|
||||||
|
표시: <strong className="text-wing-text">{filtered.length}</strong>건 | 최신순 정렬
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타임라인 목록 */}
|
||||||
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-900 text-white">
|
||||||
|
{['날짜', '변경 유형', '제목', '설명', '참고사항'].map((h) => (
|
||||||
|
<th
|
||||||
|
key={h}
|
||||||
|
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const hex = getChangeTypeColor(row.changeType);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`${row.historyId}-${i}`}
|
||||||
|
className="border-b border-wing-border align-top even:bg-wing-card"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-3 min-w-[110px]">
|
||||||
|
<div className="font-bold text-wing-text">
|
||||||
|
{row.changeDate}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 min-w-[110px]">
|
||||||
|
<span
|
||||||
|
className="inline-block text-white rounded px-2 py-0.5 text-[10px] font-bold"
|
||||||
|
style={{ background: hex }}
|
||||||
|
>
|
||||||
|
{row.changeType}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 min-w-[180px] font-semibold text-wing-text leading-relaxed">
|
||||||
|
{row.updateTitle}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 min-w-[280px] leading-relaxed text-wing-text">
|
||||||
|
{row.description}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 min-w-[200px] text-blue-600 leading-relaxed">
|
||||||
|
{row.collectionNote && `💡 ${row.collectionNote}`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-wing-muted text-sm">
|
||||||
|
해당 유형의 변경 이력이 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
349
frontend/src/components/screening/RiskTab.tsx
Normal file
349
frontend/src/components/screening/RiskTab.tsx
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { screeningGuideApi, type RiskCategoryResponse, type RiskIndicatorResponse } from '../../api/screeningGuideApi';
|
||||||
|
|
||||||
|
interface RiskTabProps {
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewMode = 'table' | 'card';
|
||||||
|
|
||||||
|
const CAT_COLORS: Record<string, string> = {
|
||||||
|
'AIS': 'bg-blue-800',
|
||||||
|
'Port Calls': 'bg-emerald-800',
|
||||||
|
'Associated with Russia': 'bg-red-800',
|
||||||
|
'Behavioural Risk': 'bg-amber-800',
|
||||||
|
'Safety, Security & Inspections': 'bg-blue-600',
|
||||||
|
'Flag Risk': 'bg-purple-800',
|
||||||
|
'Owner & Classification': 'bg-teal-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CAT_HEX: Record<string, string> = {
|
||||||
|
'AIS': '#1e40af',
|
||||||
|
'Port Calls': '#065f46',
|
||||||
|
'Associated with Russia': '#991b1b',
|
||||||
|
'Behavioural Risk': '#92400e',
|
||||||
|
'Safety, Security & Inspections': '#1d4ed8',
|
||||||
|
'Flag Risk': '#6b21a8',
|
||||||
|
'Owner & Classification': '#0f766e',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCatColor(categoryName: string): string {
|
||||||
|
return CAT_COLORS[categoryName] ?? 'bg-slate-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatHex(categoryName: string): string {
|
||||||
|
return CAT_HEX[categoryName] ?? '#374151';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlatRow {
|
||||||
|
category: string;
|
||||||
|
indicator: RiskIndicatorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RiskTab({ lang }: RiskTabProps) {
|
||||||
|
const [categories, setCategories] = useState<RiskCategoryResponse[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('전체');
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('table');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
screeningGuideApi
|
||||||
|
.getRiskIndicators(lang)
|
||||||
|
.then((res) => setCategories(res.data ?? []))
|
||||||
|
.catch((err: Error) => setError(err.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [lang]);
|
||||||
|
|
||||||
|
const flatRows: FlatRow[] = categories.flatMap((cat) =>
|
||||||
|
cat.indicators.map((ind) => ({ category: cat.categoryName, indicator: ind })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered: FlatRow[] =
|
||||||
|
selectedCategory === '전체'
|
||||||
|
? flatRows
|
||||||
|
: flatRows.filter((r) => r.category === selectedCategory);
|
||||||
|
|
||||||
|
function downloadCSV() {
|
||||||
|
const bom = '\uFEFF';
|
||||||
|
const headers = [
|
||||||
|
'카테고리',
|
||||||
|
'필드키',
|
||||||
|
'필드명',
|
||||||
|
'설명',
|
||||||
|
'RED 조건',
|
||||||
|
'AMBER 조건',
|
||||||
|
'GREEN 조건',
|
||||||
|
'데이터 타입',
|
||||||
|
'이력 관리 참고사항',
|
||||||
|
];
|
||||||
|
const rows = flatRows.map((r) =>
|
||||||
|
[
|
||||||
|
r.category,
|
||||||
|
r.indicator.fieldKey,
|
||||||
|
r.indicator.fieldName,
|
||||||
|
r.indicator.description,
|
||||||
|
r.indicator.conditionRed,
|
||||||
|
r.indicator.conditionAmber,
|
||||||
|
r.indicator.conditionGreen,
|
||||||
|
r.indicator.dataType,
|
||||||
|
r.indicator.collectionNote,
|
||||||
|
]
|
||||||
|
.map((v) => `"${(v ?? '').replace(/"/g, '""')}"`)
|
||||||
|
.join(','),
|
||||||
|
);
|
||||||
|
const csv = bom + [headers.join(','), ...rows].join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = 'MIRS_Risk_Indicators.csv';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-20 text-wing-muted">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl mb-2">⏳</div>
|
||||||
|
<div className="text-sm">데이터를 불러오는 중...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-red-50 border border-red-300 rounded-xl p-6 text-red-800 text-sm">
|
||||||
|
<strong>데이터 로딩 실패:</strong> {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 카테고리 요약 카드 */}
|
||||||
|
<div className="grid grid-cols-4 gap-3 lg:grid-cols-7">
|
||||||
|
{categories.map((cat) => {
|
||||||
|
const isActive = selectedCategory === cat.categoryName;
|
||||||
|
const hex = getCatHex(cat.categoryName);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cat.categoryCode}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedCategory(isActive ? '전체' : cat.categoryName)
|
||||||
|
}
|
||||||
|
className="rounded-lg p-3 text-center cursor-pointer transition-all border-2"
|
||||||
|
style={{
|
||||||
|
background: isActive ? hex : undefined,
|
||||||
|
borderColor: isActive ? hex : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-xl font-bold ${isActive ? 'text-white' : 'text-wing-text'}`}
|
||||||
|
>
|
||||||
|
{cat.indicators.length}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`text-xs font-semibold leading-tight mt-1 ${isActive ? 'text-white' : 'text-wing-muted'}`}
|
||||||
|
>
|
||||||
|
{cat.categoryName}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컨트롤 바 */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCategory('전체')}
|
||||||
|
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-colors border ${
|
||||||
|
selectedCategory === '전체'
|
||||||
|
? 'bg-slate-900 text-white border-slate-900'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
전체 ({flatRows.length})
|
||||||
|
</button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<span className="text-xs text-wing-muted">
|
||||||
|
표시:{' '}
|
||||||
|
<strong className="text-wing-text">{filtered.length}</strong>개 항목
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')}
|
||||||
|
className="px-3 py-1.5 rounded-lg border border-wing-border bg-wing-card text-wing-muted text-xs font-semibold hover:text-wing-text transition-colors"
|
||||||
|
>
|
||||||
|
{viewMode === 'table' ? '📋 카드 보기' : '📊 테이블 보기'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={downloadCSV}
|
||||||
|
className="px-4 py-1.5 rounded-lg bg-green-600 text-white text-xs font-bold hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
⬇ CSV 다운로드
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 뷰 */}
|
||||||
|
{viewMode === 'table' && (
|
||||||
|
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-900 text-white">
|
||||||
|
{[
|
||||||
|
'카테고리',
|
||||||
|
'필드명',
|
||||||
|
'설명',
|
||||||
|
'🔴 RED',
|
||||||
|
'🟡 AMBER',
|
||||||
|
'🟢 GREEN',
|
||||||
|
'데이터 타입',
|
||||||
|
'이력 관리 참고',
|
||||||
|
].map((h) => (
|
||||||
|
<th
|
||||||
|
key={h}
|
||||||
|
className="px-3 py-2.5 text-left font-semibold whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const showCat =
|
||||||
|
i === 0 ||
|
||||||
|
filtered[i - 1].category !== row.category;
|
||||||
|
const hex = getCatHex(row.category);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`${row.indicator.indicatorId}-${i}`}
|
||||||
|
className="border-b border-wing-border align-top even:bg-wing-card"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2.5 min-w-[110px]">
|
||||||
|
{showCat && (
|
||||||
|
<span
|
||||||
|
className="inline-block text-white rounded px-2 py-0.5 text-[10px] font-bold"
|
||||||
|
style={{ background: hex }}
|
||||||
|
>
|
||||||
|
{row.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[160px]">
|
||||||
|
<div className="font-bold text-wing-text">
|
||||||
|
{row.indicator.fieldName}
|
||||||
|
</div>
|
||||||
|
<div className="text-wing-muted text-[10px] mt-0.5">
|
||||||
|
{row.indicator.fieldKey}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[220px] leading-relaxed text-wing-text">
|
||||||
|
{row.indicator.description}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-red-50 border border-red-300 rounded px-2 py-1 text-red-800">
|
||||||
|
{row.indicator.conditionRed}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-amber-50 border border-amber-300 rounded px-2 py-1 text-amber-800">
|
||||||
|
{row.indicator.conditionAmber}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[130px]">
|
||||||
|
<div className="bg-green-50 border border-green-300 rounded px-2 py-1 text-green-800">
|
||||||
|
{row.indicator.conditionGreen}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[110px] text-wing-muted">
|
||||||
|
{row.indicator.dataType}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2.5 min-w-[200px] text-blue-600 leading-relaxed">
|
||||||
|
{row.indicator.collectionNote &&
|
||||||
|
`💡 ${row.indicator.collectionNote}`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 뷰 */}
|
||||||
|
{viewMode === 'card' && (
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
{filtered.map((row, i) => {
|
||||||
|
const hex = getCatHex(row.category);
|
||||||
|
const colorClass = getCatColor(row.category);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${row.indicator.indicatorId}-${i}`}
|
||||||
|
className="bg-wing-surface rounded-xl shadow-md overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${colorClass} px-4 py-2.5 flex justify-between items-center`}
|
||||||
|
style={{ background: hex }}
|
||||||
|
>
|
||||||
|
<span className="text-white text-[10px] font-bold">
|
||||||
|
{row.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/75 text-[10px]">
|
||||||
|
{row.indicator.dataType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="font-bold text-sm text-wing-text mb-0.5">
|
||||||
|
{row.indicator.fieldName}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-wing-muted mb-3">
|
||||||
|
{row.indicator.fieldKey}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-wing-text leading-relaxed mb-3">
|
||||||
|
{row.indicator.description}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||||
|
<div className="bg-red-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-red-800 mb-1">
|
||||||
|
🔴 RED
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-red-800">
|
||||||
|
{row.indicator.conditionRed}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-amber-800 mb-1">
|
||||||
|
🟡 AMBER
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-amber-800">
|
||||||
|
{row.indicator.conditionAmber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 rounded-lg p-2">
|
||||||
|
<div className="text-[10px] font-bold text-green-800 mb-1">
|
||||||
|
🟢 GREEN
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-green-800">
|
||||||
|
{row.indicator.conditionGreen}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{row.indicator.collectionNote && (
|
||||||
|
<div className="bg-blue-50 rounded-lg p-3 text-[11px] text-blue-700 leading-relaxed">
|
||||||
|
💡 {row.indicator.collectionNote}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
frontend/src/pages/ScreeningGuide.tsx
Normal file
100
frontend/src/pages/ScreeningGuide.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import RiskTab from '../components/screening/RiskTab';
|
||||||
|
import ComplianceTab from '../components/screening/ComplianceTab';
|
||||||
|
import MethodologyTab from '../components/screening/MethodologyTab';
|
||||||
|
|
||||||
|
type ActiveTab = 'risk' | 'compliance' | 'methodology';
|
||||||
|
|
||||||
|
interface TabButtonProps {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabButton({ active, onClick, children }: TabButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all border ${
|
||||||
|
active
|
||||||
|
? 'bg-slate-900 text-white border-slate-900 shadow-md'
|
||||||
|
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text hover:bg-wing-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScreeningGuide() {
|
||||||
|
const [activeTab, setActiveTab] = useState<ActiveTab>('risk');
|
||||||
|
const [lang, setLang] = useState('KO');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-red-900 rounded-xl p-6 text-white">
|
||||||
|
<div className="text-xs opacity-75 mb-1">
|
||||||
|
S&P Global · Maritime Intelligence Risk Suite (MIRS)
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-bold mb-1">
|
||||||
|
Risk & Compliance Screening Guide
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm opacity-85">
|
||||||
|
위험 지표 및 컴플라이언스 심사 기준 가이드
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 탭 + 언어 토글 */}
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'risk'}
|
||||||
|
onClick={() => setActiveTab('risk')}
|
||||||
|
>
|
||||||
|
Risk Indicators
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'compliance'}
|
||||||
|
onClick={() => setActiveTab('compliance')}
|
||||||
|
>
|
||||||
|
Compliance
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'methodology'}
|
||||||
|
onClick={() => setActiveTab('methodology')}
|
||||||
|
>
|
||||||
|
Methodology History
|
||||||
|
</TabButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 border border-wing-border rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => setLang('EN')}
|
||||||
|
className={`px-3 py-1.5 text-xs font-bold transition-colors ${
|
||||||
|
lang === 'EN'
|
||||||
|
? 'bg-slate-900 text-white'
|
||||||
|
: 'bg-wing-card text-wing-muted hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLang('KO')}
|
||||||
|
className={`px-3 py-1.5 text-xs font-bold transition-colors ${
|
||||||
|
lang === 'KO'
|
||||||
|
? 'bg-slate-900 text-white'
|
||||||
|
: 'bg-wing-card text-wing-muted hover:text-wing-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
KO
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 탭 내용 */}
|
||||||
|
{activeTab === 'risk' && <RiskTab lang={lang} />}
|
||||||
|
{activeTab === 'compliance' && <ComplianceTab lang={lang} />}
|
||||||
|
{activeTab === 'methodology' && <MethodologyTab lang={lang} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -50,12 +50,20 @@ public class SwaggerConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi screeningGuideApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("4. Screening Guide")
|
||||||
|
.pathsToMatch("/api/screening-guide/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GroupedOpenApi bypassApi() {
|
public GroupedOpenApi bypassApi() {
|
||||||
return GroupedOpenApi.builder()
|
return GroupedOpenApi.builder()
|
||||||
.group("3. Bypass API")
|
.group("3. Bypass API")
|
||||||
.pathsToMatch("/api/**")
|
.pathsToMatch("/api/**")
|
||||||
.pathsToExclude("/api/batch/**", "/api/bypass-config/**")
|
.pathsToExclude("/api/batch/**", "/api/bypass-config/**", "/api/screening-guide/**")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.snp.batch.global.controller;
|
||||||
|
|
||||||
|
import com.snp.batch.common.web.ApiResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.ComplianceCategoryResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.MethodologyHistoryResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.RiskCategoryResponse;
|
||||||
|
import com.snp.batch.service.ScreeningGuideService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Risk & Compliance Screening 가이드 조회 컨트롤러
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/screening-guide")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "Screening Guide", description = "Risk & Compliance Screening 가이드")
|
||||||
|
public class ScreeningGuideController {
|
||||||
|
|
||||||
|
private final ScreeningGuideService screeningGuideService;
|
||||||
|
|
||||||
|
@Operation(summary = "Risk 지표 목록 조회", description = "카테고리별 Risk 지표 목록을 조회합니다.")
|
||||||
|
@GetMapping("/risk-indicators")
|
||||||
|
public ResponseEntity<ApiResponse<List<RiskCategoryResponse>>> getRiskIndicators(
|
||||||
|
@Parameter(description = "언어 코드", example = "KO")
|
||||||
|
@RequestParam(defaultValue = "KO") String lang) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getRiskIndicators(lang)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Compliance 지표 목록 조회", description = "카테고리별 Compliance 지표 목록을 조회합니다.")
|
||||||
|
@GetMapping("/compliance-indicators")
|
||||||
|
public ResponseEntity<ApiResponse<List<ComplianceCategoryResponse>>> getComplianceIndicators(
|
||||||
|
@Parameter(description = "언어 코드", example = "KO")
|
||||||
|
@RequestParam(defaultValue = "KO") String lang,
|
||||||
|
@Parameter(description = "지표 유형 (SHIP/COMPANY)", example = "SHIP")
|
||||||
|
@RequestParam(defaultValue = "SHIP") String type) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getComplianceIndicators(lang, type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "방법론 변경 이력 조회", description = "방법론 변경 이력을 조회합니다.")
|
||||||
|
@GetMapping("/methodology-history")
|
||||||
|
public ResponseEntity<ApiResponse<List<MethodologyHistoryResponse>>> getMethodologyHistory(
|
||||||
|
@Parameter(description = "언어 코드", example = "KO")
|
||||||
|
@RequestParam(defaultValue = "KO") String lang) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getMethodologyHistory(lang)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,10 @@ public class WebViewController {
|
|||||||
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
||||||
"/recollects", "/recollects/{id:\\d+}",
|
"/recollects", "/recollects/{id:\\d+}",
|
||||||
"/schedules", "/schedule-timeline", "/monitoring",
|
"/schedules", "/schedule-timeline", "/monitoring",
|
||||||
"/bypass-config",
|
"/bypass-config", "/screening-guide",
|
||||||
"/jobs/**", "/executions/**", "/recollects/**",
|
"/jobs/**", "/executions/**", "/recollects/**",
|
||||||
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
||||||
"/bypass-config/**"})
|
"/bypass-config/**", "/screening-guide/**"})
|
||||||
public String forward() {
|
public String forward() {
|
||||||
return "forward:/index.html";
|
return "forward:/index.html";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.snp.batch.global.dto.screening;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ComplianceCategoryResponse {
|
||||||
|
|
||||||
|
private String category;
|
||||||
|
private String indicatorType;
|
||||||
|
private List<ComplianceIndicatorResponse> indicators;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.snp.batch.global.dto.screening;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ComplianceIndicatorResponse {
|
||||||
|
|
||||||
|
private Integer indicatorId;
|
||||||
|
private String fieldKey;
|
||||||
|
private String fieldName;
|
||||||
|
private String description;
|
||||||
|
private String conditionRed;
|
||||||
|
private String conditionAmber;
|
||||||
|
private String conditionGreen;
|
||||||
|
private String dataType;
|
||||||
|
private String collectionNote;
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package com.snp.batch.global.dto.screening;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MethodologyHistoryResponse {
|
||||||
|
|
||||||
|
private Integer historyId;
|
||||||
|
private String changeDate;
|
||||||
|
private String changeType;
|
||||||
|
private String updateTitle;
|
||||||
|
private String description;
|
||||||
|
private String collectionNote;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.snp.batch.global.dto.screening;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RiskCategoryResponse {
|
||||||
|
|
||||||
|
private String categoryCode;
|
||||||
|
private String categoryName;
|
||||||
|
private List<RiskIndicatorResponse> indicators;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.snp.batch.global.dto.screening;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RiskIndicatorResponse {
|
||||||
|
|
||||||
|
private Integer indicatorId;
|
||||||
|
private String fieldKey;
|
||||||
|
private String fieldName;
|
||||||
|
private String description;
|
||||||
|
private String conditionRed;
|
||||||
|
private String conditionAmber;
|
||||||
|
private String conditionGreen;
|
||||||
|
private String dataType;
|
||||||
|
private String collectionNote;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 유형 코드 마스터 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "change_type", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ChangeType {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "type_code", length = 30)
|
||||||
|
private String typeCode;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.IdClass;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 유형 다국어 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "change_type_lang", schema = "std_snp_data")
|
||||||
|
@IdClass(ChangeTypeLangId.class)
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ChangeTypeLang {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "type_code", length = 30)
|
||||||
|
private String typeCode;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "type_name", nullable = false, length = 100)
|
||||||
|
private String typeName;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 유형 다국어 복합 PK
|
||||||
|
*/
|
||||||
|
public class ChangeTypeLangId implements Serializable {
|
||||||
|
|
||||||
|
private String typeCode;
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
public ChangeTypeLangId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChangeTypeLangId(String typeCode, String langCode) {
|
||||||
|
this.typeCode = typeCode;
|
||||||
|
this.langCode = langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof ChangeTypeLangId that)) return false;
|
||||||
|
return Objects.equals(typeCode, that.typeCode) && Objects.equals(langCode, that.langCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(typeCode, langCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴플라이언스 지표 마스터 엔티티 (읽기 전용)
|
||||||
|
* indicator_type: SHIP 또는 COMPANY
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "compliance_indicator", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ComplianceIndicator {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "indicator_id")
|
||||||
|
private Integer indicatorId;
|
||||||
|
|
||||||
|
@Column(name = "indicator_type", nullable = false, length = 20)
|
||||||
|
private String indicatorType;
|
||||||
|
|
||||||
|
@Column(name = "category", nullable = false, length = 100)
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Column(name = "field_key", nullable = false, length = 200)
|
||||||
|
private String fieldKey;
|
||||||
|
|
||||||
|
@Column(name = "data_type_code", length = 50)
|
||||||
|
private String dataTypeCode;
|
||||||
|
|
||||||
|
@Column(name = "collection_note", columnDefinition = "TEXT")
|
||||||
|
private String collectionNote;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.IdClass;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴플라이언스 지표 다국어 번역 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "compliance_indicator_lang", schema = "std_snp_data")
|
||||||
|
@IdClass(ComplianceIndicatorLangId.class)
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class ComplianceIndicatorLang {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "indicator_id")
|
||||||
|
private Integer indicatorId;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "field_name", nullable = false, length = 500)
|
||||||
|
private String fieldName;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "condition_red", length = 500)
|
||||||
|
private String conditionRed;
|
||||||
|
|
||||||
|
@Column(name = "condition_amber", length = 500)
|
||||||
|
private String conditionAmber;
|
||||||
|
|
||||||
|
@Column(name = "condition_green", length = 500)
|
||||||
|
private String conditionGreen;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴플라이언스 지표 다국어 복합 PK
|
||||||
|
*/
|
||||||
|
public class ComplianceIndicatorLangId implements Serializable {
|
||||||
|
|
||||||
|
private Integer indicatorId;
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
public ComplianceIndicatorLangId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComplianceIndicatorLangId(Integer indicatorId, String langCode) {
|
||||||
|
this.indicatorId = indicatorId;
|
||||||
|
this.langCode = langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof ComplianceIndicatorLangId that)) return false;
|
||||||
|
return Objects.equals(indicatorId, that.indicatorId) && Objects.equals(langCode, that.langCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(indicatorId, langCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다국어 언어 코드 메타 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "language", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Language {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "lang_name", nullable = false, length = 50)
|
||||||
|
private String langName;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private Boolean isActive;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 이력 마스터 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "methodology_history", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class MethodologyHistory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "history_id")
|
||||||
|
private Integer historyId;
|
||||||
|
|
||||||
|
@Column(name = "change_date", nullable = false)
|
||||||
|
private LocalDate changeDate;
|
||||||
|
|
||||||
|
@Column(name = "change_type_code", nullable = false, length = 30)
|
||||||
|
private String changeTypeCode;
|
||||||
|
|
||||||
|
@Column(name = "collection_note", columnDefinition = "TEXT")
|
||||||
|
private String collectionNote;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.IdClass;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 이력 다국어 번역 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "methodology_history_lang", schema = "std_snp_data")
|
||||||
|
@IdClass(MethodologyHistoryLangId.class)
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class MethodologyHistoryLang {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "history_id")
|
||||||
|
private Integer historyId;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "update_title", length = 500)
|
||||||
|
private String updateTitle;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 이력 다국어 복합 PK
|
||||||
|
*/
|
||||||
|
public class MethodologyHistoryLangId implements Serializable {
|
||||||
|
|
||||||
|
private Integer historyId;
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
public MethodologyHistoryLangId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodologyHistoryLangId(Integer historyId, String langCode) {
|
||||||
|
this.historyId = historyId;
|
||||||
|
this.langCode = langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof MethodologyHistoryLangId that)) return false;
|
||||||
|
return Objects.equals(historyId, that.historyId) && Objects.equals(langCode, that.langCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(historyId, langCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 마스터 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "risk_indicator", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class RiskIndicator {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "indicator_id")
|
||||||
|
private Integer indicatorId;
|
||||||
|
|
||||||
|
@Column(name = "category_code", nullable = false, length = 50)
|
||||||
|
private String categoryCode;
|
||||||
|
|
||||||
|
@Column(name = "field_key", nullable = false, length = 200)
|
||||||
|
private String fieldKey;
|
||||||
|
|
||||||
|
@Column(name = "data_type_code", length = 50)
|
||||||
|
private String dataTypeCode;
|
||||||
|
|
||||||
|
@Column(name = "collection_note", columnDefinition = "TEXT")
|
||||||
|
private String collectionNote;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 카테고리 마스터 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "risk_indicator_category", schema = "std_snp_data")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class RiskIndicatorCategory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "category_code", length = 50)
|
||||||
|
private String categoryCode;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.IdClass;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 카테고리 다국어 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "risk_indicator_category_lang", schema = "std_snp_data")
|
||||||
|
@IdClass(RiskIndicatorCategoryLangId.class)
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class RiskIndicatorCategoryLang {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "category_code", length = 50)
|
||||||
|
private String categoryCode;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "category_name", nullable = false, length = 200)
|
||||||
|
private String categoryName;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 카테고리 다국어 복합 PK
|
||||||
|
*/
|
||||||
|
public class RiskIndicatorCategoryLangId implements Serializable {
|
||||||
|
|
||||||
|
private String categoryCode;
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
public RiskIndicatorCategoryLangId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RiskIndicatorCategoryLangId(String categoryCode, String langCode) {
|
||||||
|
this.categoryCode = categoryCode;
|
||||||
|
this.langCode = langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof RiskIndicatorCategoryLangId that)) return false;
|
||||||
|
return Objects.equals(categoryCode, that.categoryCode) && Objects.equals(langCode, that.langCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(categoryCode, langCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.IdClass;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 다국어 번역 엔티티 (읽기 전용)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "risk_indicator_lang", schema = "std_snp_data")
|
||||||
|
@IdClass(RiskIndicatorLangId.class)
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class RiskIndicatorLang {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "indicator_id")
|
||||||
|
private Integer indicatorId;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "lang_code", length = 5)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Column(name = "field_name", nullable = false, length = 500)
|
||||||
|
private String fieldName;
|
||||||
|
|
||||||
|
@Column(name = "description", columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "condition_red", length = 500)
|
||||||
|
private String conditionRed;
|
||||||
|
|
||||||
|
@Column(name = "condition_amber", length = 500)
|
||||||
|
private String conditionAmber;
|
||||||
|
|
||||||
|
@Column(name = "condition_green", length = 500)
|
||||||
|
private String conditionGreen;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.snp.batch.global.model.screening;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위험 지표 다국어 복합 PK
|
||||||
|
*/
|
||||||
|
public class RiskIndicatorLangId implements Serializable {
|
||||||
|
|
||||||
|
private Integer indicatorId;
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
public RiskIndicatorLangId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RiskIndicatorLangId(Integer indicatorId, String langCode) {
|
||||||
|
this.indicatorId = indicatorId;
|
||||||
|
this.langCode = langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof RiskIndicatorLangId that)) return false;
|
||||||
|
return Objects.equals(indicatorId, that.indicatorId) && Objects.equals(langCode, that.langCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(indicatorId, langCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.ChangeTypeLang;
|
||||||
|
import com.snp.batch.global.model.screening.ChangeTypeLangId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ChangeTypeLangRepository extends JpaRepository<ChangeTypeLang, ChangeTypeLangId> {
|
||||||
|
|
||||||
|
List<ChangeTypeLang> findByLangCode(String langCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.ComplianceIndicatorLang;
|
||||||
|
import com.snp.batch.global.model.screening.ComplianceIndicatorLangId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ComplianceIndicatorLangRepository extends JpaRepository<ComplianceIndicatorLang, ComplianceIndicatorLangId> {
|
||||||
|
|
||||||
|
List<ComplianceIndicatorLang> findByLangCode(String langCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.ComplianceIndicator;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ComplianceIndicatorRepository extends JpaRepository<ComplianceIndicator, Integer> {
|
||||||
|
|
||||||
|
List<ComplianceIndicator> findByIndicatorTypeOrderBySortOrderAsc(String indicatorType);
|
||||||
|
|
||||||
|
List<ComplianceIndicator> findAllByOrderBySortOrderAsc();
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.MethodologyHistoryLang;
|
||||||
|
import com.snp.batch.global.model.screening.MethodologyHistoryLangId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MethodologyHistoryLangRepository extends JpaRepository<MethodologyHistoryLang, MethodologyHistoryLangId> {
|
||||||
|
|
||||||
|
List<MethodologyHistoryLang> findByLangCode(String langCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.MethodologyHistory;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MethodologyHistoryRepository extends JpaRepository<MethodologyHistory, Integer> {
|
||||||
|
|
||||||
|
List<MethodologyHistory> findAllByOrderByChangeDateDescSortOrderAsc();
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorCategoryLang;
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorCategoryLangId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RiskIndicatorCategoryLangRepository extends JpaRepository<RiskIndicatorCategoryLang, RiskIndicatorCategoryLangId> {
|
||||||
|
|
||||||
|
List<RiskIndicatorCategoryLang> findByLangCode(String langCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorLang;
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorLangId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RiskIndicatorLangRepository extends JpaRepository<RiskIndicatorLang, RiskIndicatorLangId> {
|
||||||
|
|
||||||
|
List<RiskIndicatorLang> findByLangCodeOrderByIndicatorIdAsc(String langCode);
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.snp.batch.global.repository.screening;
|
||||||
|
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicator;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RiskIndicatorRepository extends JpaRepository<RiskIndicator, Integer> {
|
||||||
|
|
||||||
|
List<RiskIndicator> findAllByOrderByCategoryCodeAscSortOrderAsc();
|
||||||
|
}
|
||||||
164
src/main/java/com/snp/batch/service/ScreeningGuideService.java
Normal file
164
src/main/java/com/snp/batch/service/ScreeningGuideService.java
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
package com.snp.batch.service;
|
||||||
|
|
||||||
|
import com.snp.batch.global.dto.screening.ComplianceCategoryResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.ComplianceIndicatorResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.MethodologyHistoryResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.RiskCategoryResponse;
|
||||||
|
import com.snp.batch.global.dto.screening.RiskIndicatorResponse;
|
||||||
|
import com.snp.batch.global.model.screening.ChangeTypeLang;
|
||||||
|
import com.snp.batch.global.model.screening.ComplianceIndicator;
|
||||||
|
import com.snp.batch.global.model.screening.ComplianceIndicatorLang;
|
||||||
|
import com.snp.batch.global.model.screening.MethodologyHistory;
|
||||||
|
import com.snp.batch.global.model.screening.MethodologyHistoryLang;
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicator;
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorCategoryLang;
|
||||||
|
import com.snp.batch.global.model.screening.RiskIndicatorLang;
|
||||||
|
import com.snp.batch.global.repository.screening.ChangeTypeLangRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.ComplianceIndicatorLangRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.ComplianceIndicatorRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.MethodologyHistoryLangRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.MethodologyHistoryRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.RiskIndicatorCategoryLangRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.RiskIndicatorLangRepository;
|
||||||
|
import com.snp.batch.global.repository.screening.RiskIndicatorRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScreeningGuideService {
|
||||||
|
|
||||||
|
private final RiskIndicatorRepository riskIndicatorRepo;
|
||||||
|
private final RiskIndicatorLangRepository riskIndicatorLangRepo;
|
||||||
|
private final RiskIndicatorCategoryLangRepository riskCategoryLangRepo;
|
||||||
|
private final ComplianceIndicatorRepository complianceIndicatorRepo;
|
||||||
|
private final ComplianceIndicatorLangRepository complianceIndicatorLangRepo;
|
||||||
|
private final MethodologyHistoryRepository methodologyHistoryRepo;
|
||||||
|
private final MethodologyHistoryLangRepository methodologyHistoryLangRepo;
|
||||||
|
private final ChangeTypeLangRepository changeTypeLangRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리별 Risk 지표 목록 조회
|
||||||
|
*
|
||||||
|
* @param lang 언어 코드 (예: KO, EN)
|
||||||
|
* @return 카테고리별 Risk 지표 응답 목록
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RiskCategoryResponse> getRiskIndicators(String lang) {
|
||||||
|
Map<String, String> catNameMap = riskCategoryLangRepo.findByLangCode(lang).stream()
|
||||||
|
.collect(Collectors.toMap(RiskIndicatorCategoryLang::getCategoryCode, RiskIndicatorCategoryLang::getCategoryName));
|
||||||
|
|
||||||
|
Map<Integer, RiskIndicatorLang> langMap = riskIndicatorLangRepo.findByLangCodeOrderByIndicatorIdAsc(lang).stream()
|
||||||
|
.collect(Collectors.toMap(RiskIndicatorLang::getIndicatorId, Function.identity()));
|
||||||
|
|
||||||
|
List<RiskIndicator> indicators = riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc();
|
||||||
|
|
||||||
|
Map<String, List<RiskIndicator>> grouped = indicators.stream()
|
||||||
|
.collect(Collectors.groupingBy(RiskIndicator::getCategoryCode, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
|
||||||
|
return grouped.entrySet().stream().map(entry -> {
|
||||||
|
String catCode = entry.getKey();
|
||||||
|
List<RiskIndicatorResponse> indicatorResponses = entry.getValue().stream().map(ri -> {
|
||||||
|
RiskIndicatorLang langData = langMap.get(ri.getIndicatorId());
|
||||||
|
return RiskIndicatorResponse.builder()
|
||||||
|
.indicatorId(ri.getIndicatorId())
|
||||||
|
.fieldKey(ri.getFieldKey())
|
||||||
|
.fieldName(langData != null ? langData.getFieldName() : ri.getFieldKey())
|
||||||
|
.description(langData != null ? langData.getDescription() : "")
|
||||||
|
.conditionRed(langData != null ? langData.getConditionRed() : "")
|
||||||
|
.conditionAmber(langData != null ? langData.getConditionAmber() : "")
|
||||||
|
.conditionGreen(langData != null ? langData.getConditionGreen() : "")
|
||||||
|
.dataType(ri.getDataTypeCode())
|
||||||
|
.collectionNote(ri.getCollectionNote())
|
||||||
|
.build();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return RiskCategoryResponse.builder()
|
||||||
|
.categoryCode(catCode)
|
||||||
|
.categoryName(catNameMap.getOrDefault(catCode, catCode))
|
||||||
|
.indicators(indicatorResponses)
|
||||||
|
.build();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리별 Compliance 지표 목록 조회
|
||||||
|
*
|
||||||
|
* @param lang 언어 코드 (예: KO, EN)
|
||||||
|
* @param type 지표 유형 필터 (SHIP/COMPANY, null 또는 빈 문자열이면 전체)
|
||||||
|
* @return 카테고리별 Compliance 지표 응답 목록
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<ComplianceCategoryResponse> getComplianceIndicators(String lang, String type) {
|
||||||
|
List<ComplianceIndicator> indicators = (type != null && !type.isBlank())
|
||||||
|
? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type)
|
||||||
|
: complianceIndicatorRepo.findAllByOrderBySortOrderAsc();
|
||||||
|
|
||||||
|
Map<Integer, ComplianceIndicatorLang> langMap = complianceIndicatorLangRepo.findByLangCode(lang).stream()
|
||||||
|
.collect(Collectors.toMap(ComplianceIndicatorLang::getIndicatorId, Function.identity()));
|
||||||
|
|
||||||
|
Map<String, List<ComplianceIndicator>> grouped = indicators.stream()
|
||||||
|
.collect(Collectors.groupingBy(ComplianceIndicator::getCategory, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
|
||||||
|
return grouped.entrySet().stream().map(entry -> {
|
||||||
|
List<ComplianceIndicatorResponse> indicatorResponses = entry.getValue().stream().map(ci -> {
|
||||||
|
ComplianceIndicatorLang langData = langMap.get(ci.getIndicatorId());
|
||||||
|
return ComplianceIndicatorResponse.builder()
|
||||||
|
.indicatorId(ci.getIndicatorId())
|
||||||
|
.fieldKey(ci.getFieldKey())
|
||||||
|
.fieldName(langData != null ? langData.getFieldName() : ci.getFieldKey())
|
||||||
|
.description(langData != null ? langData.getDescription() : "")
|
||||||
|
.conditionRed(langData != null ? langData.getConditionRed() : "")
|
||||||
|
.conditionAmber(langData != null ? langData.getConditionAmber() : "")
|
||||||
|
.conditionGreen(langData != null ? langData.getConditionGreen() : "")
|
||||||
|
.dataType(ci.getDataTypeCode())
|
||||||
|
.collectionNote(ci.getCollectionNote())
|
||||||
|
.build();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return ComplianceCategoryResponse.builder()
|
||||||
|
.category(entry.getKey())
|
||||||
|
.indicatorType(type)
|
||||||
|
.indicators(indicatorResponses)
|
||||||
|
.build();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방법론 변경 이력 조회
|
||||||
|
*
|
||||||
|
* @param lang 언어 코드 (예: KO, EN)
|
||||||
|
* @return 방법론 변경 이력 응답 목록
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<MethodologyHistoryResponse> getMethodologyHistory(String lang) {
|
||||||
|
Map<String, String> changeTypeMap = changeTypeLangRepo.findByLangCode(lang).stream()
|
||||||
|
.collect(Collectors.toMap(ChangeTypeLang::getTypeCode, ChangeTypeLang::getTypeName));
|
||||||
|
|
||||||
|
Map<Integer, MethodologyHistoryLang> langMap = methodologyHistoryLangRepo.findByLangCode(lang).stream()
|
||||||
|
.collect(Collectors.toMap(MethodologyHistoryLang::getHistoryId, Function.identity()));
|
||||||
|
|
||||||
|
List<MethodologyHistory> histories = methodologyHistoryRepo.findAllByOrderByChangeDateDescSortOrderAsc();
|
||||||
|
|
||||||
|
return histories.stream().map(mh -> {
|
||||||
|
MethodologyHistoryLang langData = langMap.get(mh.getHistoryId());
|
||||||
|
return MethodologyHistoryResponse.builder()
|
||||||
|
.historyId(mh.getHistoryId())
|
||||||
|
.changeDate(mh.getChangeDate() != null ? mh.getChangeDate().toString() : "")
|
||||||
|
.changeType(changeTypeMap.getOrDefault(mh.getChangeTypeCode(), mh.getChangeTypeCode()))
|
||||||
|
.updateTitle(langData != null ? langData.getUpdateTitle() : "")
|
||||||
|
.description(langData != null ? langData.getDescription() : "")
|
||||||
|
.collectionNote(mh.getCollectionNote())
|
||||||
|
.build();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
불러오는 중...
Reference in New Issue
Block a user