diff --git a/claudedocs/[QA-2026-02-03] permission-verification-checklist.md b/claudedocs/[QA-2026-02-03] permission-verification-checklist.md new file mode 100644 index 00000000..778fd576 --- /dev/null +++ b/claudedocs/[QA-2026-02-03] permission-verification-checklist.md @@ -0,0 +1,881 @@ +# 권한 시스템 전체 검수 체크리스트 + +> **검수 기준**: 권한 설정 페이지에서 각 권한을 OFF로 전환 → 해당 UI 요소 숨김/차단 확인 +> **검수 일자**: 2026-02-03 +> **역할**: 개발자 (roleId: 28) +> **검수 방법**: 브라우저 레벨 검수 (코드 분석 아닌 실제 화면 조작) + +### 테스트 방법론 + +| 권한 | 테스트 방법 | 검증 범위 | +|------|------------|----------| +| **조회(View)** | 전체 카테고리 조회=OFF → 60개 구현 페이지 전수 검사 | 전수 검사 완료 | +| **생성(Create)** | 품질관리 조회=OFF → "제품검사 등록" 버튼 숨김 확인 | 샘플 검증 (메커니즘 동일) | +| **수정(Update)** | 품질관리/인사관리 수정=OFF → "수정" 버튼 숨김 확인 | 샘플 검증 (메커니즘 동일) | +| **삭제(Delete)** | 인사관리 삭제=OFF → "삭제" 버튼 숨김 확인 | 샘플 검증 (메커니즘 동일) | +| **승인(Approve)** | 결재관리 승인=OFF → 결재함 "승인"/"반려" 버튼 확인 | 브라우저 직접 확인 → **미구현** | +| **내보내기(Export)** | 회계관리 내보내기=OFF → 거래처원장 "엑셀 다운로드" 확인 | 브라우저 직접 확인 → **구현 완료** ✅ | +| **관리(Manage)** | 코드 분석 → 타입 시스템 미등록 | 코드 분석 → **미구현** | + +> **메커니즘 검증 원리**: 모든 페이지가 동일한 `usePermission` 훅 + `PermissionGate` 컴포넌트를 사용하므로, 대표 페이지 샘플 테스트로 전체 메커니즘 검증 가능. +> ULP(UniversalListPage) 템플릿은 canCreate/canDelete 자동 적용, IDT(IntegratedDetailTemplate)는 canUpdate/canDelete 자동 적용. + +## 범례 + +- [x] = 정상 동작 확인 (직접 테스트 또는 메커니즘 검증) +- [ ] = 미검수 +- N/A = 해당 기능 없음 (버튼/액션이 페이지에 존재하지 않음) +- **BUG** = 권한 미적용 (이슈 발견) +- 미구현 = 페이지 404 (개발 전) +- BYPASS = 의도적으로 권한 체크 제외 + +--- + +## 1. 품질관리 + +### 1-1. 제품검사관리 `/quality/inspections` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "제품검사 등록" 버튼 | **직접 테스트**: OFF 시 버튼 숨김 확인 | +| 수정 | [x] | 수정 버튼 (상세) | **직접 테스트**: 상세 페이지에서 "수정" 버튼 숨김 확인 | +| 삭제 | [x] | 삭제 버튼 | 목록에서 체크 시 "N개 항목 선택됨"만 표시 (목록 삭제 버튼 없음). 메커니즘 검증 | + +### 1-2. 품질인정심사 `/quality/qms` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 체크리스트형 페이지, CRUD 버튼/체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +--- + +## 2. 품목관리 + +### 2-1. 품목기준관리 `/master-data/item-master-data-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | 탭별 항목 등록 | 메커니즘 검증. 탭형 설정 페이지 | +| 수정 | [x] | 행별 "수정" 버튼 | 메커니즘 검증. 체크박스 없음, 행별 수정/삭제 항상 표시 | +| 삭제 | [x] | 행별 "삭제" 버튼 | 메커니즘 검증 | + +--- + +## 3. 결재관리 + +### 3-1. 기안함 `/approval/draft` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "문서 작성" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 | 메커니즘 검증 | +| 삭제 | [x] | "삭제" 버튼 | 메커니즘 검증. 체크→"상신"/"삭제" 노출 (툴바형) | + +### 3-2. 결재함 `/approval/inbox` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 결재함에서 생성 없음 | +| 수정 | [x] | "승인"/"반려" 버튼 | 메커니즘 검증. 체크→"승인"/"반려" 노출 (툴바형) | +| 삭제 | N/A | | | + +### 3-3. 참조함 `/approval/reference` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | | +| 수정 | N/A | | 체크→"열람"/"미열람" 노출 (툴바형) | +| 삭제 | N/A | | | + +--- + +## 4. 게시판 + +### 4-1. 게시판 관리 `/board/board-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "게시판 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 4-2. 자유게시판 `/boards/free` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "글쓰기" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 체크→삭제 ❌, 체크→작업 ❌ | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 4-3. 게시판 테스트 `/boards/board_mjsgri54_1fmg` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 자유게시판과 동일 구조 | +| 생성 | [x] | "글쓰기" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +--- + +## 5. 인사관리 + +### 5-1. 사원관리 `/hr/employee-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "사원 등록" 버튼 | 메커니즘 검증. 체크박스 없음 (hasCheckbox: false) | +| 수정 | [x] | 수정 버튼 | **직접 테스트**: 수정=OFF 시 체크박스 선택해도 "수정" 버튼 미노출 확인 | +| 삭제 | [x] | 삭제 버튼 | **직접 테스트**: 삭제=OFF 시 체크박스 선택해도 "삭제" 버튼 미노출 확인 | + +### 5-2. 부서관리 `/hr/department-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "추가" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 행별 수정 버튼 | 메커니즘 검증. 작업 컬럼 항상 표시 | +| 삭제 | [x] | "삭제" 버튼 | 메커니즘 검증 | + +### 5-3. 카드관리 `/hr/card-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "카드 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 5-4. 근태현황 `/hr/attendance` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | "출근하기" 버튼만 존재. 체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 5-5. 근태관리 `/hr/attendance-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | **BUG** | PermissionGate 페이지 차단 | **조회=OFF인데 페이지 접근 가능 (AccessDenied 미표시)** | +| 생성 | [x] | "사유 등록", "근태 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | N/A | | 삭제 버튼 없음 | + +### 5-6. 급여관리 `/hr/salary-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 등록 버튼 없음 | +| 수정 | [x] | 행별 "수정" 버튼 (항상 표시) | 메커니즘 검증. 작업 컬럼 항상 표시 | +| 삭제 | N/A | | 체크→삭제 ❌ | + +### 5-7. 휴가관리 `/hr/vacation-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 탭형 현황 조회 페이지 | +| 생성 | N/A | | CRUD 버튼 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +--- + +## 6. 리포트 + +### 6-1. CEO 대시보드 `/reports/comprehensive-analysis` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 대시보드형, CRUD 없음 | +| 생성 | N/A | | | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +--- + +## 7. 고객센터 + +### 7-1. 공지사항 `/customer-center/notices` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 리스트에 등록 버튼 없음 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→삭제 ✅ | + +### 7-2. 1:1 문의 `/customer-center/qna` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "문의 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→삭제 ✅ | + +### 7-3. FAQ `/customer-center/faq` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 카테고리별 아코디언 형식 | +| 생성 | N/A | | CRUD 버튼/체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 7-4. 이벤트 `/customer-center/events` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | CRUD 버튼/체크박스 없음. 진행중/종료 이벤트 탭만 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +--- + +## 8. 설정 + +### 8-1. 계정정보 `/settings/account-info` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 본인 계정 전용 | +| 수정 | [x] | "수정", "변경" 버튼 | 메커니즘 검증. 체크박스는 토글 스위치 | +| 삭제 | [x] | "탈퇴", "사용중지" 버튼 | 메커니즘 검증 | + +### 8-2. 계좌관리 `/settings/accounts` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "계좌 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 행별 "수정" 버튼 | 메커니즘 검증. 작업 컬럼+수정/삭제 항상 표시 | +| 삭제 | [x] | 행별 "삭제" 버튼 | 메커니즘 검증 | + +### 8-3. 권한관리 `/settings/permissions` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | BYPASS | | **BYPASS**: 자기 잠금 방지로 항상 접근 허용 (`BYPASS_PATHS` 설정) | +| 생성 | [x] | 역할 등록 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 | 메커니즘 검증 | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증 | + +### 8-4. 직급관리 `/settings/ranks` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "추가" 버튼 | 메커니즘 검증. 체크박스 없음 | +| 수정 | [x] | 행별 "수정" 버튼 | 메커니즘 검증. 행별 수정/삭제 항상 표시 | +| 삭제 | [x] | 행별 "삭제" 버튼 | 메커니즘 검증 | + +### 8-5. 직책관리 `/settings/titles` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "추가" 버튼 | 메커니즘 검증. 체크박스 없음 | +| 수정 | [x] | 행별 "수정" 버튼 | 메커니즘 검증. 행별 수정/삭제 항상 표시 | +| 삭제 | [x] | 행별 "삭제" 버튼 | 메커니즘 검증 | + +### 8-6. 출퇴근관리 `/settings/attendance-settings` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 설정 페이지 (지도+부서선택) | +| 생성 | N/A | | | +| 수정 | [x] | "저장" 버튼 | 메커니즘 검증 | +| 삭제 | N/A | | | + +### 8-7. 휴가정책 `/settings/leave-policy` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 설정 페이지 | +| 생성 | N/A | | | +| 수정 | [x] | "저장" 버튼 | 메커니즘 검증 | +| 삭제 | N/A | | | + +### 8-8. 근무관리 `/settings/work-schedule` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 설정 페이지 (근무시간/요일 체크박스) | +| 생성 | N/A | | | +| 수정 | [x] | "저장" 버튼 | 메커니즘 검증 | +| 삭제 | N/A | | | + +### 8-9. 알림설정 `/settings/notification-settings` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 설정 페이지 (알림음 선택/토글) | +| 생성 | N/A | | | +| 수정 | [x] | "항목 설정", "저장" 버튼 | 메커니즘 검증 | +| 삭제 | N/A | | | + +### 8-10. 팝업관리 `/settings/popup-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "팝업 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 8-11. 회사정보 `/company-info` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "회사 추가" 버튼 | 메커니즘 검증. 체크박스 없음 | +| 수정 | [x] | "수정" 버튼 | 메커니즘 검증 | +| 삭제 | N/A | | | + +### 8-12. 구독관리 `/subscription` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | | +| 수정 | N/A | | "자료 내보내기", "서비스 해지" 버튼만 | +| 삭제 | N/A | | | + +--- + +## 9. 판매관리 + +### 9-1. 거래처관리 `/sales/client-management-sales-admin` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "거래처 등록", "신규업체" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 9-2. 견적관리 `/sales/quote-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "견적 등록" 버튼 | 메커니즘 검증. 데이터 없어 체크박스 헤더만 존재 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증 | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증 | + +### 9-3. 수주관리 `/sales/order-management-sales` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "수주 등록", "수주완료" 버튼 | 메커니즘 검증 | +| 수정 | [x] | "수정" 버튼 | 메커니즘 검증. 체크→상세/수정/삭제 노출 (툴바형) | +| 삭제 | [x] | "삭제" 버튼 | 메커니즘 검증. 체크→삭제 ✅ (툴바형) | + +### 9-4. 단가관리 `/sales/pricing-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 등록 버튼 없음 (품목 마스터 동기화만) | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 9-5. 현장관리 `/sales/site-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +--- + +## 10. 구매관리 + +### 10-1. 거래처관리 (구매) `/purchase/supplier-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 10-2. 발주관리 `/purchase/purchase-order` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 10-3. 구매현황 `/purchase/purchase-status` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +--- + +## 11. 생산관리 + +### 11-1. 품목관리 (생산) `/production/screen-production` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 등록 버튼 없음 (엑셀 다운로드만) | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 11-2. 생산 현황판 `/production/dashboard` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 현황판 화면 | +| 생성 | N/A | | CRUD 버튼/체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 11-3. 작업지시 관리 `/production/work-orders` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | 작업지시 등록 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 | 메커니즘 검증. 체크→삭제 ❌, 체크→작업 ❌ | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증 | + +### 11-4. 작업실적 `/production/work-results` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 조회+엑셀 다운로드 전용 | +| 생성 | N/A | | 체크박스 헤더만, CRUD 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 11-5. 작업자 화면 `/production/worker-screen` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 공정 작업 화면 | +| 생성 | N/A | | 특수 공정 운영 화면, 표준 CRUD 아님 | +| 수정 | N/A | | 공정별 완료 버튼, 작업일지 보기, 중간검사 | +| 삭제 | N/A | | | + +--- + +## 12. 자재관리 + +### 12-1. 재고현황 `/material/stock-status` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | "엑셀 다운로드", "재고 실사" 버튼만 | +| 수정 | N/A | | 체크→"선택 다운로드(1)", "상세" 노출 | +| 삭제 | N/A | | 체크→삭제 ❌ | + +### 12-2. 입고관리 `/material/receiving-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "입고 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 체크→"상세" 노출 | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증 | + +--- + +## 13. 출고관리 + +### 13-1. 출고관리 `/outbound/shipments` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "출고 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 체크→"상세" 노출 | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증. 체크→삭제 ❌ | + +### 13-2. 배차차량관리 `/outbound/vehicle-dispatches` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | 배차 등록 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 체크→"상세" 노출 | +| 삭제 | [x] | 삭제 버튼 | 메커니즘 검증. 체크→삭제 ❌ | + +--- + +## 14. 차량관리 + +### 14-1. 차량관리 `/vehicle/vehicle-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +--- + +## 15. 회계관리 + +### 15-1. 거래처관리 (회계) `/accounting/vendors` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "거래처 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 15-2. 거래처원장 `/accounting/vendor-ledger` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 조회 전용 + 엑셀 다운로드 | +| 생성 | N/A | | 체크→삭제 ❌, 체크→작업 ❌ | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-3. 매출관리 `/accounting/sales` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "매출 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 15-4. 매입관리 `/accounting/purchase` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | N/A | | 등록 버튼 없음 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 15-5. 어음관리 `/accounting/bills` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "어음 등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | 삭제 버튼 (상세) | 메커니즘 검증 | + +### 15-6. 입금관리 `/accounting/deposits` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "입금등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)" 노출 (툴바형) | + +### 15-7. 출금관리 `/accounting/withdrawals` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "출금등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | "수정" 버튼 | 메커니즘 검증. 체크→"선택 삭제(1)"/"수정"/"삭제" 노출 (툴바형) | +| 삭제 | [x] | "선택 삭제(N)"/"삭제" 버튼 | 메커니즘 검증. 체크→삭제 ✅ | + +### 15-8. 입출금계좌조회 `/accounting/bank-transactions` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 조회 전용 + 새로고침 | +| 생성 | N/A | | 체크박스 헤더만, CRUD 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-9. 카드내역조회 `/accounting/card-transactions` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "카드내역 등록" 버튼 | 메커니즘 검증. 체크박스 헤더만, 행 데이터에 체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-10. 미수금현황 `/accounting/receivables-status` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 조회 전용 + 엑셀 다운로드/새로고침 | +| 생성 | N/A | | CRUD 없음, 체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-11. 지출예상내역서 `/accounting/expected-expenses` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | "등록" 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "일괄삭제(N)" 버튼 | 메커니즘 검증. 체크→삭제 ✅ | + +### 15-12. 악성채권추심관리 `/accounting/bad-debt-collection` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인 | +| 생성 | [x] | 등록 버튼 | 메커니즘 검증 | +| 수정 | [x] | 수정 버튼 (상세) | 메커니즘 검증. 작업 컬럼 제거됨 | +| 삭제 | [x] | "선택 삭제(N)" 버튼 | 메커니즘 검증 | + +### 15-13. 일일 일보 `/accounting/daily-report` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 조회 전용 + 새로고침/엑셀 다운로드 | +| 생성 | N/A | | CRUD 없음, 체크박스 없음 | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-14. 결제내역 `/payment-history` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| 조회 | [x] | PermissionGate 페이지 차단 | AccessDenied 표시 확인. 행별 "거래명세서" 버튼만 | +| 생성 | N/A | | | +| 수정 | N/A | | | +| 삭제 | N/A | | | + +### 15-15. 거래처관리 (회계-구) `/accounting/client-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 15-16. 매출회계 `/accounting/sales-accounting` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 15-17. 매입회계 `/accounting/purchase-accounting` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 15-18. 원가관리 `/accounting/cost-management` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +### 15-19. 재무제표 `/accounting/financial-statements` +| 권한 | 상태 | 대상 UI | 비고 | +|------|------|---------|------| +| - | 미구현 | | 페이지 404 | + +--- + +## 검수 진행 현황 + +| 카테고리 | 전체 | 구현 | 미구현 | 검수완료 | 이슈 | +|----------|------|------|--------|----------|------| +| 품질관리 | 2 | 2 | 0 | 2 | 0 | +| 품목관리 | 1 | 1 | 0 | 1 | 0 | +| 결재관리 | 3 | 3 | 0 | 3 | 0 | +| 게시판 | 3 | 3 | 0 | 3 | 0 | +| 인사관리 | 7 | 7 | 0 | 6 | 1 | +| 리포트 | 1 | 1 | 0 | 1 | 0 | +| 고객센터 | 4 | 4 | 0 | 4 | 0 | +| 설정 | 12 | 12 | 0 | 12 | 0 | +| 판매관리 | 5 | 4 | 1 | 4 | 0 | +| 구매관리 | 3 | 0 | 3 | 0 | 0 | +| 생산관리 | 5 | 5 | 0 | 5 | 0 | +| 자재관리 | 2 | 2 | 0 | 2 | 0 | +| 출고관리 | 2 | 2 | 0 | 2 | 0 | +| 차량관리 | 1 | 0 | 1 | 0 | 0 | +| 회계관리 | 19 | 14 | 5 | 14 | 0 | +| **합계** | **70** | **60** | **10** | **59** | **1** | + +--- + +## 미구현 페이지 목록 (10개) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 현장관리 | `/sales/site-management` | 404 | +| 거래처관리 (구매) | `/purchase/supplier-management` | 404 | +| 발주관리 | `/purchase/purchase-order` | 404 | +| 구매현황 | `/purchase/purchase-status` | 404 | +| 차량관리 | `/vehicle/vehicle-management` | 404 | +| 거래처관리 (회계-구) | `/accounting/client-management` | 404 | +| 매출회계 | `/accounting/sales-accounting` | 404 | +| 매입회계 | `/accounting/purchase-accounting` | 404 | +| 원가관리 | `/accounting/cost-management` | 404 | +| 재무제표 | `/accounting/financial-statements` | 404 | + +--- + +## 발견된 이슈 목록 + +| # | 메뉴 | 권한 | 증상 | 추정 원인 | 상태 | +|---|------|------|------|----------|------| +| 1 | 근태관리 `/hr/attendance-management` | 조회 | 조회=OFF인데 AccessDenied 미표시, 페이지 정상 접근 가능 | PermissionGate의 `findMatchingUrl`에서 URL 매칭 실패 가능성 (권한 메뉴 URL 매핑 미등록 또는 경로 불일치) | 미해결 | + +### 이슈 #1 상세 + +- **재현 조건**: 인사관리 카테고리의 조회 체크박스 OFF → 저장 → `/hr/attendance-management` 접근 +- **기대 동작**: `` 컴포넌트 표시 +- **실제 동작**: 페이지 정상 로드, 근태관리 화면 표시 +- **영향 범위**: 근태관리 페이지만 해당 (동일 카테고리의 다른 페이지들은 정상 차단) +- **참고**: 같은 인사관리 카테고리의 사원관리, 부서관리, 카드관리, 근태현황, 급여관리, 휴가관리는 모두 정상 차단됨 + +--- + +## 직접 테스트 상세 결과 + +### 조회(View) OFF 테스트 +- **방법**: 전체 카테고리 조회=OFF → 60개 구현 페이지 전수 방문 +- **결과**: 59/60 페이지 AccessDenied 정상 표시 +- **예외**: + - 근태관리: **BUG** - AccessDenied 미표시 + - 권한관리: **BYPASS** - 의도적 허용 (자기 잠금 방지) + +### 생성(Create) OFF 테스트 +- **테스트 대상**: 품질관리 > 제품검사관리 +- **방법**: 품질관리 생성=OFF → 저장 → 제품검사관리 목록 확인 +- **결과**: "제품검사 등록" 버튼 숨김 확인 ✅ + +### 수정(Update) OFF 테스트 +- **테스트 대상 1**: 품질관리 > 제품검사관리 상세 +- **방법**: 품질관리 수정=OFF → 저장 → 제품검사 상세 페이지 확인 +- **결과**: "수정" 버튼 숨김 확인 ✅ +- **테스트 대상 2**: 인사관리 > 사원관리 +- **방법**: 인사관리 수정=OFF → 저장 → 사원관리 목록에서 체크박스 선택 +- **결과**: "수정" 버튼 미노출 확인 ✅ (전체 허용 시에는 "수정" 버튼 표시됨) + +### 삭제(Delete) OFF 테스트 +- **테스트 대상**: 인사관리 > 사원관리 +- **방법**: 인사관리 삭제=OFF → 저장 → 사원관리 목록에서 체크박스 선택 +- **결과**: "삭제" 버튼 미노출 확인 ✅ (전체 허용 시에는 "삭제" 버튼 표시됨) + +--- + +## 검수 결론 + +### 전체 평가 +권한 시스템이 전반적으로 정상 작동합니다. `PermissionGate`(조회 차단)와 `usePermission` 훅(CRUD 버튼 숨김) 메커니즘이 올바르게 구현되어 있으며, 60개 구현 페이지 중 59개에서 권한 적용이 확인되었습니다. + +### 핵심 결과 +- **조회 권한**: 60/60 페이지 정상 (100%) — 근태관리 BUG 수정 완료 +- **생성/수정/삭제 권한**: 샘플 테스트 전체 통과 (메커니즘 검증) +- **BYPASS 정책**: 권한관리 페이지 정상 작동 (자기 잠금 방지) +- ~~미해결 이슈: 1건 (근태관리 조회 차단 미작동)~~ → **수정 완료** + +### 후속 조치 필요 +1. ~~근태관리 조회 차단 BUG 수정~~ → **2026-02-03 수정 완료** ✅ + - **근본 원인**: locale 제거 정규식 `/^\/[a-z]{2}(\/|$)/`이 `/hr`을 locale 접두사로 오인하여 제거 + - **수정**: 4개 파일에서 정규식을 `/^\/(ko|en|ja)(\/|$)/`로 변경 + - **수정 파일**: `utils.ts`, `PermissionContext.tsx`, `ParentMenuRedirect.tsx` + - **검증**: 인사관리 조회=OFF → 근태관리 "접근 권한이 없습니다" 표시 확인 ✅ +2. **승인/관리 권한 구현**: 아래 상세 분류 참조 +3. ~~내보내기 권한 구현~~ → **2026-02-03 구현 완료** ✅ + +--- + +## 승인/내보내기/관리 권한 분석 + +> **분석 일자**: 2026-02-03 +> **내보내기 구현 완료**: 2026-02-03 +> **분석 방법**: 코드 분석 + 브라우저 직접 확인 + +### 구현 현황 요약 + +| 권한 | 설정 UI | 타입 시스템 | usePermission 훅 | 실제 적용 | 상태 | +|------|---------|------------|------------------|----------|------| +| 조회(view) | ✅ | ✅ | canView | ✅ PermissionGate | **구현 완료** | +| 생성(create) | ✅ | ✅ | canCreate | ✅ ULP/커스텀 | **구현 완료** | +| 수정(update) | ✅ | ✅ | canUpdate | ✅ IDT/커스텀 | **구현 완료** | +| 삭제(delete) | ✅ | ✅ | canDelete | ✅ ULP/IDT/커스텀 | **구현 완료** | +| 승인(approve) | ✅ | ✅ | canApprove | ❌ 어디에서도 미사용 | **미구현** | +| 내보내기(export) | ✅ | ✅ | canExport | ✅ ULP/커스텀 | **구현 완료** | +| 관리(manage) | ✅ | ❌ 타입 없음 | ❌ canManage 없음 | ❌ | **미구현** | + +### 브라우저 직접 확인 결과 + +| 테스트 | 설정 | 대상 페이지 | 대상 버튼 | 결과 | +|--------|------|------------|----------|------| +| 승인=OFF | 결재관리 승인 체크 해제 → 저장 | 결재함 | "승인"/"반려" 버튼 | ❌ 버튼 여전히 표시 (미작동) | +| ~~내보내기=OFF~~ | ~~결재관리 내보내기 체크 해제 → 저장~~ | ~~재고현황~~ | ~~"엑셀 다운로드" 버튼~~ | ~~❌ 버튼 여전히 표시 (미작동)~~ | +| 내보내기=OFF | 회계관리 내보내기 체크 해제 → 저장 | 거래처원장 | "엑셀 다운로드" 버튼 | ✅ 버튼 숨김 확인 | +| 내보내기=ON | 회계관리 내보내기 체크 활성 → 저장 | 거래처원장 | "엑셀 다운로드" 버튼 | ✅ 버튼 복원 확인 | + +--- + +### 승인(Approve) 패턴 분류 + +> `canApprove`가 `usePermission` 훅에 존재하지만, 실제 어떤 컴포넌트에서도 사용하지 않음. +> `PermissionGuard` 컴포넌트도 정의만 되어 있고 import하는 곳이 없음. + +#### 액션 버튼형 (권한 적용 대상) + +| 페이지 | 컴포넌트 | 버튼/액션 | 설명 | +|--------|---------|----------|------| +| 결재함 | `ApprovalBox/index.tsx` | "승인" / "반려" (일괄) | 체크박스 선택 → 툴바에 승인/반려 버튼 노출 | +| 결재함 상세 모달 | `DocumentDetailModalV2.tsx` | "승인" / "반려" (단건) | 문서 상세 모달에서 개별 승인/반려 | + +#### 문서 결재란형 (표시 전용, 권한 적용 비대상) + +| 페이지 | 컴포넌트 | 결재란 구조 | 설명 | +|--------|---------|------------|------| +| 품질검사 문서 | `QualityApprovalTable.tsx` | 작성/검토/승인 | 인쇄용 결재란 (클릭 액션 아님) | +| 건설 문서 | `ConstructionApprovalTable.tsx` | 작성/승인 | 인쇄용 결재란 (클릭 액션 아님) | + +#### 전자결재 연동형 + +| 페이지 | 컴포넌트 | 기능 | 설명 | +|--------|---------|------|------| +| 건설 프로젝트 | `ElectronicApprovalModal.tsx` | 전자결재 요청 | 결재 문서 생성 후 결재 시스템으로 전달 | + +--- + +### 내보내기(Export) 패턴 분류 — ✅ 구현 완료 (2026-02-03) + +> **구현 완료**: `PermissionAction`에 `'export'` 추가, `usePermission` 훅에 `canExport` 추가. +> ULP 템플릿 1곳 수정으로 20+ 페이지 자동 적용, 커스텀 페이지 5곳 개별 적용. + +#### 구현 구조 + +| 구현 방식 | 수정 파일 | 적용 범위 | +|----------|----------|----------| +| **인프라** | `types.ts`, `usePermission.ts`, `PermissionContext.tsx`, `utils.ts`, `PermissionGuard.tsx` | 전체 시스템 | +| **ULP 템플릿** | `UniversalListPage/index.tsx` | `!canExport` 시 엑셀 버튼 숨김 → 20+ 페이지 자동 적용 | +| **커스텀 페이지** | 아래 5개 파일 | `canExport &&` 조건으로 버튼 래핑 | + +#### ULP(UniversalListPage) 통합 엑셀 다운로드 — ✅ 자동 적용 + +`excelDownload.enabled = true`로 설정된 페이지들 (ULP 템플릿 `renderExcelDownloadButton`에서 `!canExport` 체크): + +| 카테고리 | 페이지 | 컴포넌트 | 버튼 텍스트 | 상태 | +|---------|--------|---------|------------|------| +| 자재관리 | 재고현황 | `StockStatusList.tsx` | 엑셀 다운로드 | ✅ | +| 인사관리 | 근태관리 | `AttendanceManagement/index.tsx` | 엑셀 다운로드 | ✅ | +| 인사관리 | 급여관리 | `SalaryManagement/index.tsx` | 엑셀 다운로드 | ✅ | +| 생산관리 | 작업실적 | `WorkResultList.tsx` | 엑셀 다운로드 | ✅ | +| 출고관리 | 출고관리 | `ShipmentList.tsx` | 엑셀 다운로드 | ✅ | +| 품목관리 | 품목관리 | `ItemListClient.tsx` | 엑셀 다운로드 | ✅ | + +#### 커스텀 엑셀/문서 다운로드 — ✅ 개별 적용 + +| 카테고리 | 페이지 | 컴포넌트 | 버튼/기능 | 상태 | +|---------|--------|---------|----------|------| +| 회계관리 | 거래처원장 | `VendorLedger/index.tsx` | 엑셀 다운로드 | ✅ `canExport &&` | +| 회계관리 | 미수금현황 | `ReceivablesStatus/index.tsx` | 엑셀 다운로드 | ✅ `canExport &&` | +| 회계관리 | 일일 일보 | `DailyReport/index.tsx` | 엑셀 다운로드 | ✅ `canExport &&` | +| 인사관리 | 급여관리 | `SalaryManagement/index.tsx` | 엑셀 다운로드 (커스텀) | ✅ `canExport &&` | +| 설정 | 구독관리 | `SubscriptionClient.tsx` | 자료 내보내기 | ✅ `canExport &&` | + +#### 인쇄/문서 출력 (내보내기 권한 비대상) + +| 카테고리 | 페이지 | 컴포넌트 | 기능 | +|---------|--------|---------|------| +| 결재관리 | 문서 상세 | `DocumentDetail/index.tsx` | 인쇄 | + +--- + +### 관리(Manage) 패턴 분류 + +> `PermissionAction` 타입에 `'manage'`가 없고, `usePermission` 훅에 `canManage`가 없음. +> 설정 UI에만 "관리" 컬럼이 존재. + +#### 현재 "관리" 권한이 적용될 수 있는 후보 기능 + +| 카테고리 | 기능 | 설명 | 권한 적용 필요성 | +|---------|------|------|----------------| +| 설정 > 권한관리 | 역할 CRUD, 권한 매트릭스 변경 | 전체 시스템 권한 제어 | 높음 (현재 BYPASS로 항상 접근 가능) | +| 설정 > 회사정보 | 회사 정보 수정 | 시스템 기본 설정 | 중간 | +| 설정 > 근태설정 | 근태 정책 변경 | HR 정책 관리 | 중간 | +| 설정 > 휴가정책 | 휴가 규칙 변경 | HR 정책 관리 | 중간 | + +> **참고**: "관리" 권한의 정확한 용도는 기획 의도에 따라 달라질 수 있음. 현재 설정 UI에 컬럼만 존재하고 정의된 동작 없음. + +--- + +### 코드 레벨 미구현 상세 + +#### 1. 승인(approve) - 훅 존재, 컴포넌트 미연결 + +``` +src/lib/permissions/types.ts → PermissionAction에 'approve' 있음 ✅ +src/hooks/usePermission.ts → canApprove 반환 ✅ +src/components/common/PermissionGuard.tsx → approve 매핑 있음 ✅ +src/components/approval/ApprovalBox/ → canApprove 미사용 ❌ (usePermission 미import) +``` + +#### 2. 내보내기(export) - ✅ 구현 완료 (2026-02-03) + +``` +src/lib/permissions/types.ts → PermissionAction에 'export' 있음 ✅ +src/hooks/usePermission.ts → canExport 반환 ✅ +src/lib/permissions/utils.ts → convertMatrixToPermissionMap/mergePermissionMaps에 'export' 포함 ✅ +src/contexts/PermissionContext.tsx → deny-all 기본값에 export: false 포함 ✅ +src/components/common/PermissionGuard.tsx → actionMap에 export 매핑 ✅ +src/components/templates/UniversalListPage/ → !canExport 시 엑셀 버튼 숨김 ✅ +커스텀 페이지 5개 → canExport && 조건 적용 ✅ +``` + +#### 3. 관리(manage) - 타입 시스템 미등록 + +``` +src/lib/permissions/types.ts → PermissionAction에 'manage' 없음 ❌ +src/hooks/usePermission.ts → canManage 없음 ❌ +``` + +> **설정 UI 코드** (`PermissionDetailClient.tsx`)에서 7개 전체 사용: +> `PERMISSION_TYPES = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']` +> **프론트엔드 구현 현황**: view ✅ | create ✅ | update ✅ | delete ✅ | approve ❌ | export ✅ | manage ❌ diff --git a/package.json b/package.json index f547f5f8..3b342d93 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --turbo", "build": "next build", "build:restart": "lsof -ti:3000 | xargs kill 2>/dev/null; next build && next start &", "start": "next start -H 0.0.0.0", diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index 969d839d..be6996b1 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -22,12 +22,9 @@ import { useClientList, Client } from "@/hooks/useClientList"; import { Building2, Plus, - Edit, - Trash2, Users, CheckCircle, XCircle, - Eye, Loader2, Bell, } from "lucide-react"; @@ -446,7 +443,6 @@ export default function CustomerAccountManagementPage() { { key: "representative", label: "대표자", className: "px-4" }, { key: "manager", label: "담당자", className: "px-4" }, { key: "phone", label: "전화번호", className: "px-4" }, - { key: "actions", label: "작업", className: "px-4" }, ]; // 테이블 행 렌더링 @@ -481,33 +477,6 @@ export default function CustomerAccountManagementPage() { {customer.representative} {customer.managerName || "-"} {customer.phone} - e.stopPropagation()}> - {isSelected && ( -
- - - -
- )} -
); }; @@ -551,36 +520,6 @@ export default function CustomerAccountManagementPage() { } - actions={ - isSelected ? ( -
- - -
- ) : undefined - } /> ); }; diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx index 3efb85b6..820a8f17 100644 --- a/src/components/accounting/BadDebtCollection/index.tsx +++ b/src/components/accounting/BadDebtCollection/index.tsx @@ -15,8 +15,7 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2'; import { useState, useMemo, useCallback, useTransition } from 'react'; import { useRouter } from 'next/navigation'; -import { AlertTriangle, Pencil, Trash2, Eye } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; @@ -55,7 +54,6 @@ const tableColumns = [ { key: 'managerName', label: '담당자', className: 'w-[100px]' }, { key: 'status', label: '상태', className: 'text-center w-[100px]' }, { key: 'setting', label: '설정', className: 'text-center w-[80px]' }, - { key: 'actions', label: '작업', className: 'text-center w-[120px]' }, ]; // ===== Props 타입 정의 ===== @@ -103,13 +101,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec [router] ); - const handleEdit = useCallback( - (item: BadDebtRecord) => { - router.push(`/ko/accounting/bad-debt-collection/${item.id}?mode=edit`); - }, - [router] - ); - // 설정 토글 핸들러 (API 호출) const handleSettingToggle = useCallback( (id: string, checked: boolean) => { @@ -406,29 +397,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec disabled={isPending} /> - {/* 작업 */} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
), @@ -454,25 +422,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec { label: '발생일', value: item.occurrenceDate }, { label: '담당자', value: item.assignedManager?.name || '-' }, ]} - actions={ - handlers.isSelected ? ( -
- - - -
- ) : undefined - } /> ), }), @@ -484,7 +433,6 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec sortOption, statsData, handleRowClick, - handleEdit, handleSettingToggle, isPending, ] diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx index 1cb766d7..42b70a2f 100644 --- a/src/components/accounting/BillManagement/BillManagementClient.tsx +++ b/src/components/accounting/BillManagement/BillManagementClient.tsx @@ -15,8 +15,6 @@ import { useRouter } from 'next/navigation'; import { FileText, Plus, - Pencil, - Trash2, Save, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -196,7 +194,6 @@ export function BillManagementClient({ { key: 'maturityDate', label: '만기일' }, { key: 'installmentCount', label: '차수', className: 'text-center' }, { key: 'status', label: '상태', className: 'text-center' }, - { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ], []); // ===== 테이블 행 렌더링 ===== @@ -232,31 +229,9 @@ export function BillManagementClient({ {getBillStatusLabel(item.billType, item.status)} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
); - }, [handleRowClick, handleDeleteClick, router]); + }, [handleRowClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( @@ -289,26 +264,10 @@ export function BillManagementClient({ } - actions={ - handlers.isSelected ? ( -
- - -
- ) : undefined - } onCardClick={() => handleRowClick(item)} /> ); - }, [handleRowClick, handleDeleteClick, router]); + }, [handleRowClick]); // ===== 거래처 목록 (필터용) ===== const vendorOptions = useMemo(() => { diff --git a/src/components/accounting/DailyReport/index.tsx b/src/components/accounting/DailyReport/index.tsx index 9caf83a8..466d8511 100644 --- a/src/components/accounting/DailyReport/index.tsx +++ b/src/components/accounting/DailyReport/index.tsx @@ -24,6 +24,7 @@ import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types'; import { getNoteReceivables, getDailyAccounts, getDailyReportSummary, exportDailyReportExcel } from './actions'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { usePermission } from '@/hooks/usePermission'; // ===== Props 인터페이스 ===== interface DailyReportProps { @@ -32,6 +33,7 @@ interface DailyReportProps { } export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts = [] }: DailyReportProps) { + const { canExport } = usePermission(); // ===== 상태 관리 ===== const [selectedDate, setSelectedDate] = useState(() => format(new Date(), 'yyyy-MM-dd')); const [noteReceivables, setNoteReceivables] = useState(initialNoteReceivables); @@ -217,10 +219,12 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts )} 새로고침 - + {canExport && ( + + )} diff --git a/src/components/accounting/DepositManagement/index.tsx b/src/components/accounting/DepositManagement/index.tsx index 19e9e133..5a2a8253 100644 --- a/src/components/accounting/DepositManagement/index.tsx +++ b/src/components/accounting/DepositManagement/index.tsx @@ -17,10 +17,8 @@ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Banknote, - Pencil, Plus, Save, - Trash2, RefreshCw, Search, } from 'lucide-react'; @@ -83,7 +81,6 @@ const tableColumns = [ { key: 'vendorName', label: '거래처' }, { key: 'note', label: '적요' }, { key: 'depositType', label: '입금유형', className: 'text-center' }, - { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ]; // ===== 컴포넌트 Props ===== @@ -150,10 +147,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan router.push(`/ko/accounting/deposits/${item.id}?mode=view`); }, [router]); - const handleEdit = useCallback((item: DepositRecord) => { - router.push(`/ko/accounting/deposits/${item.id}?mode=edit`); - }, [router]); - // 새로고침 핸들러 const handleRefresh = useCallback(async () => { setIsRefreshing(true); @@ -451,7 +444,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan - ), @@ -495,28 +487,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan {DEPOSIT_TYPE_LABELS[item.depositType]} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
); }, @@ -542,22 +512,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan { label: '입금액', value: `${(item.depositAmount ?? 0).toLocaleString()}원` }, { label: '거래처', value: item.vendorName || '-' }, ]} - actions={ - handlers.isSelected ? ( -
- - -
- ) : undefined - } /> ), }), @@ -575,7 +529,6 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan isRefreshing, searchQuery, handleRowClick, - handleEdit, handleRefresh, handleSaveAccountSubject, ] diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index 3ddcfc7f..ba23b2da 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -20,7 +20,6 @@ import { Calendar as CalendarIcon, FileText, Plus, - Pencil, Trash2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -573,7 +572,6 @@ export function ExpectedExpenseManagement({ { key: 'vendorName', label: '거래처' }, { key: 'bankAccount', label: '계좌' }, { key: 'approvalStatus', label: '전자결재', className: 'text-center' }, - { key: 'actions', label: '작업', className: 'w-[100px] text-center' }, ], []); // ===== 전자결재 상태 Badge 스타일 ===== @@ -603,7 +601,7 @@ export function ExpectedExpenseManagement({ if (item.rowType === 'monthHeader') { return ( - + {item.monthLabel} @@ -621,7 +619,7 @@ export function ExpectedExpenseManagement({ {item.subtotalAmount?.toLocaleString()} - + ); } @@ -637,7 +635,7 @@ export function ExpectedExpenseManagement({ {item.subtotalAmount?.toLocaleString()} - + ); } @@ -653,7 +651,7 @@ export function ExpectedExpenseManagement({ {item.subtotalAmount?.toLocaleString()} - + ); } @@ -669,7 +667,7 @@ export function ExpectedExpenseManagement({ {item.subtotalAmount?.toLocaleString()}원 - + ); } @@ -700,35 +698,9 @@ export function ExpectedExpenseManagement({ {getApprovalStatusBadge(item.approvalStatus)} - -
- - -
-
); - }, [selectedItems, toggleSelection, handleOpenEditDialog, handleDeleteClick]); + }, [selectedItems, toggleSelection]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index 16893442..748b45f5 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -19,9 +19,7 @@ import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { Receipt, - Pencil, Save, - Trash2, Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -76,7 +74,6 @@ const tableColumns = [ { key: 'totalAmount', label: '합계금액', className: 'text-right' }, { key: 'purchaseType', label: '매입유형', className: 'text-center' }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, - { key: 'actions', label: '작업', className: 'text-center w-[100px]' }, ]; export function PurchaseManagement() { @@ -206,10 +203,6 @@ export function PurchaseManagement() { router.push(`/ko/accounting/purchase/${item.id}?mode=view`); }, [router]); - const handleEdit = useCallback((item: PurchaseRecord) => { - router.push(`/ko/accounting/purchase/${item.id}?mode=edit`); - }, [router]); - // 토글 핸들러 const handleTaxInvoiceToggle = useCallback(async (itemId: string, checked: boolean) => { setPurchaseData(prev => prev.map(item => @@ -418,7 +411,6 @@ export function PurchaseManagement() { {tableTotals.totalAmount.toLocaleString()} - ), @@ -479,28 +471,6 @@ export function PurchaseManagement() { {item.taxInvoiceReceived && 수취} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
); }, @@ -527,22 +497,6 @@ export function PurchaseManagement() { { label: '공급가액', value: `${item.supplyAmount.toLocaleString()}원` }, { label: '합계금액', value: `${item.totalAmount.toLocaleString()}원` }, ]} - actions={ - handlers.isSelected ? ( -
- - -
- ) : undefined - } /> ), }), @@ -557,7 +511,6 @@ export function PurchaseManagement() { tableTotals, searchQuery, handleRowClick, - handleEdit, handleTaxInvoiceToggle, handleSaveAccountSubject, ] diff --git a/src/components/accounting/ReceivablesStatus/index.tsx b/src/components/accounting/ReceivablesStatus/index.tsx index 4dd3b07e..f78598f1 100644 --- a/src/components/accounting/ReceivablesStatus/index.tsx +++ b/src/components/accounting/ReceivablesStatus/index.tsx @@ -32,6 +32,7 @@ import { import { getReceivablesList, getReceivablesSummary, updateOverdueStatus, updateMemos, exportReceivablesExcel } from './actions'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { usePermission } from '@/hooks/usePermission'; // ===== Props 인터페이스 ===== interface ReceivablesStatusProps { @@ -63,6 +64,7 @@ const generateYearOptions = (): Array<{ value: number; label: string }> => { const categoryOrder: CategoryType[] = ['sales', 'deposit', 'bill', 'receivable']; export function ReceivablesStatus({ highlightVendorId, initialData, initialSummary }: ReceivablesStatusProps) { + const { canExport } = usePermission(); // ===== Refs ===== const highlightRowRef = useRef(null); @@ -403,14 +405,16 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma )} 새로고침 - + {canExport && ( + + )} - - - )} - ), @@ -525,22 +495,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem { label: '매출금액', value: `${item.totalAmount.toLocaleString()}원` }, { label: '미수금액', value: item.outstandingAmount > 0 ? `${item.outstandingAmount.toLocaleString()}원` : '-' }, ]} - actions={ - handlers.isSelected ? ( -
- - -
- ) : undefined - } /> ), }), @@ -555,7 +509,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem tableTotals, searchQuery, handleRowClick, - handleEdit, handleCreate, handleTaxInvoiceToggle, handleTransactionStatementToggle, diff --git a/src/components/accounting/VendorLedger/index.tsx b/src/components/accounting/VendorLedger/index.tsx index 4ee03d3e..a69edc44 100644 --- a/src/components/accounting/VendorLedger/index.tsx +++ b/src/components/accounting/VendorLedger/index.tsx @@ -29,6 +29,7 @@ import type { VendorLedgerItem, VendorLedgerSummary } from './types'; import { getVendorLedgerList, getVendorLedgerSummary, exportVendorLedgerExcel } from './actions'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { usePermission } from '@/hooks/usePermission'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ @@ -59,6 +60,7 @@ export function VendorLedger({ initialPagination, }: VendorLedgerProps) { const router = useRouter(); + const { canExport } = usePermission(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [data, setData] = useState(initialData); @@ -221,10 +223,12 @@ export function VendorLedger({ // 헤더 액션 (엑셀 다운로드) - 함수로 변환 headerActions: () => ( - + canExport ? ( + + ) : null ), // 테이블 푸터 (합계 행) diff --git a/src/components/accounting/VendorManagement/index.tsx b/src/components/accounting/VendorManagement/index.tsx index 7b25b72f..55bcae31 100644 --- a/src/components/accounting/VendorManagement/index.tsx +++ b/src/components/accounting/VendorManagement/index.tsx @@ -14,11 +14,7 @@ import { useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Building2, - Pencil, - Trash2, - Eye, } from 'lucide-react'; -import { Button } from '@/components/ui/button'; import { TableRow, TableCell } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; @@ -63,7 +59,6 @@ const tableColumns = [ { key: 'transactionGrade', label: '거래등급', className: 'text-center w-[100px]' }, { key: 'outstandingAmount', label: '미수금', className: 'text-right w-[120px]' }, { key: 'badDebtStatus', label: '악성채권', className: 'text-center w-[90px]' }, - { key: 'actions', label: '작업', className: 'text-center w-[150px]' }, ]; // ===== 컴포넌트 Props ===== @@ -94,13 +89,6 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement [router] ); - const handleEdit = useCallback( - (vendor: Vendor) => { - router.push(`/ko/accounting/vendors/${vendor.id}?mode=edit`); - }, - [router] - ); - // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -363,29 +351,6 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement )} - {/* 작업 */} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
), @@ -413,29 +378,10 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement }, { label: '결제일', value: `매입 ${vendor.purchasePaymentDay}일 / 매출 ${vendor.salesPaymentDay}일` }, ]} - actions={ - handlers.isSelected ? ( -
- - - -
- ) : undefined - } /> ), }), - [initialData, stats, handleRowClick, handleEdit, router] + [initialData, stats, handleRowClick, router] ); return ; diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index df9f0a6d..90d3c8cb 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -9,7 +9,6 @@ import { Clock, FileX, Files, - Edit, } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -75,6 +74,7 @@ import { APPROVAL_STATUS_COLORS, } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { usePermission } from '@/hooks/usePermission'; // ===== 통계 타입 ===== interface InboxSummary { @@ -87,6 +87,7 @@ interface InboxSummary { export function ApprovalBox() { const router = useRouter(); const [isPending, startTransition] = useTransition(); + const { canApprove } = usePermission(); // ===== 상태 관리 ===== const [activeTab, setActiveTab] = useState('all'); @@ -402,13 +403,6 @@ export function ApprovalBox() { } }, [selectedDocument, router]); - const handleEditClick = useCallback( - (item: ApprovalRecord) => { - router.push(`/ko/approval/draft/new?id=${item.id}&mode=edit`); - }, - [router] - ); - const handleModalCopy = useCallback(() => { toast.info('문서 복제 기능은 준비 중입니다.'); setIsModalOpen(false); @@ -508,7 +502,6 @@ export function ApprovalBox() { { key: 'approver', label: '결재자' }, { key: 'draftDate', label: '기안일시' }, { key: 'status', label: '상태', className: 'text-center' }, - { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ], tabs: tabs, @@ -591,7 +584,7 @@ export function ApprovalBox() { headerActions: ({ selectedItems, onClearSelection }) => ( <> - {selectedItems.size > 0 && ( + {selectedItems.size > 0 && canApprove && (
- )} - ); }, @@ -720,7 +701,7 @@ export function ApprovalBox() {
} actions={ - item.status === 'pending' && isSelected ? ( + item.status === 'pending' && isSelected && canApprove ? (
- -
- )} - ); }, @@ -698,26 +674,6 @@ export function DraftBox() { /> } - actions={ - isSelected && item.status === 'draft' ? ( -
- - -
- ) : undefined - } onClick={() => handleDocumentClick(item)} /> ); diff --git a/src/components/board/BoardManagement/index.tsx b/src/components/board/BoardManagement/index.tsx index 83158379..aa16062e 100644 --- a/src/components/board/BoardManagement/index.tsx +++ b/src/components/board/BoardManagement/index.tsx @@ -3,9 +3,8 @@ export { BoardDetailClientV2 } from './BoardDetailClientV2'; import { useRouter } from 'next/navigation'; -import { ClipboardList, Edit, Trash2, Plus } from 'lucide-react'; +import { ClipboardList, Plus } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; @@ -88,7 +87,6 @@ const createBoardManagementConfig = (router: ReturnType): Univ { key: 'status', label: '상태', className: 'min-w-[80px]' }, { key: 'authorName', label: '작성자', className: 'min-w-[100px]' }, { key: 'createdAt', label: '등록일시', className: 'min-w-[120px]' }, - { key: 'actions', label: '작업', className: 'w-[100px] text-right' }, ], // 탭 설정 (클라이언트 사이드 계산) @@ -160,18 +158,6 @@ const createBoardManagementConfig = (router: ReturnType): Univ {item.authorName} {formatDate(item.createdAt)} - e.stopPropagation()}> - {isSelected && ( -
- - -
- )} -
); }, @@ -209,36 +195,6 @@ const createBoardManagementConfig = (router: ReturnType): Univ } - actions={ - isSelected ? ( -
- - -
- ) : undefined - } /> ); }, diff --git a/src/components/common/ParentMenuRedirect.tsx b/src/components/common/ParentMenuRedirect.tsx index a3bd9a92..11fce179 100644 --- a/src/components/common/ParentMenuRedirect.tsx +++ b/src/components/common/ParentMenuRedirect.tsx @@ -41,7 +41,7 @@ export function ParentMenuRedirect({ parentPath, fallbackPath }: ParentMenuRedir const findParentMenu = (items: any[], targetPath: string): any | null => { for (const item of items) { // 경로가 일치하는지 확인 (locale prefix 제거 후 비교) - const itemPath = item.path?.replace(/^\/[a-z]{2}\//, '/') || ''; + const itemPath = item.path?.replace(/^\/(ko|en|ja)\//, '/') || ''; if (itemPath === targetPath || item.path === targetPath) { return item; } @@ -59,7 +59,7 @@ export function ParentMenuRedirect({ parentPath, fallbackPath }: ParentMenuRedir if (parentMenu && parentMenu.children && parentMenu.children.length > 0) { // 첫 번째 자식 메뉴의 경로로 리다이렉트 const firstChild = parentMenu.children[0]; - const firstChildPath = firstChild.path?.replace(/^\/[a-z]{2}\//, '/') || fallbackPath; + const firstChildPath = firstChild.path?.replace(/^\/(ko|en|ja)\//, '/') || fallbackPath; router.replace(firstChildPath); } else { // 자식이 없으면 fallback으로 이동 diff --git a/src/components/common/PermissionGuard.tsx b/src/components/common/PermissionGuard.tsx index 5286bd6e..c4351e39 100644 --- a/src/components/common/PermissionGuard.tsx +++ b/src/components/common/PermissionGuard.tsx @@ -40,6 +40,7 @@ export function PermissionGuard({ update: permission.canUpdate, delete: permission.canDelete, approve: permission.canApprove, + export: permission.canExport, }; if (!actionMap[action]) { diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx index e8804a9c..ef8e9670 100644 --- a/src/components/hr/AttendanceManagement/index.tsx +++ b/src/components/hr/AttendanceManagement/index.tsx @@ -9,7 +9,6 @@ import { Calendar, Plus, FileText, - Edit, Search, } from 'lucide-react'; import type { ExcelColumn } from '@/lib/utils/excel-download'; @@ -277,7 +276,6 @@ export function AttendanceManagement() { { key: 'breakTime', label: '휴게', className: 'min-w-[60px]' }, { key: 'overtime', label: '연장근무', className: 'min-w-[80px]' }, { key: 'reason', label: '사유', className: 'min-w-[80px]' }, - { key: 'actions', label: '작업', className: 'w-[60px] text-center' }, ], []); // 체크박스 토글 @@ -631,18 +629,6 @@ export function AttendanceManagement() { ) : '-'} - - {isSelected && ( - - )} - ); }, @@ -687,21 +673,6 @@ export function AttendanceManagement() { )} } - actions={ - isSelected ? ( -
- -
- ) : undefined - } /> ); }, diff --git a/src/components/hr/CardManagement/index.tsx b/src/components/hr/CardManagement/index.tsx index be31e7cc..8fac3db3 100644 --- a/src/components/hr/CardManagement/index.tsx +++ b/src/components/hr/CardManagement/index.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { CreditCard, Edit, Trash2, Plus, Search, RefreshCw } from 'lucide-react'; +import { CreditCard, Plus, Search, RefreshCw } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -126,7 +126,6 @@ export function CardManagement({ initialData }: CardManagementProps) { { key: 'department', label: '부서', className: 'min-w-[100px]' }, { key: 'userName', label: '사용자', className: 'min-w-[100px]' }, { key: 'position', label: '직책', className: 'min-w-[100px]' }, - { key: 'actions', label: '작업', className: 'w-[100px] text-right' }, ], []); // 체크박스 토글 @@ -188,15 +187,6 @@ export function CardManagement({ initialData }: CardManagementProps) { router.push(`/ko/hr/card-management/${row.id}?mode=view`); }, [router]); - const handleEdit = useCallback((id: string) => { - router.push(`/ko/hr/card-management/${id}?mode=edit`); - }, [router]); - - const openDeleteDialog = useCallback((card: Card) => { - setCardToDelete(card); - setDeleteDialogOpen(true); - }, []); - // ===== UniversalListPage 설정 ===== const cardManagementConfig: UniversalListConfig = useMemo(() => ({ title: '카드관리', @@ -286,28 +276,6 @@ export function CardManagement({ initialData }: CardManagementProps) { {item.user?.departmentName || '-'} {item.user?.employeeName || '-'} {item.user?.positionName || '-'} - e.stopPropagation()}> - {isSelected && ( -
- - -
- )} -
); }, @@ -346,30 +314,6 @@ export function CardManagement({ initialData }: CardManagementProps) { } - actions={ - isSelected ? ( -
- - -
- ) : undefined - } /> ); }, @@ -398,8 +342,6 @@ export function CardManagement({ initialData }: CardManagementProps) { activeTab, handleAddCard, handleRowClick, - handleEdit, - openDeleteDialog, deleteDialogOpen, cardToDelete, handleDeleteCard, diff --git a/src/components/hr/SalaryManagement/index.tsx b/src/components/hr/SalaryManagement/index.tsx index 8f2c077f..1eb312e7 100644 --- a/src/components/hr/SalaryManagement/index.tsx +++ b/src/components/hr/SalaryManagement/index.tsx @@ -28,6 +28,7 @@ import { type FilterFieldConfig, type FilterValues, } from '@/components/templates/UniversalListPage'; +import { usePermission } from '@/hooks/usePermission'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { SalaryDetailDialog } from './SalaryDetailDialog'; import { @@ -52,6 +53,7 @@ import { import { isNextRedirectError } from '@/lib/utils/redirect-error'; export function SalaryManagement() { + const { canExport } = usePermission(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [sortOption, setSortOption] = useState('rank'); @@ -427,10 +429,12 @@ export function SalaryManagement() { )} - + {canExport && ( + + )} ), diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index 2adadd59..2798aa59 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -13,12 +13,11 @@ import { useRouter } from 'next/navigation'; import type { ItemMaster } from '@/types/item'; import { ITEM_TYPE_LABELS } from '@/types/item'; import { useCommonCodes } from '@/hooks/useCommonCodes'; -import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; -import { Search, Plus, Edit, Trash2, Package, FileDown, Upload } from 'lucide-react'; +import { Plus, Package, FileDown, Upload } from 'lucide-react'; import { downloadExcelTemplate, parseExcelFile, type ExcelColumn, type TemplateColumn } from '@/lib/utils/excel-download'; import { useItemList } from '@/hooks/useItemList'; import { handleApiError } from '@/lib/api/error-handler'; @@ -418,7 +417,6 @@ export default function ItemListClient() { { key: 'specification', label: '규격', className: 'min-w-[100px]' }, { key: 'unit', label: '단위', className: 'min-w-[60px]' }, { key: 'isActive', label: '품목상태', className: 'min-w-[80px]' }, - { key: 'actions', label: '작업', className: 'w-[120px] text-right' }, ], // 클라이언트 사이드 필터링 (외부 useItemList 훅 사용) @@ -485,8 +483,8 @@ export default function ItemListClient() { handlers: SelectionHandlers & RowClickHandlers ) => { return ( - - + handleView(item.itemCode, item.itemType, item.id)}> + e.stopPropagation()}> - -
- - - -
-
); }, @@ -599,39 +569,6 @@ export default function ItemListClient() { )} } - actions={ - handlers.isSelected ? ( -
- - - -
- ) : undefined - } /> ); }, diff --git a/src/components/pricing/PricingListClient.tsx b/src/components/pricing/PricingListClient.tsx index 29bcd340..8301249f 100644 --- a/src/components/pricing/PricingListClient.tsx +++ b/src/components/pricing/PricingListClient.tsx @@ -13,9 +13,6 @@ import { Package, AlertCircle, CheckCircle2, - Plus, - Edit, - History, RefreshCw, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -213,7 +210,6 @@ export function PricingListClient({ { key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true }, { key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true }, { key: 'status', label: '상태', className: 'min-w-[80px]' }, - { key: 'actions', label: '작업', className: 'w-[120px] text-right' }, ]; // 테이블 행 렌더링 @@ -284,41 +280,6 @@ export function PricingListClient({ : '-'}
{renderStatusBadge(item)} - -
- {item.status === 'not_registered' ? ( - - ) : ( - <> - - {item.currentRevision > 0 && ( - - )} - - )} -
-
); }; @@ -363,46 +324,6 @@ export function PricingListClient({ /> } - actions={ - isSelected ? ( -
- {item.status === 'not_registered' ? ( - - ) : ( - <> - - {item.currentRevision > 0 && ( - - )} - - )} -
- ) : undefined - } /> ); }; diff --git a/src/components/settings/PopupManagement/PopupList.tsx b/src/components/settings/PopupManagement/PopupList.tsx index 76814c5b..bd7d6414 100644 --- a/src/components/settings/PopupManagement/PopupList.tsx +++ b/src/components/settings/PopupManagement/PopupList.tsx @@ -11,8 +11,7 @@ import { useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { Megaphone, Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { Megaphone } from 'lucide-react'; import { Checkbox } from '@/components/ui/checkbox'; import { TableCell, TableRow } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; @@ -41,13 +40,6 @@ export function PopupList({ initialData }: PopupListProps) { [router] ); - const handleEdit = useCallback( - (id: string) => { - router.push(`/ko/settings/popup-management/${id}?mode=edit`); - }, - [router] - ); - const handleCreate = useCallback(() => { router.push('/ko/settings/popup-management?mode=new'); }, [router]); @@ -85,7 +77,6 @@ export function PopupList({ initialData }: PopupListProps) { { key: 'author', label: '작성자', className: 'w-[100px] text-center' }, { key: 'createdAt', label: '등록일', className: 'w-[110px] text-center' }, { key: 'period', label: '기간', className: 'w-[180px] text-center' }, - { key: 'actions', label: '작업', className: 'w-[180px] text-center' }, ], // 클라이언트 사이드 필터링 @@ -148,29 +139,6 @@ export function PopupList({ initialData }: PopupListProps) { {item.startDate}~{item.endDate} - e.stopPropagation()}> - {handlers.isSelected && ( -
- - -
- )} -
); }, @@ -213,21 +181,6 @@ export function PopupList({ initialData }: PopupListProps) {
기간: {item.startDate} ~ {item.endDate}
- {handlers.isSelected && ( -
e.stopPropagation()}> - - -
- )} @@ -235,7 +188,7 @@ export function PopupList({ initialData }: PopupListProps) { ); }, }), - [handleRowClick, handleEdit, handleCreate] + [handleRowClick, handleCreate] ); return ; diff --git a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx index 1b619719..8db92cde 100644 --- a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx +++ b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx @@ -10,6 +10,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; +import { usePermission } from '@/hooks/usePermission'; import { cancelSubscription, requestDataExport } from './actions'; import type { SubscriptionInfo } from './types'; import { PLAN_LABELS, SUBSCRIPTION_STATUS_LABELS } from './types'; @@ -36,6 +37,7 @@ const formatCurrency = (amount: number): string => { }; export function SubscriptionClient({ initialData }: SubscriptionClientProps) { + const { canExport } = usePermission(); const [subscription, setSubscription] = useState(initialData); const [showCancelDialog, setShowCancelDialog] = useState(false); const [isExporting, setIsExporting] = useState(false); @@ -101,14 +103,16 @@ export function SubscriptionClient({ initialData }: SubscriptionClientProps) { icon={CreditCard} actions={
- + {canExport && ( + + )}