feat: Risk&Compliance 값 변경 이력 확인 페이지 개발 (#111) #114
@ -5,6 +5,15 @@
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- Risk&Compliance 값 변경 이력 확인 페이지 개발 (#111)
|
||||
- 선박 위험지표/선박 제재/회사 제재 변경 이력 조회
|
||||
- 선박/회사 기본정보 및 현재 Risk&Compliance 상태 조회
|
||||
- Risk narrative(이전값/이후값) 표시 (LATERAL JOIN)
|
||||
- indicator column_name 매핑으로 다국어 필드명 지원
|
||||
- 다국어 캐시 (KO/EN 동시 조회, 언어 토글 즉시 전환)
|
||||
- 독립 페이지 분리 (/risk-compliance-history)
|
||||
- 국가코드 ISO2 변환 → 국기 이모지 표시
|
||||
- Compliance 탭 분리 (Sanctions/Port Calls/STS/Suspicious)
|
||||
- favicon 변경 (#105)
|
||||
- BY PASS API 등록 프로세스 설계 및 개발 (#63)
|
||||
- 화면에서 API 정보 입력 → Java 코드 자동 생성 (Controller, Service)
|
||||
|
||||
193
docs/ddl/indicator_column_name_mapping.sql
Normal file
193
docs/ddl/indicator_column_name_mapping.sql
Normal file
@ -0,0 +1,193 @@
|
||||
-- =============================================================================
|
||||
-- Risk & Compliance Indicator 테이블에 column_name 매핑
|
||||
-- 목적: flctn_col_nm (이력 테이블 변동컬럼명) → indicator 테이블 JOIN 가능하도록
|
||||
-- 참고: INSERT SQL의 field_key 기준으로 매핑 (risk_indicator.sql, compliance_indicator.sql)
|
||||
-- =============================================================================
|
||||
|
||||
-- ※ column_name 컬럼이 이미 INSERT SQL에 NULL로 포함되어 있으므로
|
||||
-- ALTER TABLE은 column_name 컬럼이 없는 경우에만 실행
|
||||
-- ALTER TABLE std_snp_data.risk_indicator ADD COLUMN column_name varchar(100);
|
||||
-- ALTER TABLE std_snp_data.compliance_indicator ADD COLUMN column_name varchar(100);
|
||||
|
||||
|
||||
-- 1. UPDATE: Risk Indicator
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- AIS
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ais_notrcv_elps_days' WHERE field_key = 'Time since last seen on AIS';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ais_lwrnk_days' WHERE field_key = 'Days under AIS coverage (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'mmsi_anom_message' WHERE field_key = 'Anomalous AIS Messages from MMSI (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ais_up_imo_desc' WHERE field_key = 'IMO number transmitted correctly in AIS';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'othr_ship_nm_voy_yn' WHERE field_key = 'Sailing under name transmitted on AIS';
|
||||
|
||||
-- PORT_CALLS
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'port_prtcll' WHERE field_key = 'Port calls (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'recent_sanction_prtcll' WHERE field_key = 'Most recent sanctioned port call';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'port_risk' WHERE field_key = 'Highest ECR risk port call (last 12 months)';
|
||||
|
||||
-- ASSOCIATED_WITH_RUSSIA
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'rss_ownr_reg' WHERE field_key = 'Russian registration or ownership since February 2022';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'rss_port_call' WHERE field_key = 'Russian port calls since February 2022';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'rss_sts' WHERE field_key = 'Russian tanker STS since December 2022';
|
||||
|
||||
-- BEHAVIOURAL_RISK
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'recent_dark_actv' WHERE field_key = 'Most recent suspicious behavior detected';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'sts_job' WHERE field_key = 'Ship-to-Ship operations (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'draft_chg' WHERE field_key = 'Draught changes (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'drift_chg' WHERE field_key = 'Drifting high seas (last 12 months)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ilgl_fshr_viol' WHERE field_key = 'Illegal Unreported or Unregulated (IUU) Fishing Violation';
|
||||
|
||||
-- SAFETY_SECURITY_AND_INSPECTIONS
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'risk_event' WHERE field_key = 'Casualty & risk events (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'fltsfty' WHERE field_key = 'Fleet casualty & risk (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'vslage' WHERE field_key = 'Age of ship (compared to peer group average)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'psc_inspection' WHERE field_key = 'Inspection (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'psc_inspection_elps_hr' WHERE field_key = 'Time since last inspection';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'psc_defect' WHERE field_key = 'PSC defects (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'psc_detained' WHERE field_key = 'PSC detentions (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'now_smgrc_evdc' WHERE field_key = 'Current Safety Management Certificate inspected';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'flt_psc' WHERE field_key = 'Fleet PSC detentions (last 3 years)';
|
||||
|
||||
-- FLAG_RISK
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ntnlty_chg' WHERE field_key = 'Flag changes (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ntnlty_prs_mou_perf' WHERE field_key = 'Flag Paris MOU performance';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ntnlty_tky_mou_perf' WHERE field_key = 'Flag Tokyo MOU performance';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ntnlty_uscg_mou_perf' WHERE field_key = 'Flag US Coastguard MOU performance';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'uscg_excl_ship_cert' WHERE field_key = 'Flag US Coastguard Qualship 21';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'risk_data_maint' WHERE field_key = 'Risk Data Maintained For Vessel';
|
||||
|
||||
-- OWNER_AND_CLASSIFICATION
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'now_clfic' WHERE field_key = 'Classification Society';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'clfic_status_chg' WHERE field_key = 'Class status changes (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'spc_inspection_ovdue' WHERE field_key = 'Special survey overdue';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'pni_insrnc' WHERE field_key = 'P&I club check';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ship_nm_chg' WHERE field_key = 'Name changes (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'docc_chg' WHERE field_key = 'DOC company changes (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'gbo_chg' WHERE field_key = 'Group owner changes (last 3 years)';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'ownr_unk' WHERE field_key = 'Ownership unknown';
|
||||
UPDATE std_snp_data.risk_indicator SET column_name = 'sngl_ship_voy' WHERE field_key = 'Single-ship fleet (technical manager)';
|
||||
|
||||
|
||||
-- 2. UPDATE: Compliance Indicator - SHIP
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- ※ DB 컬럼이 없는 지표 (Suspicious Behavior, Ownership Screening 등)는 매핑하지 않음
|
||||
|
||||
-- Sanctions - Ship (US OFAC)
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ofac_sanction_list' WHERE field_key = 'Ship on OFAC Sanctions List (SDN)' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ofac_non_sdn_sanction_list' WHERE field_key = 'Ship on OFAC Consolidated (Non-SDN) List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ofac_cutn_list' WHERE field_key = 'Ship on OFAC Advisory List' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Sanctions - Ownership (US OFAC)
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_ofac_sanction_list' WHERE field_key = 'Ownership on OFAC Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_ofcs_sanction_list' WHERE field_key = 'Ownership on OFAC SSI List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_ofac_sanction_country' WHERE field_key = 'Ownership in OFAC Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_ofac_sanction_hstry' WHERE field_key = 'Historical Ownership in OFAC Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_prnt_company_ofac_sanction_country' WHERE field_key = 'Parent Company in OFAC Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Sanctions - Ship (Non-US)
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_un_sanction_list' WHERE field_key = 'Ship on UN Security Council Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_eu_sanction_list' WHERE field_key = 'Ship on EU Commission Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_swi_sanction_list' WHERE field_key = 'Ship on Swiss SECO Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_bes_sanction_list' WHERE field_key = 'Ship on HM Treasury (BES) Sanctions List' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Sanctions - Ownership (Non-US)
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_un_sanction_list' WHERE field_key = 'Ownership on UN Security Council Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_eu_sanction_list' WHERE field_key = 'Ownership on EU Commission Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_swi_sanction_list' WHERE field_key = 'Ownership on Swiss SECO Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_bes_sanction_list' WHERE field_key = 'Ownership on HM Treasury (BES) Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_can_sanction_list' WHERE field_key = 'Ownership on Government of Canada Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_aus_sanction_list' WHERE field_key = 'Ownership on Australian DFAT Sanctions List' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_uae_sanction_list' WHERE field_key = 'Ownership on UAE Sanctions List' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Sanctions - FATF
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_fatf_rgl_zone' WHERE field_key = 'Ownership in FATF High-risk or Non-cooperative Jurisdiction' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_prnt_company_fatf_rgl_zone' WHERE field_key = 'Parent Company in FATF High-risk or Non-cooperative Jurisdiction' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Sanctions - Other
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_ownr_prnt_company_ncmplnc' WHERE field_key = 'Parent Company Noncompliant' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_flg_sanction_country' WHERE field_key = 'Flag Country Sanctioned' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_flg_sanction_country_hstry' WHERE field_key = 'Historical Flag Country Sanctioned' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_scrty_lgl_dspt_event' WHERE field_key = 'Security and Legal Dispute Event (Last 3 Years)' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_flg_dspt' WHERE field_key = 'Flag (MMSI, Call Sign) False or Flag Unknown' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Port Calls
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_sanction_country_prtcll_last_thr_m' WHERE field_key = 'Port Call Last 3 Months to Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_sanction_country_prtcll_last_six_m' WHERE field_key = 'Port Call Last 180 Days to Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_sanction_country_prtcll_last_twelve_m' WHERE field_key = 'Port Call Last 12 Months to Sanctioned Country' AND indicator_type = 'SHIP';
|
||||
|
||||
-- STS Activity
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_sts_prtnr_non_compliance_twelve_m' WHERE field_key = 'STS Activity – Partner Ship Compliance Status' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Suspicious Behavior
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_dark_actv_ind' WHERE field_key = 'Dark for Extended Period in Watched Area (Severe)' AND indicator_type = 'SHIP';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'ship_dtld_info_ntmntd' WHERE field_key = 'Not Seen 7 Days Near Sanctioned/Sensitive Country' AND indicator_type = 'SHIP';
|
||||
|
||||
-- Compliance Screening History
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'lgl_snths_sanction' WHERE field_key = 'Overall Compliance Change History' AND indicator_type = 'SHIP';
|
||||
|
||||
|
||||
-- 3. UPDATE: Compliance Indicator - COMPANY
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- US Treasury Sanctions
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_ofac_sanction_list' WHERE field_key = 'Company on OFAC Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_ofac_non_sdn_sanction_list' WHERE field_key = 'Company on OFAC Non-SDN Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_ofacssi_sanction_list' WHERE field_key = 'Company on OFAC SSI Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_ofac_sanction_country' WHERE field_key = 'Company in US Treasury OFAC Sanctioned Country' AND indicator_type = 'COMPANY';
|
||||
|
||||
-- Non-US Sanctions
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_un_sanction_list' WHERE field_key = 'Company on UN Security Council Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_eu_sanction_list' WHERE field_key = 'Company on EU Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_bes_sanction_list' WHERE field_key = 'Company on HM Treasury (BES) Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_can_sanction_list' WHERE field_key = 'Company on Canadian Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_aus_sanction_list' WHERE field_key = 'Company on Australian DFAT Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_uae_sanction_list' WHERE field_key = 'Company on UAE Entity List' AND indicator_type = 'COMPANY';
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_swiss_sanction_list' WHERE field_key = 'Company on Swiss SECO Entity List' AND indicator_type = 'COMPANY';
|
||||
|
||||
-- FATF Jurisdiction
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_fatf_cmptnc_country' WHERE field_key = 'Company in FATF High-risk & Non-cooperative Jurisdiction' AND indicator_type = 'COMPANY';
|
||||
|
||||
-- Parent Company
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'prnt_company_compliance_risk' WHERE field_key = 'Parent Company Compliance Risk' AND indicator_type = 'COMPANY';
|
||||
|
||||
-- Compliance Screening Change History
|
||||
UPDATE std_snp_data.compliance_indicator SET column_name = 'company_snths_compliance_status' WHERE field_key = 'Historical Compliance Change Date (Company)' AND indicator_type = 'COMPANY';
|
||||
|
||||
|
||||
-- 4. 검증 쿼리
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- 매핑된 항목 수 확인
|
||||
SELECT 'risk_indicator' as table_name,
|
||||
COUNT(*) as total,
|
||||
COUNT(column_name) as mapped,
|
||||
COUNT(*) - COUNT(column_name) as unmapped
|
||||
FROM std_snp_data.risk_indicator
|
||||
UNION ALL
|
||||
SELECT 'compliance_indicator (SHIP)',
|
||||
COUNT(*), COUNT(column_name), COUNT(*) - COUNT(column_name)
|
||||
FROM std_snp_data.compliance_indicator WHERE indicator_type = 'SHIP'
|
||||
UNION ALL
|
||||
SELECT 'compliance_indicator (COMPANY)',
|
||||
COUNT(*), COUNT(column_name), COUNT(*) - COUNT(column_name)
|
||||
FROM std_snp_data.compliance_indicator WHERE indicator_type = 'COMPANY';
|
||||
|
||||
-- 매핑 안 된 항목 확인 (DB 컬럼 없는 지표 = 정상적으로 NULL)
|
||||
SELECT indicator_id, field_key, column_name
|
||||
FROM std_snp_data.risk_indicator WHERE column_name IS NULL;
|
||||
|
||||
SELECT indicator_id, field_key, indicator_type, column_name
|
||||
FROM std_snp_data.compliance_indicator WHERE column_name IS NULL;
|
||||
|
||||
-- 전체 매핑 확인 (다국어 포함)
|
||||
SELECT ri.indicator_id, ri.field_key, ri.column_name, ril.field_name
|
||||
FROM std_snp_data.risk_indicator ri
|
||||
LEFT JOIN std_snp_data.risk_indicator_lang ril
|
||||
ON ri.indicator_id = ril.indicator_id AND ril.lang_code = 'KO'
|
||||
ORDER BY ri.indicator_id;
|
||||
|
||||
SELECT ci.indicator_id, ci.field_key, ci.indicator_type, ci.column_name, cil.field_name
|
||||
FROM std_snp_data.compliance_indicator ci
|
||||
LEFT JOIN std_snp_data.compliance_indicator_lang cil
|
||||
ON ci.indicator_id = cil.indicator_id AND cil.lang_code = 'KO'
|
||||
ORDER BY ci.indicator_type, ci.indicator_id;
|
||||
@ -16,6 +16,7 @@ const Schedules = lazy(() => import('./pages/Schedules'));
|
||||
const Timeline = lazy(() => import('./pages/Timeline'));
|
||||
const BypassConfig = lazy(() => import('./pages/BypassConfig'));
|
||||
const ScreeningGuide = lazy(() => import('./pages/ScreeningGuide'));
|
||||
const RiskComplianceHistory = lazy(() => import('./pages/RiskComplianceHistory'));
|
||||
|
||||
function AppLayout() {
|
||||
const { toasts, removeToast } = useToastContext();
|
||||
@ -36,6 +37,7 @@ function AppLayout() {
|
||||
<Route path="/schedule-timeline" element={<Timeline />} />
|
||||
<Route path="/bypass-config" element={<BypassConfig />} />
|
||||
<Route path="/screening-guide" element={<ScreeningGuide />} />
|
||||
<Route path="/risk-compliance-history" element={<RiskComplianceHistory />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@ -53,6 +53,67 @@ export interface MethodologyHistoryResponse {
|
||||
collectionNote: string;
|
||||
}
|
||||
|
||||
// 값 변경 이력 타입
|
||||
export interface ChangeHistoryResponse {
|
||||
rowIndex: number;
|
||||
searchKey: string;
|
||||
lastModifiedDate: string;
|
||||
changedColumnName: string;
|
||||
beforeValue: string;
|
||||
afterValue: string;
|
||||
fieldName: string;
|
||||
narrative: string;
|
||||
prevNarrative: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
// 선박 기본 정보
|
||||
export interface ShipInfoResponse {
|
||||
imoNo: string;
|
||||
shipName: string;
|
||||
shipStatus: string;
|
||||
nationalityCode: string;
|
||||
nationalityIsoCode: string | null;
|
||||
nationality: string;
|
||||
shipType: string;
|
||||
dwt: string;
|
||||
gt: string;
|
||||
buildYear: string;
|
||||
mmsiNo: string;
|
||||
callSign: string;
|
||||
shipTypeGroup: string;
|
||||
}
|
||||
|
||||
// 회사 기본 정보
|
||||
export interface CompanyInfoResponse {
|
||||
companyCode: string;
|
||||
fullName: string;
|
||||
abbreviation: string;
|
||||
status: string;
|
||||
parentCompanyCode: string | null;
|
||||
parentCompanyName: string | null;
|
||||
registrationCountry: string;
|
||||
registrationCountryCode: string;
|
||||
registrationCountryIsoCode: string | null;
|
||||
controlCountry: string | null;
|
||||
controlCountryCode: string | null;
|
||||
controlCountryIsoCode: string | null;
|
||||
foundedDate: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
}
|
||||
|
||||
// 지표 현재 상태
|
||||
export interface IndicatorStatusResponse {
|
||||
columnName: string;
|
||||
fieldName: string;
|
||||
category: string;
|
||||
value: string | null;
|
||||
narrative: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const BASE = '/snp-api/api/screening-guide';
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
@ -68,4 +129,20 @@ export const screeningGuideApi = {
|
||||
fetchJson<ApiResponse<ComplianceCategoryResponse[]>>(`${BASE}/compliance-indicators?lang=${lang}&type=${type}`),
|
||||
getMethodologyHistory: (lang = 'KO') =>
|
||||
fetchJson<ApiResponse<MethodologyHistoryResponse[]>>(`${BASE}/methodology-history?lang=${lang}`),
|
||||
getShipRiskHistory: (imoNo: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<ChangeHistoryResponse[]>>(`${BASE}/history/ship-risk?imoNo=${imoNo}&lang=${lang}`),
|
||||
getShipComplianceHistory: (imoNo: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<ChangeHistoryResponse[]>>(`${BASE}/history/ship-compliance?imoNo=${imoNo}&lang=${lang}`),
|
||||
getCompanyComplianceHistory: (companyCode: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<ChangeHistoryResponse[]>>(`${BASE}/history/company-compliance?companyCode=${companyCode}&lang=${lang}`),
|
||||
getShipInfo: (imoNo: string) =>
|
||||
fetchJson<ApiResponse<ShipInfoResponse>>(`${BASE}/ship-info?imoNo=${imoNo}`),
|
||||
getShipRiskStatus: (imoNo: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<IndicatorStatusResponse[]>>(`${BASE}/ship-risk-status?imoNo=${imoNo}&lang=${lang}`),
|
||||
getShipComplianceStatus: (imoNo: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<IndicatorStatusResponse[]>>(`${BASE}/ship-compliance-status?imoNo=${imoNo}&lang=${lang}`),
|
||||
getCompanyInfo: (companyCode: string) =>
|
||||
fetchJson<ApiResponse<CompanyInfoResponse>>(`${BASE}/company-info?companyCode=${companyCode}`),
|
||||
getCompanyComplianceStatus: (companyCode: string, lang = 'KO') =>
|
||||
fetchJson<ApiResponse<IndicatorStatusResponse[]>>(`${BASE}/company-compliance-status?companyCode=${companyCode}&lang=${lang}`),
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ const navItems = [
|
||||
{ path: '/schedule-timeline', label: '타임라인', icon: '📅' },
|
||||
{ path: '/bypass-config', label: 'Bypass API', icon: '🔗' },
|
||||
{ path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' },
|
||||
{ path: '/risk-compliance-history', label: 'Change History', icon: '📜' },
|
||||
];
|
||||
|
||||
export default function Navbar() {
|
||||
|
||||
908
frontend/src/components/screening/HistoryTab.tsx
Normal file
908
frontend/src/components/screening/HistoryTab.tsx
Normal file
@ -0,0 +1,908 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { screeningGuideApi, type ChangeHistoryResponse, type ShipInfoResponse, type CompanyInfoResponse, type IndicatorStatusResponse } from '../../api/screeningGuideApi';
|
||||
|
||||
type HistoryType = 'ship-risk' | 'ship-compliance' | 'company-compliance';
|
||||
|
||||
interface HistoryTabProps {
|
||||
lang: string;
|
||||
}
|
||||
|
||||
const HISTORY_TYPES: {
|
||||
key: HistoryType;
|
||||
label: string;
|
||||
searchLabel: string;
|
||||
searchPlaceholder: string;
|
||||
overallColumn: string | null;
|
||||
}[] = [
|
||||
{
|
||||
key: 'ship-risk',
|
||||
label: '선박 위험지표',
|
||||
searchLabel: 'IMO 번호',
|
||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
||||
overallColumn: null,
|
||||
},
|
||||
{
|
||||
key: 'ship-compliance',
|
||||
label: '선박 제재',
|
||||
searchLabel: 'IMO 번호',
|
||||
searchPlaceholder: 'IMO 번호 : 9672533',
|
||||
overallColumn: 'lgl_snths_sanction',
|
||||
},
|
||||
{
|
||||
key: 'company-compliance',
|
||||
label: '회사 제재',
|
||||
searchLabel: '회사 코드',
|
||||
searchPlaceholder: '회사 코드 : 1288896',
|
||||
overallColumn: 'company_snths_compliance_status',
|
||||
},
|
||||
];
|
||||
|
||||
interface GroupedHistory {
|
||||
lastModifiedDate: string;
|
||||
items: ChangeHistoryResponse[];
|
||||
overallBefore: string | null;
|
||||
overallAfter: string | null;
|
||||
}
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
'0': { label: 'All Clear', className: 'bg-green-100 text-green-800 border-green-300' },
|
||||
'1': { label: 'Warning', className: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
||||
'2': { label: 'Severe', className: 'bg-red-100 text-red-800 border-red-300' },
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
'0': '#22c55e',
|
||||
'1': '#eab308',
|
||||
'2': '#ef4444',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
'0': 'All Clear',
|
||||
'1': 'Warning',
|
||||
'2': 'Severe',
|
||||
};
|
||||
|
||||
function StatusBadge({ value }: { value: string | null }) {
|
||||
if (value == null || value === '') return null;
|
||||
const status = STATUS_MAP[value];
|
||||
if (!status) return <span className="text-xs text-wing-muted">{value}</span>;
|
||||
return (
|
||||
<span className={`inline-block rounded-full px-3 py-0.5 text-xs font-bold border ${status.className}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function countryFlag(code: string | null | undefined): string {
|
||||
if (!code || code.length < 2) return '';
|
||||
const cc = code.slice(0, 2).toUpperCase();
|
||||
const codePoints = [...cc].map((c) => 0x1F1E6 + c.charCodeAt(0) - 65);
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
function RiskValueCell({ value, narrative }: { value: string | null; narrative?: string }) {
|
||||
if (value == null || value === '') return null;
|
||||
const color = STATUS_COLORS[value] ?? '#6b7280';
|
||||
const label = narrative || STATUS_LABELS[value] || value;
|
||||
return (
|
||||
<div className="inline-flex items-start gap-1.5">
|
||||
<span style={{ color }} className="text-sm leading-tight">●</span>
|
||||
<span className="text-xs text-wing-text leading-relaxed text-left">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const COMPLIANCE_STATUS: Record<string, { label: string; className: string }> = {
|
||||
'0': { label: 'No', className: 'bg-green-100 text-green-800' },
|
||||
'1': { label: 'Warning', className: 'bg-yellow-100 text-yellow-800' },
|
||||
'2': { label: 'Yes', className: 'bg-red-100 text-red-800' },
|
||||
};
|
||||
|
||||
function getRiskLabel(item: IndicatorStatusResponse): string {
|
||||
// IUU Fishing: All Clear -> None recorded
|
||||
if (item.columnName === 'ilgl_fshr_viol' && item.value === '0') return 'None recorded';
|
||||
// Risk Data Maintained: 0 -> Yes, 1 -> Not Maintained
|
||||
if (item.columnName === 'risk_data_maint') {
|
||||
if (item.value === '0') return 'Yes';
|
||||
if (item.value === '1') return 'Not Maintained';
|
||||
}
|
||||
return item.narrative || STATUS_LABELS[item.value ?? ''] || item.value || '-';
|
||||
}
|
||||
|
||||
function RiskStatusGrid({ items }: { items: IndicatorStatusResponse[] }) {
|
||||
// Risk Data Maintained 체크: Not Maintained(1)이면 해당 지표만 표시
|
||||
const riskDataMaintained = items.find((i) => i.columnName === 'risk_data_maint');
|
||||
const isNotMaintained = riskDataMaintained?.value === '1';
|
||||
|
||||
const displayItems = isNotMaintained
|
||||
? items.filter((i) => i.columnName === 'risk_data_maint')
|
||||
: items;
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const map = new Map<string, IndicatorStatusResponse[]>();
|
||||
for (const item of displayItems) {
|
||||
const cat = item.category || 'Other';
|
||||
if (!map.has(cat)) map.set(cat, []);
|
||||
map.get(cat)!.push(item);
|
||||
}
|
||||
return Array.from(map.entries());
|
||||
}, [displayItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isNotMaintained && (
|
||||
<div className="mb-3 px-3 py-2 bg-yellow-50 border border-yellow-300 rounded-lg text-xs text-yellow-800 font-medium">
|
||||
Risk Data is not maintained for this vessel. Only the maintenance status is shown.
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{categories.map(([category, catItems]) => (
|
||||
<div key={category}>
|
||||
<div className="text-xs font-bold text-wing-text mb-2 pb-1 border-b border-wing-border">
|
||||
{category}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => {
|
||||
const color = STATUS_COLORS[item.value ?? ''] ?? '#6b7280';
|
||||
const label = getRiskLabel(item);
|
||||
return (
|
||||
<div key={item.columnName} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-wing-text truncate flex-1">{item.fieldName}</span>
|
||||
<span style={{ color }} className="shrink-0 text-sm">●</span>
|
||||
<span
|
||||
className="shrink-0 rounded px-2 py-0.5 text-[10px] font-bold text-wing-text text-center bg-wing-surface border border-wing-border"
|
||||
style={{ minWidth: '140px' }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 선박 Compliance 탭 분류
|
||||
const SHIP_COMPLIANCE_TABS: { key: string; label: string; match: (category: string) => boolean }[] = [
|
||||
{ key: 'sanctions', label: 'Sanctions', match: (cat) => cat.includes('Sanctions') || cat.includes('FATF') || cat.includes('Other Compliance') },
|
||||
{ key: 'portcalls', label: 'Port Calls', match: (cat) => cat === 'Port Calls' },
|
||||
{ key: 'sts', label: 'STS Activity', match: (cat) => cat === 'STS Activity' },
|
||||
{ key: 'suspicious', label: 'Suspicious Behavior', match: (cat) => cat === 'Suspicious Behavior' },
|
||||
];
|
||||
|
||||
// Compliance 예외 처리
|
||||
function getComplianceLabel(item: IndicatorStatusResponse, isCompany: boolean): string | null {
|
||||
// Parent Company 관련: null -> No Parent
|
||||
if (item.value == null || item.value === '') {
|
||||
if (item.fieldName.includes('Parent Company') || item.fieldName.includes('Parent company')) return 'No Parent';
|
||||
if (isCompany && item.columnName === 'prnt_company_compliance_risk') return 'No Parent';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 제외할 컬럼명
|
||||
const SHIP_COMPLIANCE_EXCLUDE = ['lgl_snths_sanction']; // Overall은 토글 헤더에 표시
|
||||
const COMPANY_COMPLIANCE_EXCLUDE = ['company_snths_compliance_status']; // Overall은 토글 헤더에 표시
|
||||
|
||||
function ComplianceStatusItem({ item, isCompany }: { item: IndicatorStatusResponse; isCompany: boolean }) {
|
||||
const overrideLabel = getComplianceLabel(item, isCompany);
|
||||
const status = overrideLabel ? null : COMPLIANCE_STATUS[item.value ?? ''];
|
||||
const displayLabel = overrideLabel || (status ? status.label : (item.value ?? '-'));
|
||||
const displayClass = overrideLabel
|
||||
? 'bg-wing-surface text-wing-muted border border-wing-border'
|
||||
: status ? status.className : 'bg-wing-surface text-wing-muted border border-wing-border';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-wing-text truncate flex-1">{item.fieldName}</span>
|
||||
<span
|
||||
className={`shrink-0 rounded px-2 py-0.5 text-[10px] font-bold text-center ${displayClass}`}
|
||||
style={{ minWidth: '80px' }}
|
||||
>
|
||||
{displayLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComplianceStatusGrid({ items, isCompany }: { items: IndicatorStatusResponse[]; isCompany: boolean }) {
|
||||
const [activeTab, setActiveTab] = useState('sanctions');
|
||||
|
||||
// 제외 항목 필터링
|
||||
const excludeList = isCompany ? COMPANY_COMPLIANCE_EXCLUDE : SHIP_COMPLIANCE_EXCLUDE;
|
||||
const filteredItems = items.filter((i) => !excludeList.includes(i.columnName));
|
||||
|
||||
// 회사: 탭 없이 2컬럼 그리드
|
||||
if (isCompany) {
|
||||
const categories = useMemo(() => {
|
||||
const map = new Map<string, IndicatorStatusResponse[]>();
|
||||
for (const item of filteredItems) {
|
||||
const cat = item.category || 'Other';
|
||||
if (!map.has(cat)) map.set(cat, []);
|
||||
map.get(cat)!.push(item);
|
||||
}
|
||||
return Array.from(map.entries());
|
||||
}, [filteredItems]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{categories.map(([category, catItems]) => (
|
||||
<div key={category}>
|
||||
{categories.length > 1 && (
|
||||
<div className="text-xs font-bold text-wing-text mb-2 pb-1 border-b border-wing-border">
|
||||
{category}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => (
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 선박: 탭 기반 분류
|
||||
const tabData = useMemo(() => {
|
||||
const result: Record<string, IndicatorStatusResponse[]> = {};
|
||||
for (const tab of SHIP_COMPLIANCE_TABS) {
|
||||
result[tab.key] = filteredItems.filter((i) => tab.match(i.category));
|
||||
}
|
||||
return result;
|
||||
}, [filteredItems]);
|
||||
|
||||
const currentItems = tabData[activeTab] ?? [];
|
||||
|
||||
// 현재 탭 내 카테고리별 그룹핑
|
||||
const categories = useMemo(() => {
|
||||
const map = new Map<string, IndicatorStatusResponse[]>();
|
||||
for (const item of currentItems) {
|
||||
const cat = item.category || 'Other';
|
||||
if (!map.has(cat)) map.set(cat, []);
|
||||
map.get(cat)!.push(item);
|
||||
}
|
||||
return Array.from(map.entries());
|
||||
}, [currentItems]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 탭 버튼 */}
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{SHIP_COMPLIANCE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all border ${
|
||||
activeTab === tab.key
|
||||
? 'bg-slate-900 text-white border-slate-900'
|
||||
: 'bg-wing-card text-wing-muted border-wing-border hover:text-wing-text hover:bg-wing-hover'
|
||||
}`}
|
||||
>
|
||||
{tab.label} ({tabData[tab.key]?.length ?? 0})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 현재 탭 내용 */}
|
||||
{currentItems.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{categories.map(([category, catItems]) => (
|
||||
<div key={category}>
|
||||
{categories.length > 1 && (
|
||||
<div className="text-xs font-bold text-wing-text mb-2 pb-1 border-b border-wing-border">
|
||||
{category}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
{catItems.map((item) => (
|
||||
<ComplianceStatusItem key={item.columnName} item={item} isCompany={isCompany} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-xs text-wing-muted py-4">해당 항목이 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HistoryTab({ lang }: HistoryTabProps) {
|
||||
const [historyType, setHistoryType] = useState<HistoryType>('ship-risk');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [cache, setCache] = useState<Record<string, ChangeHistoryResponse[]>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searched, setSearched] = useState(false);
|
||||
const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set());
|
||||
const [shipInfo, setShipInfo] = useState<ShipInfoResponse | null>(null);
|
||||
const [companyInfo, setCompanyInfo] = useState<CompanyInfoResponse | null>(null);
|
||||
const [riskStatusCache, setRiskStatusCache] = useState<Record<string, IndicatorStatusResponse[]>>({});
|
||||
const [complianceStatusCache, setComplianceStatusCache] = useState<Record<string, IndicatorStatusResponse[]>>({});
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['info', 'risk', 'compliance', 'history']));
|
||||
|
||||
const currentType = HISTORY_TYPES.find((t) => t.key === historyType)!;
|
||||
const isRisk = historyType === 'ship-risk';
|
||||
const data = cache[lang] ?? [];
|
||||
const riskStatus = riskStatusCache[lang] ?? [];
|
||||
const complianceStatus = complianceStatusCache[lang] ?? [];
|
||||
|
||||
const grouped: GroupedHistory[] = useMemo(() => {
|
||||
const map = new Map<string, ChangeHistoryResponse[]>();
|
||||
for (const item of data) {
|
||||
const key = item.lastModifiedDate;
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(item);
|
||||
}
|
||||
return Array.from(map.entries()).map(([date, items]) => {
|
||||
const overallColumn = currentType.overallColumn;
|
||||
if (overallColumn) {
|
||||
const overallItem = items.find((item) => item.changedColumnName === overallColumn);
|
||||
return {
|
||||
lastModifiedDate: date,
|
||||
items,
|
||||
overallBefore: overallItem?.beforeValue ?? null,
|
||||
overallAfter: overallItem?.afterValue ?? null,
|
||||
};
|
||||
}
|
||||
return { lastModifiedDate: date, items, overallBefore: null, overallAfter: null };
|
||||
});
|
||||
}, [data, currentType.overallColumn]);
|
||||
|
||||
function handleSearch() {
|
||||
const trimmed = searchValue.trim();
|
||||
if (!trimmed) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSearched(true);
|
||||
setExpandedDates(new Set());
|
||||
setExpandedSections(new Set(['info', 'risk', 'compliance', 'history']));
|
||||
|
||||
const isShip = historyType !== 'company-compliance';
|
||||
const showRisk = historyType === 'ship-risk';
|
||||
|
||||
const getHistoryCall = (l: string) =>
|
||||
historyType === 'ship-risk'
|
||||
? screeningGuideApi.getShipRiskHistory(trimmed, l)
|
||||
: historyType === 'ship-compliance'
|
||||
? screeningGuideApi.getShipComplianceHistory(trimmed, l)
|
||||
: screeningGuideApi.getCompanyComplianceHistory(trimmed, l);
|
||||
|
||||
const promises: Promise<any>[] = [
|
||||
getHistoryCall('KO'),
|
||||
getHistoryCall('EN'),
|
||||
];
|
||||
|
||||
if (isShip) {
|
||||
promises.push(
|
||||
screeningGuideApi.getShipInfo(trimmed),
|
||||
screeningGuideApi.getShipComplianceStatus(trimmed, 'KO'),
|
||||
screeningGuideApi.getShipComplianceStatus(trimmed, 'EN'),
|
||||
);
|
||||
if (showRisk) {
|
||||
promises.push(
|
||||
screeningGuideApi.getShipRiskStatus(trimmed, 'KO'),
|
||||
screeningGuideApi.getShipRiskStatus(trimmed, 'EN'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
promises.push(
|
||||
screeningGuideApi.getCompanyInfo(trimmed),
|
||||
screeningGuideApi.getCompanyComplianceStatus(trimmed, 'KO'),
|
||||
screeningGuideApi.getCompanyComplianceStatus(trimmed, 'EN'),
|
||||
);
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then((results) => {
|
||||
setCache({ KO: results[0].data ?? [], EN: results[1].data ?? [] });
|
||||
if (isShip) {
|
||||
setShipInfo(results[2].data);
|
||||
setCompanyInfo(null);
|
||||
setComplianceStatusCache({ KO: results[3].data ?? [], EN: results[4].data ?? [] });
|
||||
if (showRisk) {
|
||||
setRiskStatusCache({ KO: results[5].data ?? [], EN: results[6].data ?? [] });
|
||||
} else {
|
||||
setRiskStatusCache({});
|
||||
}
|
||||
} else {
|
||||
setShipInfo(null);
|
||||
setCompanyInfo(results[2].data);
|
||||
setRiskStatusCache({});
|
||||
setComplianceStatusCache({ KO: results[3].data ?? [], EN: results[4].data ?? [] });
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === 'Enter') handleSearch();
|
||||
}
|
||||
|
||||
function handleTypeChange(type: HistoryType) {
|
||||
setHistoryType(type);
|
||||
setSearchValue('');
|
||||
setCache({});
|
||||
setError(null);
|
||||
setSearched(false);
|
||||
setExpandedDates(new Set());
|
||||
setShipInfo(null);
|
||||
setCompanyInfo(null);
|
||||
setRiskStatusCache({});
|
||||
setComplianceStatusCache({});
|
||||
setExpandedSections(new Set(['info', 'risk', 'compliance', 'history']));
|
||||
}
|
||||
|
||||
function toggleSection(section: string) {
|
||||
setExpandedSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) next.delete(section);
|
||||
else next.add(section);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDate(date: string) {
|
||||
setExpandedDates((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(date)) next.delete(date);
|
||||
else next.add(date);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 이력 유형 선택 */}
|
||||
<div className="flex gap-2 flex-wrap justify-center">
|
||||
{HISTORY_TYPES.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => handleTypeChange(t.key)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all border ${
|
||||
historyType === t.key
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="flex gap-3 items-center justify-center">
|
||||
<div className="relative flex-1 min-w-[200px]">
|
||||
<span className="absolute inset-y-0 left-3 flex items-center text-wing-muted">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={currentType.searchPlaceholder}
|
||||
className="w-full pl-10 pr-8 py-2 border border-wing-border rounded-lg text-sm
|
||||
focus:ring-2 focus:ring-wing-accent focus:border-wing-accent outline-none"
|
||||
/>
|
||||
{searchValue && (
|
||||
<button
|
||||
onClick={() => setSearchValue('')}
|
||||
className="absolute inset-y-0 right-3 flex items-center text-wing-muted hover:text-wing-text"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={!searchValue.trim() || loading}
|
||||
className="px-5 py-2 rounded-lg bg-slate-900 text-white text-sm font-bold hover:bg-slate-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? '조회 중...' : '조회'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 에러 */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-300 rounded-xl p-4 text-red-800 text-sm">
|
||||
<strong>조회 실패:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 결과: 3개 섹션 */}
|
||||
{searched && !loading && !error && (
|
||||
<div className="space-y-3">
|
||||
{/* Section 1: 기본 정보 */}
|
||||
{(shipInfo || companyInfo) && (
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('info')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('info') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">
|
||||
{shipInfo ? '선박 기본 정보' : '회사 기본 정보'}
|
||||
</span>
|
||||
</button>
|
||||
{expandedSections.has('info') && (
|
||||
<div className="border-t border-wing-border px-4 py-4">
|
||||
{shipInfo && (
|
||||
<div className="flex gap-6">
|
||||
{/* 좌측: 핵심 식별 정보 */}
|
||||
<div className="min-w-[220px] space-y-2">
|
||||
<div>
|
||||
<div className="text-lg font-bold text-wing-text">{shipInfo.shipName || '-'}</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-12">IMO</span>
|
||||
<span className="font-mono font-medium text-wing-text">{shipInfo.imoNo}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-12">MMSI</span>
|
||||
<span className="font-mono font-medium text-wing-text">{shipInfo.mmsiNo || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-12">Status</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.shipStatus || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="w-px bg-wing-border" />
|
||||
|
||||
{/* 우측: 스펙 정보 */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">국적</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(shipInfo.nationalityIsoCode)} {shipInfo.nationality || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">선종</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.shipType || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">DWT</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.dwt || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">GT</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.gt || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">건조연도</span>
|
||||
<span className="font-medium text-wing-text">{shipInfo.buildYear || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo && (
|
||||
<div className="flex gap-6">
|
||||
{/* 좌측: 핵심 식별 정보 */}
|
||||
<div className="min-w-[220px] space-y-2">
|
||||
<div>
|
||||
<div className="text-lg font-bold text-wing-text">{companyInfo.fullName || '-'}</div>
|
||||
{companyInfo.abbreviation && (
|
||||
<div className="text-xs text-wing-muted">{companyInfo.abbreviation}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">Code</span>
|
||||
<span className="font-mono font-medium text-wing-text">{companyInfo.companyCode}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">Status</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.status || '-'}</span>
|
||||
</div>
|
||||
{companyInfo.parentCompanyName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">모회사</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.parentCompanyName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="w-px bg-wing-border" />
|
||||
|
||||
{/* 우측: 상세 정보 */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">등록국가</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(companyInfo.registrationCountryIsoCode)} {companyInfo.registrationCountry || '-'}
|
||||
</span>
|
||||
</div>
|
||||
{companyInfo.controlCountry && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">관리국가</span>
|
||||
<span className="font-medium text-wing-text">
|
||||
{countryFlag(companyInfo.controlCountryIsoCode)} {companyInfo.controlCountry}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.foundedDate && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">설립일</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.foundedDate}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.email && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">이메일</span>
|
||||
<span className="font-medium text-wing-text truncate">{companyInfo.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.phone && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">전화</span>
|
||||
<span className="font-medium text-wing-text">{companyInfo.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{companyInfo.website && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-wing-muted w-16">웹사이트</span>
|
||||
<a
|
||||
href={companyInfo.website.startsWith('http') ? companyInfo.website : `https://${companyInfo.website}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-600 hover:underline truncate"
|
||||
>
|
||||
{companyInfo.website}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 2: Current Risk Indicators (선박 탭만) */}
|
||||
{riskStatus.length > 0 && (
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('risk')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('risk') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">Current Risk Indicators</span>
|
||||
</button>
|
||||
{expandedSections.has('risk') && (
|
||||
<div className="border-t border-wing-border px-4 py-4">
|
||||
<RiskStatusGrid items={riskStatus} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 3: Current Compliance (선박 제재/회사 제재 탭만) */}
|
||||
{complianceStatus.length > 0 && !isRisk && (() => {
|
||||
const isCompany = historyType === 'company-compliance';
|
||||
const overallItem = isCompany
|
||||
? complianceStatus.find((i) => i.columnName === 'company_snths_compliance_status')
|
||||
: null;
|
||||
return (
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('compliance')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('compliance') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">Current Compliance</span>
|
||||
{overallItem && (
|
||||
<StatusBadge value={overallItem.value} />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections.has('compliance') && (
|
||||
<div className="border-t border-wing-border px-4 py-4">
|
||||
<ComplianceStatusGrid items={complianceStatus} isCompany={isCompany} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Section 4: 값 변경 이력 */}
|
||||
<div className="bg-wing-surface rounded-xl shadow-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('history')}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<span className={`text-xs transition-transform ${expandedSections.has('history') ? 'rotate-90' : ''}`}>▶</span>
|
||||
<span className="text-sm font-semibold text-wing-text">값 변경 이력</span>
|
||||
<span className="text-xs text-wing-muted">
|
||||
{grouped.length}개 일시, {data.length}건 변동
|
||||
</span>
|
||||
</button>
|
||||
{expandedSections.has('history') && (
|
||||
<div className="border-t border-wing-border">
|
||||
{grouped.length > 0 ? (
|
||||
<div className="space-y-2 p-2">
|
||||
{grouped.map((group) => {
|
||||
const isExpanded = expandedDates.has(group.lastModifiedDate);
|
||||
const hasOverall =
|
||||
group.overallBefore != null || group.overallAfter != null;
|
||||
const displayItems = (currentType.overallColumn
|
||||
? group.items.filter(
|
||||
(item) => item.changedColumnName !== currentType.overallColumn,
|
||||
)
|
||||
: [...group.items]
|
||||
).sort((a, b) => (a.sortOrder ?? Infinity) - (b.sortOrder ?? Infinity));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.lastModifiedDate}
|
||||
className="bg-wing-surface rounded-xl shadow-md overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleDate(group.lastModifiedDate)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-wing-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span
|
||||
className={`text-xs transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span className="text-sm text-wing-muted">Change Date :</span>
|
||||
<span className="text-sm font-semibold text-wing-text">
|
||||
{group.lastModifiedDate}
|
||||
</span>
|
||||
{hasOverall ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusBadge value={group.overallBefore} />
|
||||
<span className="text-xs text-wing-muted">→</span>
|
||||
<StatusBadge value={group.overallAfter} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-wing-muted">
|
||||
{displayItems.length}건 변동
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasOverall && (
|
||||
<span className="text-xs text-wing-muted">
|
||||
{displayItems.length}건 변동
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && displayItems.length > 0 && (
|
||||
<div className="border-t border-wing-border">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900 dark:bg-slate-700 text-white">
|
||||
<th
|
||||
style={{ width: '50%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
필드명
|
||||
</th>
|
||||
<th
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
이전값
|
||||
</th>
|
||||
<th
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2 text-center font-semibold"
|
||||
>
|
||||
이후값
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayItems.map((row) => (
|
||||
<tr
|
||||
key={row.rowIndex}
|
||||
className="border-b border-wing-border even:bg-wing-card"
|
||||
>
|
||||
<td
|
||||
style={{ width: '50%' }}
|
||||
className="px-4 py-2.5 text-wing-text"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{row.fieldName || row.changedColumnName}
|
||||
</div>
|
||||
{row.fieldName && (
|
||||
<div className="text-wing-muted mt-0.5">
|
||||
{row.changedColumnName}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
{isRisk ? (
|
||||
<>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<RiskValueCell value={row.beforeValue} narrative={row.prevNarrative ?? undefined} />
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<RiskValueCell
|
||||
value={row.afterValue}
|
||||
narrative={row.narrative ?? undefined}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<StatusBadge value={row.beforeValue} />
|
||||
</td>
|
||||
<td
|
||||
style={{ width: '25%' }}
|
||||
className="px-4 py-2.5 text-center"
|
||||
>
|
||||
<StatusBadge value={row.afterValue} />
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8 text-wing-muted">
|
||||
<div className="text-center">
|
||||
<div className="text-xl mb-1">📭</div>
|
||||
<div className="text-sm">변경 이력이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 초기 상태 */}
|
||||
{!searched && (
|
||||
<div className="flex items-center justify-center py-16 text-wing-muted">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-3">🔍</div>
|
||||
<div className="text-sm">{currentType.searchLabel}을(를) 입력하고 조회하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
frontend/src/pages/RiskComplianceHistory.tsx
Normal file
51
frontend/src/pages/RiskComplianceHistory.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import HistoryTab from '../components/screening/HistoryTab';
|
||||
|
||||
export default function RiskComplianceHistory() {
|
||||
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 Change History
|
||||
</h1>
|
||||
<p className="text-sm opacity-85">
|
||||
위험 지표 및 컴플라이언스 값 변경 이력
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 언어 토글 */}
|
||||
<div className="flex justify-end">
|
||||
<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>
|
||||
|
||||
<HistoryTab lang={lang} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
package com.snp.batch.global.controller;
|
||||
|
||||
import com.snp.batch.common.web.ApiResponse;
|
||||
import com.snp.batch.global.dto.screening.ChangeHistoryResponse;
|
||||
import com.snp.batch.global.dto.screening.CompanyInfoResponse;
|
||||
import com.snp.batch.global.dto.screening.ComplianceCategoryResponse;
|
||||
import com.snp.batch.global.dto.screening.IndicatorStatusResponse;
|
||||
import com.snp.batch.global.dto.screening.MethodologyHistoryResponse;
|
||||
import com.snp.batch.global.dto.screening.RiskCategoryResponse;
|
||||
import com.snp.batch.global.dto.screening.ShipInfoResponse;
|
||||
import com.snp.batch.service.ScreeningGuideService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -55,4 +59,80 @@ public class ScreeningGuideController {
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getMethodologyHistory(lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "선박 위험지표 값 변경 이력", description = "IMO 번호로 선박 위험지표 값 변경 이력을 조회합니다.")
|
||||
@GetMapping("/history/ship-risk")
|
||||
public ResponseEntity<ApiResponse<List<ChangeHistoryResponse>>> getShipRiskDetailHistory(
|
||||
@Parameter(description = "IMO 번호", example = "9330019", required = true)
|
||||
@RequestParam String imoNo,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getShipRiskDetailHistory(imoNo, lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "선박 제재 값 변경 이력", description = "IMO 번호로 선박 제재 값 변경 이력을 조회합니다.")
|
||||
@GetMapping("/history/ship-compliance")
|
||||
public ResponseEntity<ApiResponse<List<ChangeHistoryResponse>>> getShipComplianceHistory(
|
||||
@Parameter(description = "IMO 번호", example = "9330019", required = true)
|
||||
@RequestParam String imoNo,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getShipComplianceHistory(imoNo, lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "회사 제재 값 변경 이력", description = "회사 코드로 회사 제재 값 변경 이력을 조회합니다.")
|
||||
@GetMapping("/history/company-compliance")
|
||||
public ResponseEntity<ApiResponse<List<ChangeHistoryResponse>>> getCompanyComplianceHistory(
|
||||
@Parameter(description = "회사 코드", example = "5765290", required = true)
|
||||
@RequestParam String companyCode,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getCompanyComplianceHistory(companyCode, lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "선박 기본 정보", description = "IMO 번호로 선박 기본 정보를 조회합니다.")
|
||||
@GetMapping("/ship-info")
|
||||
public ResponseEntity<ApiResponse<ShipInfoResponse>> getShipInfo(
|
||||
@Parameter(description = "IMO 번호", example = "9672533", required = true)
|
||||
@RequestParam String imoNo) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getShipInfo(imoNo)));
|
||||
}
|
||||
|
||||
@Operation(summary = "선박 현재 Risk 지표 상태", description = "IMO 번호로 선박의 현재 Risk 지표 상태를 조회합니다.")
|
||||
@GetMapping("/ship-risk-status")
|
||||
public ResponseEntity<ApiResponse<List<IndicatorStatusResponse>>> getShipRiskStatus(
|
||||
@Parameter(description = "IMO 번호", example = "9672533", required = true)
|
||||
@RequestParam String imoNo,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getShipRiskStatus(imoNo, lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "선박 현재 Compliance 상태", description = "IMO 번호로 선박의 현재 Compliance 상태를 조회합니다.")
|
||||
@GetMapping("/ship-compliance-status")
|
||||
public ResponseEntity<ApiResponse<List<IndicatorStatusResponse>>> getShipComplianceStatus(
|
||||
@Parameter(description = "IMO 번호", example = "9672533", required = true)
|
||||
@RequestParam String imoNo,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getShipComplianceStatus(imoNo, lang)));
|
||||
}
|
||||
|
||||
@Operation(summary = "회사 기본 정보", description = "회사 코드로 회사 기본 정보를 조회합니다.")
|
||||
@GetMapping("/company-info")
|
||||
public ResponseEntity<ApiResponse<CompanyInfoResponse>> getCompanyInfo(
|
||||
@Parameter(description = "회사 코드", example = "1288896", required = true)
|
||||
@RequestParam String companyCode) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getCompanyInfo(companyCode)));
|
||||
}
|
||||
|
||||
@Operation(summary = "회사 현재 Compliance 상태", description = "회사 코드로 회사의 현재 Compliance 상태를 조회합니다.")
|
||||
@GetMapping("/company-compliance-status")
|
||||
public ResponseEntity<ApiResponse<List<IndicatorStatusResponse>>> getCompanyComplianceStatus(
|
||||
@Parameter(description = "회사 코드", example = "1288896", required = true)
|
||||
@RequestParam String companyCode,
|
||||
@Parameter(description = "언어 코드", example = "KO")
|
||||
@RequestParam(defaultValue = "KO") String lang) {
|
||||
return ResponseEntity.ok(ApiResponse.success(screeningGuideService.getCompanyComplianceStatus(companyCode, lang)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,10 +15,10 @@ public class WebViewController {
|
||||
@GetMapping({"/", "/jobs", "/executions", "/executions/{id:\\d+}",
|
||||
"/recollects", "/recollects/{id:\\d+}",
|
||||
"/schedules", "/schedule-timeline", "/monitoring",
|
||||
"/bypass-config", "/screening-guide",
|
||||
"/bypass-config", "/screening-guide", "/risk-compliance-history",
|
||||
"/jobs/**", "/executions/**", "/recollects/**",
|
||||
"/schedules/**", "/schedule-timeline/**", "/monitoring/**",
|
||||
"/bypass-config/**", "/screening-guide/**"})
|
||||
"/bypass-config/**", "/screening-guide/**", "/risk-compliance-history/**"})
|
||||
public String forward() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
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 ChangeHistoryResponse {
|
||||
|
||||
private Long rowIndex;
|
||||
private String searchKey;
|
||||
private String lastModifiedDate;
|
||||
private String changedColumnName;
|
||||
private String beforeValue;
|
||||
private String afterValue;
|
||||
private String fieldName;
|
||||
private String narrative;
|
||||
private String prevNarrative;
|
||||
private Integer sortOrder;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.snp.batch.global.dto.screening;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CompanyInfoResponse {
|
||||
private String companyCode;
|
||||
private String fullName;
|
||||
private String abbreviation;
|
||||
private String status;
|
||||
private String parentCompanyCode;
|
||||
private String parentCompanyName;
|
||||
private String registrationCountry;
|
||||
private String registrationCountryCode;
|
||||
private String registrationCountryIsoCode;
|
||||
private String controlCountry;
|
||||
private String controlCountryCode;
|
||||
private String controlCountryIsoCode;
|
||||
private String foundedDate;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String website;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.snp.batch.global.dto.screening;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IndicatorStatusResponse {
|
||||
private String columnName;
|
||||
private String fieldName;
|
||||
private String category;
|
||||
private String value;
|
||||
private String narrative;
|
||||
private Integer sortOrder;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.snp.batch.global.dto.screening;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShipInfoResponse {
|
||||
private String imoNo;
|
||||
private String shipName;
|
||||
private String shipStatus;
|
||||
private String nationalityCode;
|
||||
private String nationalityIsoCode;
|
||||
private String nationality;
|
||||
private String shipType;
|
||||
private String dwt;
|
||||
private String gt;
|
||||
private String buildYear;
|
||||
private String mmsiNo;
|
||||
private String callSign;
|
||||
private String shipTypeGroup;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 회사 제재 값 변경 이력 (읽기 전용)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "tb_company_compliance_info_hstry", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class CompanyComplianceHistory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "row_index")
|
||||
private Long rowIndex;
|
||||
|
||||
@Column(name = "company_cd", nullable = false, length = 14)
|
||||
private String companyCode;
|
||||
|
||||
@Column(name = "last_mdfcn_dt", nullable = false)
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
@Column(name = "flctn_col_nm", nullable = false, length = 100)
|
||||
private String changedColumnName;
|
||||
|
||||
@Column(name = "bfr_val", length = 4)
|
||||
private String beforeValue;
|
||||
|
||||
@Column(name = "aftr_val", length = 4)
|
||||
private String afterValue;
|
||||
|
||||
@Column(name = "crt_dt", nullable = false)
|
||||
private LocalDateTime createdDate;
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.snp.batch.global.model.screening;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tb_company_dtl_info", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class CompanyDetailInfo {
|
||||
|
||||
@Id
|
||||
@Column(name = "company_cd", length = 14)
|
||||
private String companyCode;
|
||||
|
||||
@Column(name = "full_nm", length = 280)
|
||||
private String fullName;
|
||||
|
||||
@Column(name = "company_name_abbr", length = 60)
|
||||
private String abbreviation;
|
||||
|
||||
@Column(name = "country_nm", length = 40)
|
||||
private String country;
|
||||
|
||||
@Column(name = "cty_nm", length = 200)
|
||||
private String city;
|
||||
|
||||
@Column(name = "company_status", length = 8)
|
||||
private String status;
|
||||
|
||||
@Column(name = "country_reg", length = 40)
|
||||
private String registrationCountry;
|
||||
|
||||
@Column(name = "oa_addr", length = 512)
|
||||
private String address;
|
||||
|
||||
@Column(name = "prnt_company_cd", length = 14)
|
||||
private String parentCompanyCode;
|
||||
|
||||
@Column(name = "country_reg_cd", length = 6)
|
||||
private String registrationCountryCode;
|
||||
|
||||
@Column(name = "country_ctrl", length = 40)
|
||||
private String controlCountry;
|
||||
|
||||
@Column(name = "country_ctrl_cd", length = 6)
|
||||
private String controlCountryCode;
|
||||
|
||||
@Column(name = "company_fndn_ymd", length = 8)
|
||||
private String foundedDate;
|
||||
|
||||
@Column(name = "eml_addr", length = 320)
|
||||
private String email;
|
||||
|
||||
@Column(name = "tel", length = 60)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "wbst_url", length = 120)
|
||||
private String website;
|
||||
}
|
||||
@ -41,4 +41,7 @@ public class ComplianceIndicator {
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column(name = "column_name", length = 100)
|
||||
private String columnName;
|
||||
}
|
||||
|
||||
@ -37,4 +37,7 @@ public class RiskIndicator {
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder;
|
||||
|
||||
@Column(name = "column_name", length = 100)
|
||||
private String columnName;
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 선박 제재 값 변경 이력 (읽기 전용)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "tb_ship_compliance_info_hstry", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class ShipComplianceHistory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "row_index")
|
||||
private Long rowIndex;
|
||||
|
||||
@Column(name = "imo_no", nullable = false, length = 10)
|
||||
private String imoNo;
|
||||
|
||||
@Column(name = "last_mdfcn_dt", nullable = false)
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
@Column(name = "flctn_col_nm", nullable = false, length = 100)
|
||||
private String changedColumnName;
|
||||
|
||||
@Column(name = "bfr_val", length = 4)
|
||||
private String beforeValue;
|
||||
|
||||
@Column(name = "aftr_val", length = 4)
|
||||
private String afterValue;
|
||||
|
||||
@Column(name = "crt_dt", nullable = false)
|
||||
private LocalDateTime createdDate;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.snp.batch.global.model.screening;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tb_ship_country_cd", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class ShipCountryCode {
|
||||
|
||||
@Id
|
||||
@Column(name = "ship_country_cd", length = 3)
|
||||
private String shipCountryCode;
|
||||
|
||||
@Column(name = "cd_nm", length = 100)
|
||||
private String codeName;
|
||||
|
||||
@Column(name = "iso_two_cd", length = 2)
|
||||
private String isoTwoCode;
|
||||
|
||||
@Column(name = "iso_thr_cd", length = 3)
|
||||
private String isoThreeCode;
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.snp.batch.global.model.screening;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tb_ship_info_mst", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class ShipInfoMaster {
|
||||
|
||||
@Id
|
||||
@Column(name = "imo_no", length = 10)
|
||||
private String imoNo;
|
||||
|
||||
@Column(name = "ship_nm", length = 75)
|
||||
private String shipName;
|
||||
|
||||
@Column(name = "ship_status", length = 50)
|
||||
private String shipStatus;
|
||||
|
||||
@Column(name = "ntnlty_cd", length = 50)
|
||||
private String nationalityCode;
|
||||
|
||||
@Column(name = "ship_ntnlty", length = 57)
|
||||
private String nationality;
|
||||
|
||||
@Column(name = "ship_type_lv_five", length = 73)
|
||||
private String shipType;
|
||||
|
||||
@Column(name = "dwt", length = 50)
|
||||
private String dwt;
|
||||
|
||||
@Column(name = "gt", length = 50)
|
||||
private String gt;
|
||||
|
||||
@Column(name = "build_yy", length = 50)
|
||||
private String buildYear;
|
||||
|
||||
@Column(name = "mmsi_no", length = 50)
|
||||
private String mmsiNo;
|
||||
|
||||
@Column(name = "clsgn_no", length = 50)
|
||||
private String callSign;
|
||||
|
||||
@Column(name = "ship_type_lv_two", length = 50)
|
||||
private String shipTypeGroup;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 선박 위험지표 값 변경 이력 (읽기 전용)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "tb_ship_risk_detail_info_hstry", schema = "std_snp_svc")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class ShipRiskDetailHistory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "row_index")
|
||||
private Long rowIndex;
|
||||
|
||||
@Column(name = "imo_no", nullable = false, length = 10)
|
||||
private String imoNo;
|
||||
|
||||
@Column(name = "last_mdfcn_dt", nullable = false)
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
@Column(name = "flctn_col_nm", nullable = false, length = 100)
|
||||
private String changedColumnName;
|
||||
|
||||
@Column(name = "bfr_val", length = 4)
|
||||
private String beforeValue;
|
||||
|
||||
@Column(name = "aftr_val", length = 4)
|
||||
private String afterValue;
|
||||
|
||||
@Column(name = "crt_dt", nullable = false)
|
||||
private LocalDateTime createdDate;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.CompanyComplianceHistory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface CompanyComplianceHistoryRepository extends JpaRepository<CompanyComplianceHistory, Long> {
|
||||
List<CompanyComplianceHistory> findByCompanyCodeOrderByLastModifiedDateDesc(String companyCode);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.CompanyDetailInfo;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface CompanyDetailInfoRepository extends JpaRepository<CompanyDetailInfo, String> {
|
||||
Optional<CompanyDetailInfo> findByCompanyCode(String companyCode);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.ShipComplianceHistory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ShipComplianceHistoryRepository extends JpaRepository<ShipComplianceHistory, Long> {
|
||||
List<ShipComplianceHistory> findByImoNoOrderByLastModifiedDateDesc(String imoNo);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.ShipCountryCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ShipCountryCodeRepository extends JpaRepository<ShipCountryCode, String> {
|
||||
Optional<ShipCountryCode> findByShipCountryCode(String shipCountryCode);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.ShipInfoMaster;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ShipInfoMasterRepository extends JpaRepository<ShipInfoMaster, String> {
|
||||
Optional<ShipInfoMaster> findByImoNo(String imoNo);
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
package com.snp.batch.global.repository.screening;
|
||||
|
||||
import com.snp.batch.global.model.screening.ShipRiskDetailHistory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ShipRiskDetailHistoryRepository extends JpaRepository<ShipRiskDetailHistory, Long> {
|
||||
|
||||
@Query(nativeQuery = true, value = """
|
||||
SELECT h.row_index, h.imo_no, h.last_mdfcn_dt, h.flctn_col_nm, h.bfr_val, h.aftr_val,
|
||||
CASE h.flctn_col_nm
|
||||
WHEN 'ais_notrcv_elps_days' THEN d.ais_notrcv_elps_days_desc
|
||||
WHEN 'ais_lwrnk_days' THEN d.ais_lwrnk_days_desc
|
||||
WHEN 'ais_up_imo_desc' THEN d.ais_up_imo_desc_val
|
||||
WHEN 'othr_ship_nm_voy_yn' THEN d.othr_ship_nm_voy_yn_desc
|
||||
WHEN 'mmsi_anom_message' THEN d.mmsi_anom_message_desc
|
||||
WHEN 'recent_dark_actv' THEN d.recent_dark_actv_desc
|
||||
WHEN 'port_prtcll' THEN d.port_prtcll_desc
|
||||
WHEN 'port_risk' THEN d.port_risk_desc
|
||||
WHEN 'sts_job' THEN d.sts_job_desc
|
||||
WHEN 'drift_chg' THEN d.drift_chg_desc
|
||||
WHEN 'risk_event' THEN d.risk_event_desc
|
||||
WHEN 'ntnlty_chg' THEN d.ntnlty_chg_desc
|
||||
WHEN 'ntnlty_prs_mou_perf' THEN d.ntnlty_prs_mou_perf_desc
|
||||
WHEN 'ntnlty_tky_mou_perf' THEN d.ntnlty_tky_mou_perf_desc
|
||||
WHEN 'ntnlty_uscg_mou_perf' THEN d.ntnlty_uscg_mou_perf_desc
|
||||
WHEN 'uscg_excl_ship_cert' THEN d.uscg_excl_ship_cert_desc
|
||||
WHEN 'psc_inspection_elps_hr' THEN d.psc_inspection_elps_hr_desc
|
||||
WHEN 'psc_inspection' THEN d.psc_inspection_desc
|
||||
WHEN 'psc_defect' THEN d.psc_defect_desc
|
||||
WHEN 'psc_detained' THEN d.psc_detained_desc
|
||||
WHEN 'now_smgrc_evdc' THEN d.now_smgrc_evdc_desc
|
||||
WHEN 'docc_chg' THEN d.docc_chg_desc
|
||||
WHEN 'now_clfic' THEN d.now_clfic_desc
|
||||
WHEN 'clfic_status_chg' THEN d.clfic_status_chg_desc
|
||||
WHEN 'pni_insrnc' THEN d.pni_insrnc_desc
|
||||
WHEN 'ship_nm_chg' THEN d.ship_nm_chg_desc
|
||||
WHEN 'gbo_chg' THEN d.gbo_chg_desc
|
||||
WHEN 'vslage' THEN d.vslage_desc
|
||||
WHEN 'ilgl_fshr_viol' THEN d.ilgl_fshr_viol_desc
|
||||
WHEN 'draft_chg' THEN d.draft_chg_desc
|
||||
WHEN 'recent_sanction_prtcll' THEN d.recent_sanction_prtcll_desc
|
||||
WHEN 'sngl_ship_voy' THEN d.sngl_ship_voy_desc
|
||||
WHEN 'fltsfty' THEN d.fltsfty_desc
|
||||
WHEN 'flt_psc' THEN d.flt_psc_desc
|
||||
WHEN 'spc_inspection_ovdue' THEN d.spc_inspection_ovdue_desc
|
||||
WHEN 'ownr_unk' THEN d.ownr_unk_desc
|
||||
WHEN 'rss_port_call' THEN d.rss_port_call_desc
|
||||
WHEN 'rss_ownr_reg' THEN d.rss_ownr_reg_desc
|
||||
WHEN 'rss_sts' THEN d.rss_sts_desc
|
||||
END as narrative,
|
||||
CASE h.flctn_col_nm
|
||||
WHEN 'ais_notrcv_elps_days' THEN p.ais_notrcv_elps_days_desc
|
||||
WHEN 'ais_lwrnk_days' THEN p.ais_lwrnk_days_desc
|
||||
WHEN 'ais_up_imo_desc' THEN p.ais_up_imo_desc_val
|
||||
WHEN 'othr_ship_nm_voy_yn' THEN p.othr_ship_nm_voy_yn_desc
|
||||
WHEN 'mmsi_anom_message' THEN p.mmsi_anom_message_desc
|
||||
WHEN 'recent_dark_actv' THEN p.recent_dark_actv_desc
|
||||
WHEN 'port_prtcll' THEN p.port_prtcll_desc
|
||||
WHEN 'port_risk' THEN p.port_risk_desc
|
||||
WHEN 'sts_job' THEN p.sts_job_desc
|
||||
WHEN 'drift_chg' THEN p.drift_chg_desc
|
||||
WHEN 'risk_event' THEN p.risk_event_desc
|
||||
WHEN 'ntnlty_chg' THEN p.ntnlty_chg_desc
|
||||
WHEN 'ntnlty_prs_mou_perf' THEN p.ntnlty_prs_mou_perf_desc
|
||||
WHEN 'ntnlty_tky_mou_perf' THEN p.ntnlty_tky_mou_perf_desc
|
||||
WHEN 'ntnlty_uscg_mou_perf' THEN p.ntnlty_uscg_mou_perf_desc
|
||||
WHEN 'uscg_excl_ship_cert' THEN p.uscg_excl_ship_cert_desc
|
||||
WHEN 'psc_inspection_elps_hr' THEN p.psc_inspection_elps_hr_desc
|
||||
WHEN 'psc_inspection' THEN p.psc_inspection_desc
|
||||
WHEN 'psc_defect' THEN p.psc_defect_desc
|
||||
WHEN 'psc_detained' THEN p.psc_detained_desc
|
||||
WHEN 'now_smgrc_evdc' THEN p.now_smgrc_evdc_desc
|
||||
WHEN 'docc_chg' THEN p.docc_chg_desc
|
||||
WHEN 'now_clfic' THEN p.now_clfic_desc
|
||||
WHEN 'clfic_status_chg' THEN p.clfic_status_chg_desc
|
||||
WHEN 'pni_insrnc' THEN p.pni_insrnc_desc
|
||||
WHEN 'ship_nm_chg' THEN p.ship_nm_chg_desc
|
||||
WHEN 'gbo_chg' THEN p.gbo_chg_desc
|
||||
WHEN 'vslage' THEN p.vslage_desc
|
||||
WHEN 'ilgl_fshr_viol' THEN p.ilgl_fshr_viol_desc
|
||||
WHEN 'draft_chg' THEN p.draft_chg_desc
|
||||
WHEN 'recent_sanction_prtcll' THEN p.recent_sanction_prtcll_desc
|
||||
WHEN 'sngl_ship_voy' THEN p.sngl_ship_voy_desc
|
||||
WHEN 'fltsfty' THEN p.fltsfty_desc
|
||||
WHEN 'flt_psc' THEN p.flt_psc_desc
|
||||
WHEN 'spc_inspection_ovdue' THEN p.spc_inspection_ovdue_desc
|
||||
WHEN 'ownr_unk' THEN p.ownr_unk_desc
|
||||
WHEN 'rss_port_call' THEN p.rss_port_call_desc
|
||||
WHEN 'rss_ownr_reg' THEN p.rss_ownr_reg_desc
|
||||
WHEN 'rss_sts' THEN p.rss_sts_desc
|
||||
END as prev_narrative
|
||||
FROM std_snp_svc.tb_ship_risk_detail_info_hstry h
|
||||
LEFT JOIN std_snp_svc.tb_ship_risk_detail_hstry d
|
||||
ON h.imo_no = d.imo_no AND h.last_mdfcn_dt = d.last_mdfcn_dt
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM std_snp_svc.tb_ship_risk_detail_hstry prev
|
||||
WHERE prev.imo_no = h.imo_no
|
||||
AND prev.last_mdfcn_dt < h.last_mdfcn_dt
|
||||
ORDER BY prev.last_mdfcn_dt DESC
|
||||
LIMIT 1
|
||||
) p ON true
|
||||
WHERE h.imo_no = :imoNo
|
||||
ORDER BY h.last_mdfcn_dt DESC
|
||||
""")
|
||||
List<Object[]> findRiskHistoryWithNarrative(@Param("imoNo") String imoNo);
|
||||
}
|
||||
@ -1,11 +1,18 @@
|
||||
package com.snp.batch.service;
|
||||
|
||||
import com.snp.batch.global.dto.screening.ChangeHistoryResponse;
|
||||
import com.snp.batch.global.dto.screening.CompanyInfoResponse;
|
||||
import com.snp.batch.global.dto.screening.ComplianceCategoryResponse;
|
||||
import com.snp.batch.global.dto.screening.ComplianceIndicatorResponse;
|
||||
import com.snp.batch.global.dto.screening.IndicatorStatusResponse;
|
||||
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.dto.screening.ShipInfoResponse;
|
||||
import com.snp.batch.global.model.screening.ChangeTypeLang;
|
||||
import com.snp.batch.global.model.screening.CompanyDetailInfo;
|
||||
import com.snp.batch.global.model.screening.ShipCountryCode;
|
||||
import com.snp.batch.global.model.screening.CompanyComplianceHistory;
|
||||
import com.snp.batch.global.model.screening.ComplianceIndicator;
|
||||
import com.snp.batch.global.model.screening.ComplianceIndicatorLang;
|
||||
import com.snp.batch.global.model.screening.MethodologyHistory;
|
||||
@ -13,7 +20,10 @@ 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.model.screening.ShipComplianceHistory;
|
||||
import com.snp.batch.global.repository.screening.ChangeTypeLangRepository;
|
||||
import com.snp.batch.global.repository.screening.CompanyComplianceHistoryRepository;
|
||||
import com.snp.batch.global.repository.screening.CompanyDetailInfoRepository;
|
||||
import com.snp.batch.global.repository.screening.ComplianceIndicatorLangRepository;
|
||||
import com.snp.batch.global.repository.screening.ComplianceIndicatorRepository;
|
||||
import com.snp.batch.global.repository.screening.MethodologyHistoryLangRepository;
|
||||
@ -21,11 +31,20 @@ 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 com.snp.batch.global.repository.screening.ShipComplianceHistoryRepository;
|
||||
import com.snp.batch.global.repository.screening.ShipCountryCodeRepository;
|
||||
import com.snp.batch.global.repository.screening.ShipInfoMasterRepository;
|
||||
import com.snp.batch.global.repository.screening.ShipRiskDetailHistoryRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -37,6 +56,8 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class ScreeningGuideService {
|
||||
|
||||
private static final DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final RiskIndicatorRepository riskIndicatorRepo;
|
||||
private final RiskIndicatorLangRepository riskIndicatorLangRepo;
|
||||
private final RiskIndicatorCategoryLangRepository riskCategoryLangRepo;
|
||||
@ -45,6 +66,13 @@ public class ScreeningGuideService {
|
||||
private final MethodologyHistoryRepository methodologyHistoryRepo;
|
||||
private final MethodologyHistoryLangRepository methodologyHistoryLangRepo;
|
||||
private final ChangeTypeLangRepository changeTypeLangRepo;
|
||||
private final ShipRiskDetailHistoryRepository shipRiskDetailHistoryRepo;
|
||||
private final ShipComplianceHistoryRepository shipComplianceHistoryRepo;
|
||||
private final CompanyComplianceHistoryRepository companyComplianceHistoryRepo;
|
||||
private final ShipInfoMasterRepository shipInfoMasterRepo;
|
||||
private final CompanyDetailInfoRepository companyDetailInfoRepo;
|
||||
private final ShipCountryCodeRepository shipCountryCodeRepo;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 카테고리별 Risk 지표 목록 조회
|
||||
@ -161,4 +189,326 @@ public class ScreeningGuideService {
|
||||
.build();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 위험지표 값 변경 이력 조회
|
||||
*
|
||||
* @param imoNo IMO 번호
|
||||
* @param lang 언어 코드 (예: KO, EN)
|
||||
* @return 변경 이력 응답 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChangeHistoryResponse> getShipRiskDetailHistory(String imoNo, String lang) {
|
||||
Map<String, String> fieldNameMap = getRiskFieldNameMap(lang);
|
||||
Map<String, Integer> sortOrderMap = getRiskSortOrderMap();
|
||||
List<Object[]> rows = shipRiskDetailHistoryRepo.findRiskHistoryWithNarrative(imoNo);
|
||||
return rows.stream().map(row -> {
|
||||
String colName = (String) row[3];
|
||||
return ChangeHistoryResponse.builder()
|
||||
.rowIndex(((Number) row[0]).longValue())
|
||||
.searchKey((String) row[1])
|
||||
.lastModifiedDate(row[2] != null ? row[2].toString().substring(0, Math.min(row[2].toString().length(), 19)) : "")
|
||||
.changedColumnName(colName)
|
||||
.beforeValue((String) row[4])
|
||||
.afterValue((String) row[5])
|
||||
.narrative((String) row[6])
|
||||
.prevNarrative((String) row[7])
|
||||
.fieldName(fieldNameMap.getOrDefault(colName, colName))
|
||||
.sortOrder(sortOrderMap.getOrDefault(colName, Integer.MAX_VALUE))
|
||||
.build();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 제재 값 변경 이력 조회
|
||||
*
|
||||
* @param imoNo IMO 번호
|
||||
* @param lang 언어 코드 (예: KO, EN)
|
||||
* @return 변경 이력 응답 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChangeHistoryResponse> getShipComplianceHistory(String imoNo, String lang) {
|
||||
Map<String, String> fieldNameMap = getComplianceFieldNameMap(lang, "SHIP");
|
||||
Map<String, Integer> sortOrderMap = getComplianceSortOrderMap("SHIP");
|
||||
return shipComplianceHistoryRepo.findByImoNoOrderByLastModifiedDateDesc(imoNo).stream()
|
||||
.map(h -> ChangeHistoryResponse.builder()
|
||||
.rowIndex(h.getRowIndex())
|
||||
.searchKey(h.getImoNo())
|
||||
.lastModifiedDate(h.getLastModifiedDate() != null ? h.getLastModifiedDate().format(DT_FORMAT) : "")
|
||||
.changedColumnName(h.getChangedColumnName())
|
||||
.beforeValue(h.getBeforeValue())
|
||||
.afterValue(h.getAfterValue())
|
||||
.fieldName(fieldNameMap.getOrDefault(h.getChangedColumnName(), h.getChangedColumnName()))
|
||||
.sortOrder(sortOrderMap.getOrDefault(h.getChangedColumnName(), Integer.MAX_VALUE))
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 제재 값 변경 이력 조회
|
||||
*
|
||||
* @param companyCode 회사 코드
|
||||
* @param lang 언어 코드 (예: KO, EN)
|
||||
* @return 변경 이력 응답 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChangeHistoryResponse> getCompanyComplianceHistory(String companyCode, String lang) {
|
||||
Map<String, String> fieldNameMap = getComplianceFieldNameMap(lang, "COMPANY");
|
||||
Map<String, Integer> sortOrderMap = getComplianceSortOrderMap("COMPANY");
|
||||
return companyComplianceHistoryRepo.findByCompanyCodeOrderByLastModifiedDateDesc(companyCode).stream()
|
||||
.map(h -> ChangeHistoryResponse.builder()
|
||||
.rowIndex(h.getRowIndex())
|
||||
.searchKey(h.getCompanyCode())
|
||||
.lastModifiedDate(h.getLastModifiedDate() != null ? h.getLastModifiedDate().format(DT_FORMAT) : "")
|
||||
.changedColumnName(h.getChangedColumnName())
|
||||
.beforeValue(h.getBeforeValue())
|
||||
.afterValue(h.getAfterValue())
|
||||
.fieldName(fieldNameMap.getOrDefault(h.getChangedColumnName(), h.getChangedColumnName()))
|
||||
.sortOrder(sortOrderMap.getOrDefault(h.getChangedColumnName(), Integer.MAX_VALUE))
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 기본 정보 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public ShipInfoResponse getShipInfo(String imoNo) {
|
||||
return shipInfoMasterRepo.findByImoNo(imoNo)
|
||||
.map(s -> ShipInfoResponse.builder()
|
||||
.imoNo(s.getImoNo())
|
||||
.shipName(s.getShipName())
|
||||
.shipStatus(s.getShipStatus())
|
||||
.nationalityCode(s.getNationalityCode())
|
||||
.nationalityIsoCode(resolveIsoTwoCode(s.getNationalityCode()))
|
||||
.nationality(s.getNationality())
|
||||
.shipType(s.getShipType())
|
||||
.dwt(s.getDwt())
|
||||
.gt(s.getGt())
|
||||
.buildYear(s.getBuildYear())
|
||||
.mmsiNo(s.getMmsiNo())
|
||||
.callSign(s.getCallSign())
|
||||
.shipTypeGroup(s.getShipTypeGroup())
|
||||
.build())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 기본 정보 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public CompanyInfoResponse getCompanyInfo(String companyCode) {
|
||||
return companyDetailInfoRepo.findByCompanyCode(companyCode)
|
||||
.map(c -> {
|
||||
String parentCompanyName = null;
|
||||
if (c.getParentCompanyCode() != null && !c.getParentCompanyCode().isBlank()) {
|
||||
parentCompanyName = companyDetailInfoRepo.findByCompanyCode(c.getParentCompanyCode())
|
||||
.map(CompanyDetailInfo::getFullName)
|
||||
.orElse("UNKNOWN");
|
||||
}
|
||||
return CompanyInfoResponse.builder()
|
||||
.companyCode(c.getCompanyCode())
|
||||
.fullName(c.getFullName())
|
||||
.abbreviation(c.getAbbreviation())
|
||||
.status(c.getStatus())
|
||||
.parentCompanyCode(c.getParentCompanyCode())
|
||||
.parentCompanyName(parentCompanyName)
|
||||
.registrationCountry(c.getRegistrationCountry())
|
||||
.registrationCountryCode(c.getRegistrationCountryCode())
|
||||
.registrationCountryIsoCode(resolveIsoTwoCode(c.getRegistrationCountryCode()))
|
||||
.controlCountry(c.getControlCountry())
|
||||
.controlCountryCode(c.getControlCountryCode())
|
||||
.controlCountryIsoCode(resolveIsoTwoCode(c.getControlCountryCode()))
|
||||
.foundedDate(c.getFoundedDate())
|
||||
.email(c.getEmail())
|
||||
.phone(c.getPhone())
|
||||
.website(c.getWebsite())
|
||||
.build();
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 현재 Risk 지표 상태 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<IndicatorStatusResponse> getShipRiskStatus(String imoNo, String lang) {
|
||||
Map<String, String> fieldNameMap = getRiskFieldNameMap(lang);
|
||||
Map<String, Integer> sortOrderMap = getRiskSortOrderMap();
|
||||
Map<String, String> categoryMap = getRiskCategoryMap(lang);
|
||||
|
||||
try {
|
||||
Map<String, Object> row = jdbcTemplate.queryForMap(
|
||||
"SELECT * FROM std_snp_svc.tb_ship_risk_detail_info WHERE imo_no = ?", imoNo);
|
||||
|
||||
List<IndicatorStatusResponse> result = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : sortOrderMap.entrySet()) {
|
||||
String colName = entry.getKey();
|
||||
Object codeVal = row.get(colName);
|
||||
String descColName = "ais_up_imo_desc".equals(colName) ? "ais_up_imo_desc_val" : colName + "_desc";
|
||||
Object descVal = row.get(descColName);
|
||||
|
||||
result.add(IndicatorStatusResponse.builder()
|
||||
.columnName(colName)
|
||||
.fieldName(fieldNameMap.getOrDefault(colName, colName))
|
||||
.category(categoryMap.getOrDefault(colName, ""))
|
||||
.value(codeVal != null ? codeVal.toString() : null)
|
||||
.narrative(descVal != null ? descVal.toString() : null)
|
||||
.sortOrder(entry.getValue())
|
||||
.build());
|
||||
}
|
||||
result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder));
|
||||
return result;
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선박 현재 Compliance 상태 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<IndicatorStatusResponse> getShipComplianceStatus(String imoNo, String lang) {
|
||||
Map<String, String> fieldNameMap = getComplianceFieldNameMap(lang, "SHIP");
|
||||
Map<String, Integer> sortOrderMap = getComplianceSortOrderMap("SHIP");
|
||||
Map<String, String> categoryMap = getComplianceCategoryMap("SHIP");
|
||||
|
||||
try {
|
||||
Map<String, Object> row = jdbcTemplate.queryForMap(
|
||||
"SELECT * FROM std_snp_svc.tb_ship_compliance_info WHERE imo_no = ?", imoNo);
|
||||
|
||||
List<IndicatorStatusResponse> result = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : sortOrderMap.entrySet()) {
|
||||
String colName = entry.getKey();
|
||||
Object codeVal = row.get(colName);
|
||||
|
||||
result.add(IndicatorStatusResponse.builder()
|
||||
.columnName(colName)
|
||||
.fieldName(fieldNameMap.getOrDefault(colName, colName))
|
||||
.category(categoryMap.getOrDefault(colName, ""))
|
||||
.value(codeVal != null ? codeVal.toString() : null)
|
||||
.sortOrder(entry.getValue())
|
||||
.build());
|
||||
}
|
||||
result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder));
|
||||
return result;
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 현재 Compliance 상태 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<IndicatorStatusResponse> getCompanyComplianceStatus(String companyCode, String lang) {
|
||||
Map<String, String> fieldNameMap = getComplianceFieldNameMap(lang, "COMPANY");
|
||||
Map<String, Integer> sortOrderMap = getComplianceSortOrderMap("COMPANY");
|
||||
Map<String, String> categoryMap = getComplianceCategoryMap("COMPANY");
|
||||
|
||||
try {
|
||||
Map<String, Object> row = jdbcTemplate.queryForMap(
|
||||
"SELECT * FROM std_snp_svc.tb_company_compliance_info WHERE company_cd = ?", companyCode);
|
||||
|
||||
List<IndicatorStatusResponse> result = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : sortOrderMap.entrySet()) {
|
||||
String colName = entry.getKey();
|
||||
Object codeVal = row.get(colName);
|
||||
|
||||
result.add(IndicatorStatusResponse.builder()
|
||||
.columnName(colName)
|
||||
.fieldName(fieldNameMap.getOrDefault(colName, colName))
|
||||
.category(categoryMap.getOrDefault(colName, ""))
|
||||
.value(codeVal != null ? codeVal.toString() : null)
|
||||
.sortOrder(entry.getValue())
|
||||
.build());
|
||||
}
|
||||
result.sort(Comparator.comparingInt(IndicatorStatusResponse::getSortOrder));
|
||||
return result;
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveIsoTwoCode(String shipCountryCode) {
|
||||
if (shipCountryCode == null || shipCountryCode.isBlank()) return null;
|
||||
return shipCountryCodeRepo.findByShipCountryCode(shipCountryCode)
|
||||
.map(ShipCountryCode::getIsoTwoCode)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk 지표 컬럼명 → 필드명 매핑 조회
|
||||
*/
|
||||
private Map<String, String> getRiskFieldNameMap(String lang) {
|
||||
Map<Integer, String> langMap = riskIndicatorLangRepo.findByLangCodeOrderByIndicatorIdAsc(lang).stream()
|
||||
.collect(Collectors.toMap(RiskIndicatorLang::getIndicatorId, RiskIndicatorLang::getFieldName));
|
||||
return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream()
|
||||
.filter(ri -> ri.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
RiskIndicator::getColumnName,
|
||||
ri -> langMap.getOrDefault(ri.getIndicatorId(), ri.getFieldKey()),
|
||||
(a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<String, String> getRiskCategoryMap(String lang) {
|
||||
Map<String, String> catNameMap = riskCategoryLangRepo.findByLangCode(lang).stream()
|
||||
.collect(Collectors.toMap(RiskIndicatorCategoryLang::getCategoryCode, RiskIndicatorCategoryLang::getCategoryName));
|
||||
return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream()
|
||||
.filter(ri -> ri.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
RiskIndicator::getColumnName,
|
||||
ri -> catNameMap.getOrDefault(ri.getCategoryCode(), ri.getCategoryCode()),
|
||||
(a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<String, Integer> getRiskSortOrderMap() {
|
||||
return riskIndicatorRepo.findAllByOrderByCategoryCodeAscSortOrderAsc().stream()
|
||||
.filter(ri -> ri.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
RiskIndicator::getColumnName,
|
||||
RiskIndicator::getSortOrder,
|
||||
(a, b) -> a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compliance 지표 컬럼명 → 필드명 매핑 조회
|
||||
*/
|
||||
private Map<String, String> getComplianceFieldNameMap(String lang, String type) {
|
||||
Map<Integer, String> langMap = complianceIndicatorLangRepo.findByLangCode(lang).stream()
|
||||
.collect(Collectors.toMap(ComplianceIndicatorLang::getIndicatorId, ComplianceIndicatorLang::getFieldName));
|
||||
List<ComplianceIndicator> indicators = (type != null && !type.isBlank())
|
||||
? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type)
|
||||
: complianceIndicatorRepo.findAllByOrderBySortOrderAsc();
|
||||
return indicators.stream()
|
||||
.filter(ci -> ci.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
ComplianceIndicator::getColumnName,
|
||||
ci -> langMap.getOrDefault(ci.getIndicatorId(), ci.getFieldKey()),
|
||||
(a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<String, String> getComplianceCategoryMap(String type) {
|
||||
List<ComplianceIndicator> indicators = (type != null && !type.isBlank())
|
||||
? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type)
|
||||
: complianceIndicatorRepo.findAllByOrderBySortOrderAsc();
|
||||
return indicators.stream()
|
||||
.filter(ci -> ci.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
ComplianceIndicator::getColumnName,
|
||||
ComplianceIndicator::getCategory,
|
||||
(a, b) -> a));
|
||||
}
|
||||
|
||||
private Map<String, Integer> getComplianceSortOrderMap(String type) {
|
||||
List<ComplianceIndicator> indicators = (type != null && !type.isBlank())
|
||||
? complianceIndicatorRepo.findByIndicatorTypeOrderBySortOrderAsc(type)
|
||||
: complianceIndicatorRepo.findAllByOrderBySortOrderAsc();
|
||||
return indicators.stream()
|
||||
.filter(ci -> ci.getColumnName() != null)
|
||||
.collect(Collectors.toMap(
|
||||
ComplianceIndicator::getColumnName,
|
||||
ComplianceIndicator::getSortOrder,
|
||||
(a, b) -> a));
|
||||
}
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user