diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7e28c43..d98087e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ const RecollectDetail = lazy(() => import('./pages/RecollectDetail')); const Schedules = lazy(() => import('./pages/Schedules')); const Timeline = lazy(() => import('./pages/Timeline')); const BypassConfig = lazy(() => import('./pages/BypassConfig')); +const ScreeningGuide = lazy(() => import('./pages/ScreeningGuide')); function AppLayout() { const { toasts, removeToast } = useToastContext(); @@ -34,6 +35,7 @@ function AppLayout() { } /> } /> } /> + } /> diff --git a/frontend/src/api/screeningGuideApi.ts b/frontend/src/api/screeningGuideApi.ts new file mode 100644 index 0000000..e104286 --- /dev/null +++ b/frontend/src/api/screeningGuideApi.ts @@ -0,0 +1,71 @@ +// API 응답 타입 +interface ApiResponse { + 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(url: string): Promise { + 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>(`${BASE}/risk-indicators?lang=${lang}`), + getComplianceIndicators: (lang = 'KO', type = 'SHIP') => + fetchJson>(`${BASE}/compliance-indicators?lang=${lang}&type=${type}`), + getMethodologyHistory: (lang = 'KO') => + fetchJson>(`${BASE}/methodology-history?lang=${lang}`), +}; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index b04af2d..9707045 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -9,6 +9,7 @@ const navItems = [ { path: '/schedules', label: '스케줄', icon: '🕐' }, { path: '/schedule-timeline', label: '타임라인', icon: '📅' }, { path: '/bypass-config', label: 'Bypass API', icon: '🔗' }, + { path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' }, ]; export default function Navbar() { diff --git a/frontend/src/components/screening/ComplianceTab.tsx b/frontend/src/components/screening/ComplianceTab.tsx new file mode 100644 index 0000000..a4e563e --- /dev/null +++ b/frontend/src/components/screening/ComplianceTab.tsx @@ -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 = { + '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 = { + '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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('전체'); + const [viewMode, setViewMode] = useState('table'); + const [indicatorType, setIndicatorType] = useState('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 ( +
+ {/* SHIP / COMPANY 토글 */} +
+ + +
+ + {loading && ( +
+
+
+
데이터를 불러오는 중...
+
+
+ )} + + {error && ( +
+ 데이터 로딩 실패: {error} +
+ )} + + {!loading && !error && ( + <> + {/* 카테고리 요약 카드 */} +
+ {uniqueCategories.map((catName) => { + const count = flatRows.filter((r) => r.category === catName).length; + const isActive = selectedCategory === catName; + const hex = getCatHex(catName, indicatorType); + return ( + + ); + })} +
+ + {/* 컨트롤 바 */} +
+ +
+ + 표시:{' '} + {filtered.length}개 항목 + + + +
+ + {/* 테이블 뷰 */} + {viewMode === 'table' && ( +
+
+ + + + {[ + '카테고리', + '필드명', + '설명', + '🔴 RED', + '🟡 AMBER', + '🟢 GREEN', + '데이터 타입', + '이력 관리 참고', + ].map((h) => ( + + ))} + + + + {filtered.map((row, i) => { + const showCat = + i === 0 || + filtered[i - 1].category !== row.category; + const hex = getCatHex(row.category, indicatorType); + return ( + + + + + + + + + + + ); + })} + +
+ {h} +
+ {showCat && ( + + {row.category} + + )} + +
+ {row.indicator.fieldName} +
+
+ {row.indicator.fieldKey} +
+
+ {row.indicator.description} + +
+ {row.indicator.conditionRed} +
+
+
+ {row.indicator.conditionAmber} +
+
+
+ {row.indicator.conditionGreen} +
+
+ {row.indicator.dataType} + + {row.indicator.collectionNote && + `💡 ${row.indicator.collectionNote}`} +
+
+
+ )} + + {/* 카드 뷰 */} + {viewMode === 'card' && ( +
+ {filtered.map((row, i) => { + const hex = getCatHex(row.category, indicatorType); + return ( +
+
+ + {row.category} + + + {row.indicator.dataType} + +
+
+
+ {row.indicator.fieldName} +
+
+ {row.indicator.fieldKey} +
+
+ {row.indicator.description} +
+
+
+
+ 🔴 RED +
+
+ {row.indicator.conditionRed} +
+
+
+
+ 🟡 AMBER +
+
+ {row.indicator.conditionAmber} +
+
+
+
+ 🟢 GREEN +
+
+ {row.indicator.conditionGreen} +
+
+
+ {row.indicator.collectionNote && ( +
+ 💡 {row.indicator.collectionNote} +
+ )} +
+
+ ); + })} +
+ )} + + )} +
+ ); +} diff --git a/frontend/src/components/screening/MethodologyTab.tsx b/frontend/src/components/screening/MethodologyTab.tsx new file mode 100644 index 0000000..fb9aa14 --- /dev/null +++ b/frontend/src/components/screening/MethodologyTab.tsx @@ -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 = { + 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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
+
데이터를 불러오는 중...
+
+
+ ); + } + + if (error) { + return ( +
+ 데이터 로딩 실패: {error} +
+ ); + } + + return ( +
+ {/* 주의사항 배너 */} +
+ 이력 관리 주의사항: 방법론 변경은 선박·기업의 컴플라이언스 + 상태값을 자동으로 변경시킵니다. 상태 변경이 실제 리스크 변화인지, 방법론 + 업데이트 때문인지 반드시 교차 확인해야 합니다. +
+ + {/* 변경 유형 필터 */} +
+ + {uniqueTypes.map((type) => { + const count = history.filter((h) => h.changeType === type).length; + const hex = getChangeTypeColor(type); + const isActive = selectedType === type; + return ( + + ); + })} +
+ +
+ 표시: {filtered.length}건 | 최신순 정렬 +
+ + {/* 타임라인 목록 */} +
+
+ + + + {['날짜', '변경 유형', '제목', '설명', '참고사항'].map((h) => ( + + ))} + + + + {filtered.map((row, i) => { + const hex = getChangeTypeColor(row.changeType); + return ( + + + + + + + + ); + })} + +
+ {h} +
+
+ {row.changeDate} +
+
+ + {row.changeType} + + + {row.updateTitle} + + {row.description} + + {row.collectionNote && `💡 ${row.collectionNote}`} +
+
+
+ + {filtered.length === 0 && ( +
+ 해당 유형의 변경 이력이 없습니다. +
+ )} +
+ ); +} diff --git a/frontend/src/components/screening/RiskTab.tsx b/frontend/src/components/screening/RiskTab.tsx new file mode 100644 index 0000000..92b6e40 --- /dev/null +++ b/frontend/src/components/screening/RiskTab.tsx @@ -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 = { + '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 = { + '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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedCategory, setSelectedCategory] = useState('전체'); + const [viewMode, setViewMode] = useState('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 ( +
+
+
+
데이터를 불러오는 중...
+
+
+ ); + } + + if (error) { + return ( +
+ 데이터 로딩 실패: {error} +
+ ); + } + + return ( +
+ {/* 카테고리 요약 카드 */} +
+ {categories.map((cat) => { + const isActive = selectedCategory === cat.categoryName; + const hex = getCatHex(cat.categoryName); + return ( + + ); + })} +
+ + {/* 컨트롤 바 */} +
+ +
+ + 표시:{' '} + {filtered.length}개 항목 + + + +
+ + {/* 테이블 뷰 */} + {viewMode === 'table' && ( +
+
+ + + + {[ + '카테고리', + '필드명', + '설명', + '🔴 RED', + '🟡 AMBER', + '🟢 GREEN', + '데이터 타입', + '이력 관리 참고', + ].map((h) => ( + + ))} + + + + {filtered.map((row, i) => { + const showCat = + i === 0 || + filtered[i - 1].category !== row.category; + const hex = getCatHex(row.category); + return ( + + + + + + + + + + + ); + })} + +
+ {h} +
+ {showCat && ( + + {row.category} + + )} + +
+ {row.indicator.fieldName} +
+
+ {row.indicator.fieldKey} +
+
+ {row.indicator.description} + +
+ {row.indicator.conditionRed} +
+
+
+ {row.indicator.conditionAmber} +
+
+
+ {row.indicator.conditionGreen} +
+
+ {row.indicator.dataType} + + {row.indicator.collectionNote && + `💡 ${row.indicator.collectionNote}`} +
+
+
+ )} + + {/* 카드 뷰 */} + {viewMode === 'card' && ( +
+ {filtered.map((row, i) => { + const hex = getCatHex(row.category); + const colorClass = getCatColor(row.category); + return ( +
+
+ + {row.category} + + + {row.indicator.dataType} + +
+
+
+ {row.indicator.fieldName} +
+
+ {row.indicator.fieldKey} +
+
+ {row.indicator.description} +
+
+
+
+ 🔴 RED +
+
+ {row.indicator.conditionRed} +
+
+
+
+ 🟡 AMBER +
+
+ {row.indicator.conditionAmber} +
+
+
+
+ 🟢 GREEN +
+
+ {row.indicator.conditionGreen} +
+
+
+ {row.indicator.collectionNote && ( +
+ 💡 {row.indicator.collectionNote} +
+ )} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/ScreeningGuide.tsx b/frontend/src/pages/ScreeningGuide.tsx new file mode 100644 index 0000000..6f971dc --- /dev/null +++ b/frontend/src/pages/ScreeningGuide.tsx @@ -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 ( + + ); +} + +export default function ScreeningGuide() { + const [activeTab, setActiveTab] = useState('risk'); + const [lang, setLang] = useState('KO'); + + return ( +
+ {/* 헤더 */} +
+
+ S&P Global · Maritime Intelligence Risk Suite (MIRS) +
+

+ Risk & Compliance Screening Guide +

+

+ 위험 지표 및 컴플라이언스 심사 기준 가이드 +

+
+ + {/* 탭 + 언어 토글 */} +
+
+ setActiveTab('risk')} + > + Risk Indicators + + setActiveTab('compliance')} + > + Compliance + + setActiveTab('methodology')} + > + Methodology History + +
+
+ + +
+
+ + {/* 탭 내용 */} + {activeTab === 'risk' && } + {activeTab === 'compliance' && } + {activeTab === 'methodology' && } +
+ ); +} diff --git a/src/main/java/com/snp/batch/global/config/SwaggerConfig.java b/src/main/java/com/snp/batch/global/config/SwaggerConfig.java index 15b3971..52e8fd3 100644 --- a/src/main/java/com/snp/batch/global/config/SwaggerConfig.java +++ b/src/main/java/com/snp/batch/global/config/SwaggerConfig.java @@ -50,12 +50,20 @@ public class SwaggerConfig { .build(); } + @Bean + public GroupedOpenApi screeningGuideApi() { + return GroupedOpenApi.builder() + .group("4. Screening Guide") + .pathsToMatch("/api/screening-guide/**") + .build(); + } + @Bean public GroupedOpenApi bypassApi() { return GroupedOpenApi.builder() .group("3. Bypass API") .pathsToMatch("/api/**") - .pathsToExclude("/api/batch/**", "/api/bypass-config/**") + .pathsToExclude("/api/batch/**", "/api/bypass-config/**", "/api/screening-guide/**") .build(); } diff --git a/src/main/java/com/snp/batch/global/controller/ScreeningGuideController.java b/src/main/java/com/snp/batch/global/controller/ScreeningGuideController.java new file mode 100644 index 0000000..edc2784 --- /dev/null +++ b/src/main/java/com/snp/batch/global/controller/ScreeningGuideController.java @@ -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>> 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>> 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>> getMethodologyHistory( + @Parameter(description = "언어 코드", example = "KO") + @RequestParam(defaultValue = "KO") String lang) { + return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getMethodologyHistory(lang))); + } +} diff --git a/src/main/java/com/snp/batch/global/controller/WebViewController.java b/src/main/java/com/snp/batch/global/controller/WebViewController.java index 916a4ab..57a9068 100644 --- a/src/main/java/com/snp/batch/global/controller/WebViewController.java +++ b/src/main/java/com/snp/batch/global/controller/WebViewController.java @@ -15,10 +15,10 @@ public class WebViewController { @GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}", "/recollects", "/recollects/{id:\\d+}", "/schedules", "/schedule-timeline", "/monitoring", - "/bypass-config", + "/bypass-config", "/screening-guide", "/jobs/**", "/executions/**", "/recollects/**", "/schedules/**", "/schedule-timeline/**", "/monitoring/**", - "/bypass-config/**"}) + "/bypass-config/**", "/screening-guide/**"}) public String forward() { return "forward:/index.html"; } diff --git a/src/main/java/com/snp/batch/global/dto/screening/ComplianceCategoryResponse.java b/src/main/java/com/snp/batch/global/dto/screening/ComplianceCategoryResponse.java new file mode 100644 index 0000000..26257ba --- /dev/null +++ b/src/main/java/com/snp/batch/global/dto/screening/ComplianceCategoryResponse.java @@ -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 indicators; +} diff --git a/src/main/java/com/snp/batch/global/dto/screening/ComplianceIndicatorResponse.java b/src/main/java/com/snp/batch/global/dto/screening/ComplianceIndicatorResponse.java new file mode 100644 index 0000000..43667fc --- /dev/null +++ b/src/main/java/com/snp/batch/global/dto/screening/ComplianceIndicatorResponse.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/dto/screening/MethodologyHistoryResponse.java b/src/main/java/com/snp/batch/global/dto/screening/MethodologyHistoryResponse.java new file mode 100644 index 0000000..7849e88 --- /dev/null +++ b/src/main/java/com/snp/batch/global/dto/screening/MethodologyHistoryResponse.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/dto/screening/RiskCategoryResponse.java b/src/main/java/com/snp/batch/global/dto/screening/RiskCategoryResponse.java new file mode 100644 index 0000000..be9367f --- /dev/null +++ b/src/main/java/com/snp/batch/global/dto/screening/RiskCategoryResponse.java @@ -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 indicators; +} diff --git a/src/main/java/com/snp/batch/global/dto/screening/RiskIndicatorResponse.java b/src/main/java/com/snp/batch/global/dto/screening/RiskIndicatorResponse.java new file mode 100644 index 0000000..14bae94 --- /dev/null +++ b/src/main/java/com/snp/batch/global/dto/screening/RiskIndicatorResponse.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ChangeType.java b/src/main/java/com/snp/batch/global/model/screening/ChangeType.java new file mode 100644 index 0000000..4441313 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ChangeType.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLang.java b/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLang.java new file mode 100644 index 0000000..062a284 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLang.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLangId.java b/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLangId.java new file mode 100644 index 0000000..7a86247 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ChangeTypeLangId.java @@ -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); + } +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicator.java b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicator.java new file mode 100644 index 0000000..a53fff5 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicator.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLang.java b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLang.java new file mode 100644 index 0000000..aca1776 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLang.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLangId.java b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLangId.java new file mode 100644 index 0000000..012170d --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/ComplianceIndicatorLangId.java @@ -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); + } +} diff --git a/src/main/java/com/snp/batch/global/model/screening/Language.java b/src/main/java/com/snp/batch/global/model/screening/Language.java new file mode 100644 index 0000000..c75d73e --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/Language.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/MethodologyHistory.java b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistory.java new file mode 100644 index 0000000..8d51265 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistory.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLang.java b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLang.java new file mode 100644 index 0000000..08d77e3 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLang.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLangId.java b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLangId.java new file mode 100644 index 0000000..0604186 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/MethodologyHistoryLangId.java @@ -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); + } +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicator.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicator.java new file mode 100644 index 0000000..58fdb0e --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicator.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategory.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategory.java new file mode 100644 index 0000000..5f3b6bd --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategory.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLang.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLang.java new file mode 100644 index 0000000..88ffd3d --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLang.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLangId.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLangId.java new file mode 100644 index 0000000..d85d61b --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorCategoryLangId.java @@ -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); + } +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLang.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLang.java new file mode 100644 index 0000000..3932943 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLang.java @@ -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; +} diff --git a/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLangId.java b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLangId.java new file mode 100644 index 0000000..5149130 --- /dev/null +++ b/src/main/java/com/snp/batch/global/model/screening/RiskIndicatorLangId.java @@ -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); + } +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/ChangeTypeLangRepository.java b/src/main/java/com/snp/batch/global/repository/screening/ChangeTypeLangRepository.java new file mode 100644 index 0000000..7571a20 --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/ChangeTypeLangRepository.java @@ -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 { + + List findByLangCode(String langCode); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorLangRepository.java b/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorLangRepository.java new file mode 100644 index 0000000..a68762a --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorLangRepository.java @@ -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 { + + List findByLangCode(String langCode); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorRepository.java b/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorRepository.java new file mode 100644 index 0000000..d738c66 --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/ComplianceIndicatorRepository.java @@ -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 { + + List findByIndicatorTypeOrderBySortOrderAsc(String indicatorType); + + List findAllByOrderBySortOrderAsc(); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryLangRepository.java b/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryLangRepository.java new file mode 100644 index 0000000..e75fb98 --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryLangRepository.java @@ -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 { + + List findByLangCode(String langCode); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryRepository.java b/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryRepository.java new file mode 100644 index 0000000..e21990a --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/MethodologyHistoryRepository.java @@ -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 { + + List findAllByOrderByChangeDateDescSortOrderAsc(); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorCategoryLangRepository.java b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorCategoryLangRepository.java new file mode 100644 index 0000000..eb0dfe7 --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorCategoryLangRepository.java @@ -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 { + + List findByLangCode(String langCode); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorLangRepository.java b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorLangRepository.java new file mode 100644 index 0000000..3c728c4 --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorLangRepository.java @@ -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 { + + List findByLangCodeOrderByIndicatorIdAsc(String langCode); +} diff --git a/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorRepository.java b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorRepository.java new file mode 100644 index 0000000..b5d8fcc --- /dev/null +++ b/src/main/java/com/snp/batch/global/repository/screening/RiskIndicatorRepository.java @@ -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 { + + List findAllByOrderByCategoryCodeAscSortOrderAsc(); +} diff --git a/src/main/java/com/snp/batch/service/ScreeningGuideService.java b/src/main/java/com/snp/batch/service/ScreeningGuideService.java new file mode 100644 index 0000000..9984a1e --- /dev/null +++ b/src/main/java/com/snp/batch/service/ScreeningGuideService.java @@ -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 getRiskIndicators(String lang) { + Map catNameMap = riskCategoryLangRepo.findByLangCode(lang).stream() + .collect(Collectors.toMap(RiskIndicatorCategoryLang::getCategoryCode, RiskIndicatorCategoryLang::getCategoryName)); + + Map langMap = riskIndicatorLangRepo.findByLangCodeOrderByIndicatorIdAsc(lang).stream() + .collect(Collectors.toMap(RiskIndicatorLang::getIndicatorId, Function.identity())); + + List indicators = riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc(); + + Map> grouped = indicators.stream() + .collect(Collectors.groupingBy(RiskIndicator::getCategoryCode, LinkedHashMap::new, Collectors.toList())); + + return grouped.entrySet().stream().map(entry -> { + String catCode = entry.getKey(); + List 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 getComplianceIndicators(String lang, String type) { + List indicators = (type != null && !type.isBlank()) + ? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type) + : complianceIndicatorRepo.findAllByOrderBySortOrderAsc(); + + Map langMap = complianceIndicatorLangRepo.findByLangCode(lang).stream() + .collect(Collectors.toMap(ComplianceIndicatorLang::getIndicatorId, Function.identity())); + + Map> grouped = indicators.stream() + .collect(Collectors.groupingBy(ComplianceIndicator::getCategory, LinkedHashMap::new, Collectors.toList())); + + return grouped.entrySet().stream().map(entry -> { + List 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 getMethodologyHistory(String lang) { + Map changeTypeMap = changeTypeLangRepo.findByLangCode(lang).stream() + .collect(Collectors.toMap(ChangeTypeLang::getTypeCode, ChangeTypeLang::getTypeName)); + + Map langMap = methodologyHistoryLangRepo.findByLangCode(lang).stream() + .collect(Collectors.toMap(MethodologyHistoryLang::getHistoryId, Function.identity())); + + List 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(); + } +}