feat(WEB): 권한 관리 시스템 구현 및 상세 페이지 권한 통합

- PermissionContext, usePermission 훅, PermissionGuard 컴포넌트 신규 추가
- AccessDenied 접근 거부 페이지 추가
- permissions lib (체커, 매퍼, 타입) 구현
- BadDebtDetail, BoardDetail, LaborDetail, PricingDetail 등 상세 페이지 권한 적용
- ProcessDetail, StepDetail, ItemDetail, PermissionDetail 권한 연동
- RootProvider에 PermissionProvider 통합
- protected layout 권한 체크 추가
- Claude 프로젝트 설정 파일 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-03 10:17:02 +09:00
parent f0987127eb
commit e111f7b362
22 changed files with 1267 additions and 994 deletions

View File

@@ -0,0 +1,65 @@
'use client';
import { usePathname } from 'next/navigation';
import { usePermissionContext } from '@/contexts/PermissionContext';
import { findMatchingUrl } from '@/lib/permissions/utils';
import type { UsePermissionReturn } from '@/lib/permissions/types';
/**
* URL 자동매칭 권한 훅
*
* 인자 없이 호출하면 현재 URL 기반 자동 매칭.
* 특수 케이스에서 URL 직접 지정 가능.
*
* @example
* // 자동 매칭 (대부분의 경우)
* const { canView, canCreate, canUpdate, canDelete } = usePermission();
*
* // URL 직접 지정 (다른 메뉴 권한 체크 시)
* const { canApprove } = usePermission('/approval/inbox');
*/
export function usePermission(overrideUrl?: string): UsePermissionReturn {
const pathname = usePathname();
const { permissionMap, isLoading } = usePermissionContext();
const targetPath = overrideUrl || pathname;
if (isLoading || !permissionMap) {
return {
canView: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canApprove: true,
isLoading,
matchedUrl: null,
};
}
const matchedUrl = findMatchingUrl(targetPath, permissionMap);
console.log('[usePermission]', targetPath, '→ matched:', matchedUrl, '| perms:', matchedUrl ? permissionMap[matchedUrl] : 'none');
if (!matchedUrl) {
return {
canView: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canApprove: true,
isLoading: false,
matchedUrl: null,
};
}
const perms = permissionMap[matchedUrl] || {};
return {
canView: perms.view ?? true,
canCreate: perms.create ?? true,
canUpdate: perms.update ?? true,
canDelete: perms.delete ?? true,
canApprove: perms.approve ?? true,
isLoading: false,
matchedUrl,
};
}