diff --git a/backend/src/main/java/gc/mda/kcg/domain/fleet/ParentInferenceWorkflowController.java b/backend/src/main/java/gc/mda/kcg/domain/fleet/ParentInferenceWorkflowController.java index 1da357a..834abc4 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/fleet/ParentInferenceWorkflowController.java +++ b/backend/src/main/java/gc/mda/kcg/domain/fleet/ParentInferenceWorkflowController.java @@ -64,7 +64,7 @@ public class ParentInferenceWorkflowController { } @PostMapping("/exclusions/{exclusionId}/release") - @RequirePermission(resource = "parent-inference-workflow:parent-exclusion", operation = "UPDATE") + @RequirePermission(resource = "parent-inference-workflow:parent-exclusion", operation = "DELETE") public CandidateExclusion releaseExclusion( @PathVariable Long exclusionId, @RequestBody(required = false) CancelRequest req diff --git a/backend/src/main/resources/db/migration/V007__perm_tree_label_align.sql b/backend/src/main/resources/db/migration/V007__perm_tree_label_align.sql new file mode 100644 index 0000000..d9ccc92 --- /dev/null +++ b/backend/src/main/resources/db/migration/V007__perm_tree_label_align.sql @@ -0,0 +1,80 @@ +-- ============================================================================ +-- V007: 권한 트리 노드의 한국어 명칭을 좌측 사이드바 i18n 라벨과 일치시킴 +-- (구조는 유지, rsrc_nm만 갱신) +-- ============================================================================ + +-- ─── Level 0 (탭/그룹) ───────────────────────── +UPDATE kcg.auth_perm_tree SET rsrc_nm = '종합 상황판' WHERE rsrc_cd = 'dashboard'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '경보 현황판' WHERE rsrc_cd = 'monitoring'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '감시' WHERE rsrc_cd = 'surveillance'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '탐지·분석' WHERE rsrc_cd = 'detection'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '선박' WHERE rsrc_cd = 'vessel'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '위험평가' WHERE rsrc_cd = 'risk-assessment'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '순찰·경로' WHERE rsrc_cd = 'patrol'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '단속·이력' WHERE rsrc_cd = 'enforcement'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '현장 대응' WHERE rsrc_cd = 'field-ops'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'AI 운영' WHERE rsrc_cd = 'ai-operations'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '통계·보고' WHERE rsrc_cd = 'statistics'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '모선 워크플로우' WHERE rsrc_cd = 'parent-inference-workflow'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '시스템 관리' WHERE rsrc_cd = 'admin'; + +-- ─── Level 1 (서브탭/패널) ───────────────────── +-- monitoring 자식 (사이드바엔 없으나 향후 분리 가능성) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '알림 목록' WHERE rsrc_cd = 'monitoring:alert-list'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'KPI 패널' WHERE rsrc_cd = 'monitoring:kpi-panel'; + +-- surveillance 자식 (좌측: /events = live-map, /map-control) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '이벤트 목록' WHERE rsrc_cd = 'surveillance:live-map'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '위험도 지도' WHERE rsrc_cd = 'surveillance:map-control'; + +-- detection 자식 (사이드바: /dark-vessel, /gear-detection, /china-fishing) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '어구 탐지' WHERE rsrc_cd = 'detection:gear-detection'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '다크베셀 탐지' WHERE rsrc_cd = 'detection:dark-vessel'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '중국어선 분석' WHERE rsrc_cd = 'detection:china-fishing'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '어구식별' WHERE rsrc_cd = 'detection:gear-identification'; + +-- vessel 자식 (사이드바: /vessel/:id 단일) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '선박 상세' WHERE rsrc_cd = 'vessel:vessel-detail'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '환적·접촉 탐지' WHERE rsrc_cd = 'vessel:transfer-detection'; + +-- risk-assessment 자식 (사이드바: /risk-map, /enforcement-plan) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '위험도 지도' WHERE rsrc_cd = 'risk-assessment:risk-map'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '단속 계획' WHERE rsrc_cd = 'risk-assessment:enforcement-plan'; + +-- patrol 자식 (사이드바: /patrol-route, /fleet-optimization) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '순찰경로 추천' WHERE rsrc_cd = 'patrol:patrol-route'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '다함정 최적화' WHERE rsrc_cd = 'patrol:fleet-optimization'; + +-- enforcement 자식 (사이드바: /enforcement-history, /event-list) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '단속 이력' WHERE rsrc_cd = 'enforcement:enforcement-history'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '이벤트 목록' WHERE rsrc_cd = 'enforcement:event-list'; + +-- field-ops 자식 (사이드바: /mobile-service, /ship-agent, /ai-alert) +UPDATE kcg.auth_perm_tree SET rsrc_nm = '모바일 서비스' WHERE rsrc_cd = 'field-ops:mobile-service'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '함정 Agent' WHERE rsrc_cd = 'field-ops:ship-agent'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'AI 알림 발송' WHERE rsrc_cd = 'field-ops:ai-alert'; + +-- ai-operations 자식 +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'AI 의사결정 지원' WHERE rsrc_cd = 'ai-operations:ai-assistant'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'AI 모델관리' WHERE rsrc_cd = 'ai-operations:ai-model'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = 'MLOps' WHERE rsrc_cd = 'ai-operations:mlops'; + +-- statistics 자식 +UPDATE kcg.auth_perm_tree SET rsrc_nm = '통계 분석' WHERE rsrc_cd = 'statistics:statistics'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '외부 서비스' WHERE rsrc_cd = 'statistics:external-service'; + +-- parent-inference-workflow 자식 +UPDATE kcg.auth_perm_tree SET rsrc_nm = '모선 확정/거부' WHERE rsrc_cd = 'parent-inference-workflow:parent-review'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '후보 제외' WHERE rsrc_cd = 'parent-inference-workflow:parent-exclusion'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '학습 세션' WHERE rsrc_cd = 'parent-inference-workflow:label-session'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '전역 제외 관리' WHERE rsrc_cd = 'parent-inference-workflow:exclusion-management'; + +-- admin 자식 +UPDATE kcg.auth_perm_tree SET rsrc_nm = '사용자 관리' WHERE rsrc_cd = 'admin:user-management'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '역할 관리' WHERE rsrc_cd = 'admin:role-management'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '권한 관리' WHERE rsrc_cd = 'admin:permission-management'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '메뉴 설정' WHERE rsrc_cd = 'admin:menu-management'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '감사 로그' WHERE rsrc_cd = 'admin:audit-logs'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '접근 이력' WHERE rsrc_cd = 'admin:access-logs'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '로그인 이력' WHERE rsrc_cd = 'admin:login-history'; +UPDATE kcg.auth_perm_tree SET rsrc_nm = '환경설정' WHERE rsrc_cd = 'admin:system-config'; diff --git a/frontend/src/features/admin/AccessControl.tsx b/frontend/src/features/admin/AccessControl.tsx index d41b908..0f486a0 100644 --- a/frontend/src/features/admin/AccessControl.tsx +++ b/frontend/src/features/admin/AccessControl.tsx @@ -276,6 +276,7 @@ export function AccessControl() { searchPlaceholder="계정, 이름, 이메일 검색..." searchKeys={['userAcnt', 'userNm', 'email', 'rnkpNm']} exportFilename="사용자목록" + exportResource="admin:user-management" showPagination /> )} @@ -320,6 +321,7 @@ export function AccessControl() { searchPlaceholder="사용자, 액션, IP 검색..." searchKeys={['userAcnt', 'actionCd', 'resourceType', 'ipAddress']} exportFilename="감사로그" + exportResource="admin:audit-logs" title="모든 운영자 의사결정 자동 기록 (audit_log)" showPagination /> diff --git a/frontend/src/features/admin/PermissionsPanel.tsx b/frontend/src/features/admin/PermissionsPanel.tsx index 58b0dbd..8c5947b 100644 --- a/frontend/src/features/admin/PermissionsPanel.tsx +++ b/frontend/src/features/admin/PermissionsPanel.tsx @@ -134,15 +134,21 @@ export function PermissionsPanel() { const key = makeKey(rsrcCd, operCd); const explicit = draftPerms.get(key); - // 부모의 effective READ 확인 + // 1) 부모 노드의 effective READ가 거부되면 자식의 모든 작업 강제 거부 let parentReadDenied = false; if (parentCd) { const parentEff = effective.get(parentCd); parentReadDenied = !parentEff || !parentEff.has('READ'); } + if (parentReadDenied) return 'forced-denied'; - if (parentReadDenied && operCd !== 'READ') return 'forced-denied'; - if (parentReadDenied && operCd === 'READ' && parentCd) return 'forced-denied'; + // 2) 같은 노드의 READ가 effective로 거부되면 C/U/D/E도 강제 거부 + // (READ가 안 되면 그 페이지/리소스 자체에 접근 못 하므로 다른 작업 권한도 의미 없음) + if (operCd !== 'READ') { + const ownEff = effective.get(rsrcCd); + const ownReadGranted = ownEff?.has('READ') ?? false; + if (!ownReadGranted) return 'forced-denied'; + } if (explicit === 'Y') return 'explicit-granted'; if (explicit === 'N') return 'explicit-denied'; diff --git a/frontend/src/shared/components/common/DataTable.tsx b/frontend/src/shared/components/common/DataTable.tsx index 84fe7bc..5653778 100644 --- a/frontend/src/shared/components/common/DataTable.tsx +++ b/frontend/src/shared/components/common/DataTable.tsx @@ -4,6 +4,7 @@ import { Pagination } from './Pagination'; import { ExcelExport } from './ExcelExport'; import { PrintButton } from './PrintButton'; import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; +import { useAuth } from '@/app/auth/AuthContext'; /* * SFR-02 공통컴포넌트: 데이터 테이블 @@ -38,6 +39,8 @@ interface DataTableProps> { showPagination?: boolean; onRowClick?: (row: T) => void; className?: string; + /** 이 표가 속한 리소스 코드 (예: "admin:audit-logs"). EXPORT 권한 가드에 사용. */ + exportResource?: string; } export function DataTable>({ @@ -54,7 +57,12 @@ export function DataTable>({ showPagination = true, onRowClick, className = '', + exportResource, }: DataTableProps) { + // EXPORT 권한 체크: exportResource가 지정되면 hasPermission으로 가드 + // 미지정 시 항상 표시 (하위 호환) + const { hasPermission } = useAuth(); + const canExport = exportResource ? hasPermission(exportResource, 'EXPORT') : true; const [query, setQuery] = useState(''); const [page, setPage] = useState(0); const [sortKey, setSortKey] = useState(null); @@ -118,7 +126,7 @@ export function DataTable>({ /> )}
- {showExport && ( + {showExport && canExport && ( []} columns={columns.map((c) => ({ key: c.key, label: c.label }))}