fix: 권한 트리 UX 개선 + 라벨 사이드바 일치 + EXPORT 가드
PermissionsPanel UI 수정: - 같은 노드의 effective READ가 거부되면 C/U/D/E도 forced-denied (READ가 안 되면 그 페이지 자체에 접근 못 하므로 다른 작업도 의미 없음) → 사용자가 Read를 N으로 바꾸는 즉시 같은 행의 CUDE도 회색 비활성 DataTable EXPORT 권한 가드: - exportResource prop 추가 - useAuth().hasPermission(resource, 'EXPORT')로 export 버튼 표시 여부 결정 - AccessControl의 사용자 관리 / 감사 로그 DataTable에 적용 - exportResource="admin:user-management" - exportResource="admin:audit-logs" Operation 의미 명확화: - ParentExclusion release 엔드포인트를 UPDATE → DELETE 로 재분류 (제외 항목을 "삭제(해제)"하는 의미가 더 정확) V007 마이그레이션: 권한 트리 명칭을 사이드바 i18n 라벨과 일치 - Level 0 13개 + Level 1 32개 노드의 rsrc_nm을 nav.* / group.* 라벨에 맞춤 - 예: "어구탐지" → "어구 탐지", "Dark Vessel" → "다크베셀 탐지" - 권한 관리 트리를 운영자가 사이드바와 동일한 명칭으로 이해 가능 API의 RCUDE 적용 현황 (참고): - READ 19건, UPDATE 8건, CREATE 4건, DELETE 1→2건 - EXPORT는 백엔드 엔드포인트 별도 없음 → 프론트 EXPORT 가드로 처리 - 향후 백엔드 CSV/Excel 생성 API 추가 시 EXPORT operation으로 가드 검증: - V007 마이그레이션 자동 적용 + Started in 3.272s - Level 0 13개 모두 사이드바 라벨로 변경됨 확인 - 프론트 빌드 통과 (599ms) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
95ca1018b5
커밋
f545aeafac
@ -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
|
||||
|
||||
@ -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';
|
||||
@ -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
|
||||
/>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<T extends Record<string, unknown>> {
|
||||
showPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
className?: string;
|
||||
/** 이 표가 속한 리소스 코드 (예: "admin:audit-logs"). EXPORT 권한 가드에 사용. */
|
||||
exportResource?: string;
|
||||
}
|
||||
|
||||
export function DataTable<T extends Record<string, unknown>>({
|
||||
@ -54,7 +57,12 @@ export function DataTable<T extends Record<string, unknown>>({
|
||||
showPagination = true,
|
||||
onRowClick,
|
||||
className = '',
|
||||
exportResource,
|
||||
}: DataTableProps<T>) {
|
||||
// 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<string | null>(null);
|
||||
@ -118,7 +126,7 @@ export function DataTable<T extends Record<string, unknown>>({
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 ml-auto">
|
||||
{showExport && (
|
||||
{showExport && canExport && (
|
||||
<ExcelExport
|
||||
data={sorted as Record<string, unknown>[]}
|
||||
columns={columns.map((c) => ({ key: c.key, label: c.label }))}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user