- 루트 문서 30개를 도메인별 하위 폴더로 이동 - accounting/, architecture/, dev/, guides/, security/ 등 카테고리 분류 - archive/ 폴더에 QA 스크린샷 이동 - _index.md 문서 맵 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
11 KiB
권한 시스템 구현 문서
작성일: 2025-01-20 최종 수정: 2026-02-02 상태: 구현 완료 + 1차 검수 완료 — 세부 검수 및 사이드바 필터링 미진행
1. 핵심 요약
구현 방식: URL 자동매칭 + Role 기반 API
| 항목 | 내용 |
|---|---|
| API | GET /api/v1/roles/{roleId}/permissions/matrix (설정 페이지와 동일) |
| 데이터 소스 | localStorage user.roles → roleId, user.menu → menuId↔URL 매핑 |
| 매칭 전략 | Longest prefix match (locale 자동 제거) |
| 다중 역할 | Union 병합 (하나라도 허용이면 허용) |
| Fallback | fail-open (API 실패, 미등록 URL → 전체 허용) |
| 즉시 반영 | 설정 페이지에서 변경 시 reloadPermissions() 호출 |
| 자기 잠금 방지 | /settings/permissions는 항상 접근 허용 |
2. 아키텍처
2.1 보호 계층 구조
AuthenticatedLayout ← 1차: 로그인 체크
└── PermissionProvider ← 권한 데이터 로드 (Role API)
└── PermissionGate ← 2차: 페이지 접근 권한 (view 자동 체크)
└── page.tsx ← 3차: 액션 권한 (버튼 숨김)
2.2 데이터 흐름
┌─────────────────────────────────────────────────────────────┐
│ 마운트 시 (또는 reloadPermissions 호출 시) │
│ │
│ 1. localStorage 'user' → roleIds + menuIdToUrl 매핑 추출 │
│ 2. API 호출: GET /api/v1/roles/{roleId}/permissions/matrix │
│ → { permissions: { [menuId]: { view: true, ... } } } │
│ 3. menuId → URL 변환 (localStorage 메뉴 데이터 활용) │
│ → { "/approval/draft": { view: true, create: false } } │
│ 4. 다중 역할: Union 병합 │
│ 5. PermissionContext에 permissionMap 저장 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 페이지 접근 시 (PermissionGate, 자동) │
│ │
│ usePathname() → "/ko/approval/draft" │
│ → locale 제거 → "/approval/draft" │
│ → longest prefix match → "/approval/draft" │
│ → permissionMap["/approval/draft"].view === false │
│ → <AccessDenied /> 렌더링 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 버튼 제어 시 (usePermission 훅, 컴포넌트별) │
│ │
│ const { canCreate, canDelete } = usePermission(); │
│ → canCreate === false → 생성/등록 버튼 숨김 │
│ → canDelete === false → 삭제 버튼 숨김 │
└─────────────────────────────────────────────────────────────┘
3. 파일 구조 및 역할
3.1 신규 생성 파일 (7개)
| 파일 | 역할 |
|---|---|
src/lib/permissions/types.ts |
PermissionAction, PermissionMap, UsePermissionReturn 타입 |
src/lib/permissions/actions.ts |
Server Action — getRolePermissionMatrix(roleId) |
src/lib/permissions/utils.ts |
buildMenuIdToUrlMap, convertMatrixToPermissionMap, mergePermissionMaps, findMatchingUrl |
src/contexts/PermissionContext.tsx |
PermissionProvider + PermissionGate + reloadPermissions |
src/hooks/usePermission.ts |
URL 자동매칭 훅 (현재 페이지 or overrideUrl) |
src/components/common/AccessDenied.tsx |
접근 차단 UI (ServerErrorPage 패턴) |
src/components/common/PermissionGuard.tsx |
버튼 레벨 권한 래퍼 컴포넌트 |
3.2 수정 파일 (6개)
| 파일 | 수정 내용 |
|---|---|
src/contexts/RootProvider.tsx |
PermissionProvider 추가 |
src/app/[locale]/(protected)/layout.tsx |
PermissionGate 추가 |
src/components/templates/UniversalListPage/index.tsx |
usePermission() — createButton, onDelete 제어 |
src/components/templates/IntegratedDetailTemplate/index.tsx |
usePermission() — canEdit/canDelete/canCreate fallback |
src/components/production/ProductionDashboard/index.tsx |
usePermission(overrideUrl) — 네비게이션 버튼 권한 |
src/components/settings/PermissionManagement/PermissionDetailClient.tsx |
reloadPermissions() 호출 (토글/전체허용/전체거부/초기화) |
4. 핵심 구현 디테일
4.1 API 선택 — Role 기반 (중요)
백엔드에 2개의 서로 다른 API가 존재:
| API | Controller | DB 테이블 | 용도 |
|---|---|---|---|
GET /api/v1/permissions/users/{id}/menu-matrix |
PermissionController |
model_has_permissions |
유저 직접 할당 권한만 |
GET /api/v1/roles/{id}/permissions/matrix |
RolePermissionController |
role_has_permissions |
역할 기반 권한 (설정 페이지와 동일) ✅ |
설정 페이지가 Role 기반으로 저장/조회하므로, Provider도 동일한 API 사용.
4.2 누락 action = false 처리 (중요)
설정 페이지 API는 허용된 권한만 응답에 포함. 비허용 권한은 키 자체를 생략.
// API 응답 예시 (create 비허용)
{ "view": true, "update": true, "delete": true, "approve": true }
// → create 키 없음
// convertMatrixToPermissionMap에서 처리
map[url][action] = perms[action] === true; // 누락 = false
4.3 menuId → URL 매핑
설정 페이지 API는 menuId로 키잉, 프론트는 URL 기반. localStorage 메뉴 데이터로 변환:
localStorage user.menu → [{ id: "123", path: "/approval/draft", children: [...] }]
→ buildMenuIdToUrlMap → { "123": "/approval/draft" }
API permissions → { "123": { view: true, create: false } }
결합 → { "/approval/draft": { view: true, create: false } }
4.4 자기 잠금 방지
const BYPASS_PATHS = ['/settings/permissions'];
// PermissionGate에서 이 경로는 항상 통과 → 관리자가 자신의 권한 설정 페이지 접근을 막아도 복구 가능
4.5 reloadPermissions
설정 페이지에서 권한 변경 시 새로고침 없이 즉시 반영:
// PermissionDetailClient.tsx
const { reloadPermissions } = usePermissionContext();
// 토글/전체허용/전체거부/초기화 성공 후
reloadPermissions();
5. 디버깅 과정에서 발견된 이슈
5.1 AuthContext vs localStorage 키 불일치 (해결)
| 항목 | 키 | 값 |
|---|---|---|
| 로그인 시 저장 | localStorage.setItem('user', ...) |
{ id: 33, roles: [...], menu: [...] } |
| AuthContext 로드 | localStorage.getItem('mes-currentUser') |
별도 데이터 |
결론: AuthContext의 currentUser를 사용하면 안 됨. localStorage user 키에서 직접 읽기.
5.2 raw fetch vs serverFetch 래퍼 (해결)
| 방식 | 문제 |
|---|---|
fetch() 직접 사용 |
토큰 갱신 안 됨, 잘못된 API_KEY env var, Accept 헤더 누락 |
serverFetch() 래퍼 |
토큰 자동 갱신, 올바른 헤더, 401 처리 ✅ |
결론: 프로젝트의 모든 Server Action은 serverFetch() 사용 필수.
5.3 User API vs Role API (해결)
PermissionController.getMenuMatrix(scope='user')는 model_has_permissions 테이블만 조회.
설정 페이지의 토글은 role_has_permissions 테이블에 저장.
→ 서로 다른 테이블 → 데이터 불일치 → Role API로 전환.
5.4 API 응답에서 비허용 권한 생략 (해결)
RolePermissionController::matrix()는 허용된 권한만 포함, 비허용은 키 생략.
→ if (action in perms) 체크로는 비허용 감지 불가
→ perms[action] === true 로 변경 (누락 = false)
6. 검수 상태
1차 검수 (2026-02-02) ✅ 완료
- view 해제 → 페이지 접근 차단 (AccessDenied 표시)
- create 해제 → 생성/등록 버튼 숨김 (UniversalListPage 기반 페이지)
- 전체 허용 / 전체 거부 → 정상 반영
- 설정 변경 → 새로고침 없이 즉시 반영 (reloadPermissions)
- fail-open: API 실패 시 전체 허용
세부 검수 (미진행)
- update 해제 → 수정 버튼 숨김 테스트
- delete 해제 → 삭제 버튼 숨김 테스트
- approve 해제 → 승인 버튼 숨김 테스트
- IntegratedDetailTemplate 기반 페이지 검수
- 커스텀 페이지(대시보드 등) 버튼 권한 검수
- 다중 역할 유저 테스트 (Union 병합)
추가 기능 (미구현)
- 사이드바 메뉴 필터링 (view=false 메뉴 숨김)
- 디버그 console.log 제거 (
usePermission.ts등)
7. 정책 결정사항
| 상황 | 정책 | 이유 |
|---|---|---|
| 백엔드 미등록 URL | 허용 | 권한 대상 외 |
| API 실패 | 허용 (fail-open) | 폐쇄형 시스템 |
| 권한 로딩 중 | 허용 | UX 우선 |
| action 키 누락 (API 미포함) | 비허용 (false) | API가 허용만 포함 |
| 다중 역할 충돌 | Union (하나라도 허용이면 허용) | 최대 권한 원칙 |
| 권한 설정 페이지 | 항상 접근 허용 | 자기 잠금 방지 |
8. 백엔드 참고
| 구분 | 파일 경로 |
|---|---|
| RolePermissionController | sam-api/app/Http/Controllers/Api/V1/RolePermissionController.php |
| RolePermissionService | sam-api/app/Services/RolePermissionService.php |
| PermissionController | sam-api/app/Http/Controllers/Api/V1/PermissionController.php |
| PermissionService | sam-api/app/Services/PermissionService.php |
| Route 정의 | sam-api/routes/api/v1/common.php (line 74-92) |
문서 업데이트 이력:
- 2025-01-20: 최초 작성 (menuCode 수동 매핑 방식)
- 2026-02-02: 전략 전환 (menuCode → URL 자동매칭)
- 2026-02-02: Phase 1~4 구현 완료
- 2026-02-02: API 전환 (User API → Role API), 버그 수정 4건, 1차 검수 완료