diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 6817940..3b9abec 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,36 @@ ## [Unreleased] +## [2026-04-01] + +### 추가 +- Risk & Compliance Screening Guide UI 개편 및 다중언어 지원 (#124) + - Screening Guide 아코디언 리스트 UI 개편 (카테고리별 접기/펼치기) + - 언더라인 탭 및 언어 토글 디자인 통일 + - 다중언어 데이터 캐싱 (화면 로드 시 KO/EN 동시 조회) + - Compliance 카테고리 다중언어 테이블 신규 생성 (compliance_category, compliance_category_lang) + - RAG 지표 색상 테마 CSS 변수화 (다크모드/라이트모드 대응) +- S&P Bypass 피드백 반영 (#123) + - Response JSON 원본 반환 (ApiResponse 래핑 제거) + - 사용자용 API 카탈로그 페이지 추가 (/bypass-catalog) + - 운영 환경 코드 생성 차단 (app.environment 기반) + - Bypass API 코드 생성 (compliance, risk 도메인) +- 공통 UI 피드백 반영 (#121) + - 2단 탭 네비게이션 (섹션 탭 + 서브 탭) + - 섹션 간 직접 이동 + - 메인화면 카드 높이 동기화 (CSS Grid) + +### 수정 +- Change History 선박 제재 KO 데이터 조회 누락 수정 (categoryCode 기반 분류로 변경) +- S&P Collector 다크모드 미적용 및 라벨 디자인 통일 (#122) + - 실행이력상세/재수집이력상세 API 호출 로그 다크모드 적용 + - 개별 호출 로그 필터/테이블 다크모드 적용 + - 작업관리 스케줄 라벨 rounded-full 디자인 통일 + +### 변경 +- Navbar 메인 섹션 왼쪽 정렬, 서브 섹션 오른쪽 정렬로 변경 +- 불필요한 DB 컬럼 참조 코드 제거 (collection_note, update_title) + ## [2026-03-31] ### 추가 diff --git a/docs/compliance_category_migration.sql b/docs/compliance_category_migration.sql new file mode 100644 index 0000000..9dc7586 --- /dev/null +++ b/docs/compliance_category_migration.sql @@ -0,0 +1,111 @@ +-- ============================================================ +-- Compliance 카테고리 다중언어 마이그레이션 스크립트 +-- ============================================================ + +-- 1. 카테고리 마스터 테이블 생성 +CREATE TABLE IF NOT EXISTS std_snp_data.compliance_category ( + category_code VARCHAR(50) PRIMARY KEY, + indicator_type VARCHAR(20) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 2. 카테고리 다중언어 테이블 생성 +CREATE TABLE IF NOT EXISTS std_snp_data.compliance_category_lang ( + category_code VARCHAR(50) NOT NULL, + lang_code VARCHAR(5) NOT NULL, + category_name VARCHAR(200) NOT NULL, + PRIMARY KEY (category_code, lang_code) +); + +-- 3. 카테고리 마스터 데이터 삽입 +INSERT INTO std_snp_data.compliance_category (category_code, indicator_type, sort_order) VALUES + ('SANCTIONS_SHIP_US_OFAC', 'SHIP', 1), + ('SANCTIONS_OWNERSHIP_US_OFAC', 'SHIP', 2), + ('SANCTIONS_SHIP_NON_US', 'SHIP', 3), + ('SANCTIONS_OWNERSHIP_NON_US', 'SHIP', 4), + ('SANCTIONS_FATF', 'SHIP', 5), + ('SANCTIONS_OTHER', 'SHIP', 6), + ('PORT_CALLS', 'SHIP', 7), + ('STS_ACTIVITY', 'SHIP', 8), + ('SUSPICIOUS_BEHAVIOR', 'SHIP', 9), + ('OWNERSHIP_SCREENING', 'SHIP', 10), + ('COMPLIANCE_SCREENING_HISTORY', 'SHIP', 11), + ('US_TREASURY_SANCTIONS', 'COMPANY', 1), + ('NON_US_SANCTIONS', 'COMPANY', 2), + ('FATF_JURISDICTION', 'COMPANY', 3), + ('PARENT_COMPANY', 'COMPANY', 4), + ('OVERALL_COMPLIANCE_STATUS', 'COMPANY', 5), + ('RELATED_SCREENING', 'COMPANY', 6), + ('COMPANY_COMPLIANCE_HISTORY', 'COMPANY', 7); + +-- 4. 카테고리 다중언어 데이터 삽입 (EN) +INSERT INTO std_snp_data.compliance_category_lang (category_code, lang_code, category_name) VALUES + ('SANCTIONS_SHIP_US_OFAC', 'EN', 'Sanctions – Ship (US OFAC)'), + ('SANCTIONS_OWNERSHIP_US_OFAC', 'EN', 'Sanctions – Ownership (US OFAC)'), + ('SANCTIONS_SHIP_NON_US', 'EN', 'Sanctions – Ship (Non-US)'), + ('SANCTIONS_OWNERSHIP_NON_US', 'EN', 'Sanctions – Ownership (Non-US)'), + ('SANCTIONS_FATF', 'EN', 'Sanctions – FATF'), + ('SANCTIONS_OTHER', 'EN', 'Sanctions – Other'), + ('PORT_CALLS', 'EN', 'Port Calls'), + ('STS_ACTIVITY', 'EN', 'STS Activity'), + ('SUSPICIOUS_BEHAVIOR', 'EN', 'Suspicious Behavior'), + ('OWNERSHIP_SCREENING', 'EN', 'Ownership Screening'), + ('COMPLIANCE_SCREENING_HISTORY', 'EN', 'Compliance Screening History'), + ('US_TREASURY_SANCTIONS', 'EN', 'US Treasury Sanctions'), + ('NON_US_SANCTIONS', 'EN', 'Non-US Sanctions'), + ('FATF_JURISDICTION', 'EN', 'FATF Jurisdiction'), + ('PARENT_COMPANY', 'EN', 'Parent Company'), + ('OVERALL_COMPLIANCE_STATUS', 'EN', 'Overall Compliance Status'), + ('RELATED_SCREENING', 'EN', 'Related Screening'), + ('COMPANY_COMPLIANCE_HISTORY', 'EN', 'Compliance Screening Change History'); + +-- 5. 카테고리 다중언어 데이터 삽입 (KO) +INSERT INTO std_snp_data.compliance_category_lang (category_code, lang_code, category_name) VALUES + ('SANCTIONS_SHIP_US_OFAC', 'KO', '제재 – 선박 (US OFAC)'), + ('SANCTIONS_OWNERSHIP_US_OFAC', 'KO', '제재 – 소유권 (US OFAC)'), + ('SANCTIONS_SHIP_NON_US', 'KO', '제재 – 선박 (비미국)'), + ('SANCTIONS_OWNERSHIP_NON_US', 'KO', '제재 – 소유권 (비미국)'), + ('SANCTIONS_FATF', 'KO', '제재 – FATF'), + ('SANCTIONS_OTHER', 'KO', '제재 – 기타'), + ('PORT_CALLS', 'KO', '입항 이력'), + ('STS_ACTIVITY', 'KO', 'STS 활동'), + ('SUSPICIOUS_BEHAVIOR', 'KO', '의심 행위'), + ('OWNERSHIP_SCREENING', 'KO', '소유권 심사'), + ('COMPLIANCE_SCREENING_HISTORY', 'KO', '컴플라이언스 심사 이력'), + ('US_TREASURY_SANCTIONS', 'KO', '미국 재무부 제재'), + ('NON_US_SANCTIONS', 'KO', '비미국 제재'), + ('FATF_JURISDICTION', 'KO', 'FATF 관할지역'), + ('PARENT_COMPANY', 'KO', '모회사'), + ('OVERALL_COMPLIANCE_STATUS', 'KO', '종합 컴플라이언스 상태'), + ('RELATED_SCREENING', 'KO', '관련 심사'), + ('COMPANY_COMPLIANCE_HISTORY', 'KO', '컴플라이언스 심사 변경 이력'); + +-- 6. compliance_indicator 테이블: category → category_code 변환 +-- 먼저 컬럼 추가 +ALTER TABLE std_snp_data.compliance_indicator ADD COLUMN category_code VARCHAR(50); + +-- 기존 category 값을 category_code로 매핑 +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_SHIP_US_OFAC' WHERE category = 'Sanctions – Ship (US OFAC)'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_OWNERSHIP_US_OFAC' WHERE category = 'Sanctions – Ownership (US OFAC)'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_SHIP_NON_US' WHERE category = 'Sanctions – Ship (Non-US)'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_OWNERSHIP_NON_US' WHERE category = 'Sanctions – Ownership (Non-US)'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_FATF' WHERE category = 'Sanctions – FATF'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SANCTIONS_OTHER' WHERE category = 'Sanctions – Other'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'PORT_CALLS' WHERE category = 'Port Calls'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'STS_ACTIVITY' WHERE category = 'STS Activity'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'SUSPICIOUS_BEHAVIOR' WHERE category = 'Suspicious Behavior'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'OWNERSHIP_SCREENING' WHERE category = 'Ownership Screening'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'COMPLIANCE_SCREENING_HISTORY' WHERE category = 'Compliance Screening History'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'US_TREASURY_SANCTIONS' WHERE category = 'US Treasury Sanctions'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'NON_US_SANCTIONS' WHERE category = 'Non-US Sanctions'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'FATF_JURISDICTION' WHERE category = 'FATF Jurisdiction'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'PARENT_COMPANY' WHERE category = 'Parent Company'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'OVERALL_COMPLIANCE_STATUS' WHERE category = 'Overall Compliance Status'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'RELATED_SCREENING' WHERE category = 'Related Screening'; +UPDATE std_snp_data.compliance_indicator SET category_code = 'COMPANY_COMPLIANCE_HISTORY' WHERE category = 'Compliance Screening Change History'; + +-- category_code NOT NULL 설정 +ALTER TABLE std_snp_data.compliance_indicator ALTER COLUMN category_code SET NOT NULL; + +-- 기존 category 컬럼 삭제 +ALTER TABLE std_snp_data.compliance_indicator DROP COLUMN category; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96c6356..6a4d349 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ const RecollectDetail = lazy(() => import('./pages/RecollectDetail')); const Schedules = lazy(() => import('./pages/Schedules')); const Timeline = lazy(() => import('./pages/Timeline')); const BypassConfig = lazy(() => import('./pages/BypassConfig')); +const BypassCatalog = lazy(() => import('./pages/BypassCatalog')); const ScreeningGuide = lazy(() => import('./pages/ScreeningGuide')); const RiskComplianceHistory = lazy(() => import('./pages/RiskComplianceHistory')); @@ -25,26 +26,41 @@ function AppLayout() { const isMainMenu = location.pathname === '/'; return ( -
| # | URI | @@ -104,24 +104,24 @@ export default function ApiLogSection({ stepExecutionId, summary }: ApiLogSectio에러 | |||||||
|---|---|---|---|---|---|---|---|---|---|
| {page * 10 + idx + 1} |
-
+
{log.requestUri}
|
- {log.httpMethod} | +{log.httpMethod} | - | + | {log.responseTimeMs?.toLocaleString() ?? '-'} | -+ | {log.responseCount?.toLocaleString() ?? '-'} | diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index adf72d7..1c2adf2 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,49 +1,76 @@ -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useThemeContext } from '../contexts/ThemeContext'; -interface NavSection { - key: string; - title: string; - paths: string[]; - items: { path: string; label: string; icon: string }[]; +interface MenuItem { + id: string; + label: string; + path: string; } -const sections: NavSection[] = [ +interface MenuSection { + id: string; + label: string; + shortLabel: string; + icon: React.ReactNode; + defaultPath: string; + children: MenuItem[]; +} + +const MENU_STRUCTURE: MenuSection[] = [ { - key: 'collector', - title: 'S&P Collector', - paths: ['/dashboard', '/jobs', '/executions', '/recollects', '/schedules', '/schedule-timeline'], - items: [ - { path: '/dashboard', label: '대시보드', icon: '📊' }, - { path: '/executions', label: '실행 이력', icon: '📋' }, - { path: '/recollects', label: '재수집 이력', icon: '🔄' }, - { path: '/jobs', label: '작업', icon: '⚙️' }, - { path: '/schedules', label: '스케줄', icon: '🕐' }, - { path: '/schedule-timeline', label: '타임라인', icon: '📅' }, + id: 'collector', + label: 'S&P Collector', + shortLabel: 'Collector', + icon: ( + + ), + defaultPath: '/dashboard', + children: [ + { id: 'dashboard', label: '대시보드', path: '/dashboard' }, + { id: 'executions', label: '실행 이력', path: '/executions' }, + { id: 'recollects', label: '재수집 이력', path: '/recollects' }, + { id: 'jobs', label: '작업 관리', path: '/jobs' }, + { id: 'schedules', label: '스케줄', path: '/schedules' }, + { id: 'timeline', label: '타임라인', path: '/schedule-timeline' }, ], }, { - key: 'bypass', - title: 'S&P Bypass', - paths: ['/bypass-config'], - items: [ - { path: '/bypass-config', label: 'Bypass API', icon: '🔗' }, + id: 'bypass', + label: 'S&P Bypass', + shortLabel: 'Bypass', + icon: ( + + ), + defaultPath: '/bypass-catalog', + children: [ + { id: 'bypass-catalog', label: 'API 카탈로그', path: '/bypass-catalog' }, + { id: 'bypass-config', label: 'API 관리', path: '/bypass-config' }, ], }, { - key: 'risk', - title: 'S&P Risk & Compliance', - paths: ['/screening-guide', '/risk-compliance-history'], - items: [ - { path: '/screening-guide', label: 'Screening Guide', icon: '⚖️' }, - { path: '/risk-compliance-history', label: 'Change History', icon: '📜' }, + id: 'risk', + label: 'S&P Risk & Compliance', + shortLabel: 'Risk & Compliance', + icon: ( + + ), + defaultPath: '/risk-compliance-history', + children: [ + { id: 'risk-compliance-history', label: 'Change History', path: '/risk-compliance-history' }, + { id: 'screening-guide', label: 'Screening Guide', path: '/screening-guide' }, ], }, ]; -function getCurrentSection(pathname: string): NavSection | null { - for (const section of sections) { - if (section.paths.some((p) => pathname === p || pathname.startsWith(p + '/'))) { +function getCurrentSection(pathname: string): MenuSection | null { + for (const section of MENU_STRUCTURE) { + if (section.children.some((c) => pathname === c.path || pathname.startsWith(c.path + '/'))) { return section; } } @@ -52,56 +79,75 @@ function getCurrentSection(pathname: string): NavSection | null { export default function Navbar() { const location = useLocation(); + const navigate = useNavigate(); const { theme, toggle } = useThemeContext(); const currentSection = getCurrentSection(location.pathname); - // 메인 화면에서는 Navbar 숨김 + // 메인 화면에서는 숨김 if (!currentSection) return null; - const isActive = (path: string) => { - if (path === '/dashboard') return location.pathname === '/dashboard'; + const isActivePath = (path: string) => { return location.pathname === path || location.pathname.startsWith(path + '/'); }; return ( - |