1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
# 외부 시스템(NONSAP) 연동 권한 관리 구현 계획 (v2)
본 문서는 외부 오라클 시스템(NONSAP)의 뷰 테이블을 연동하여 프로젝트의 권한 관리를 처리하기 위한 구현 계획입니다.
## 1. 개요
- **목표**: 외부 시스템에서 관리되는 화면 및 권한 정보를 조회하여, 웹 애플리케이션의 페이지 접근 제어 및 기능별 권한 제어를 수행합니다.
- **환경**: Node.js Runtime (Not Edge/FaaS).
- **DB 연결**: `lib/oracle-db/db.ts`의 `oracleKnex`를 사용하여 오라클 DB에 접속합니다.
## 2. 데이터 구조 및 관계
제공된 4개의 뷰 테이블을 사용하여 권한을 판단합니다.
### 2.1 테이블 정의
1. **`CMCVW_SCR_EVCP` (화면 목록)**
* **역할**: 시스템의 모든 화면과 해당 URL을 정의합니다.
* **주요 컬럼**: `SCR_ID` (PK), `SCR_URL`, `SCRT_CHK_YN`, `DEL_YN`
2. **`CMCVW_SCR_AUTH_EVCP` (화면 권한)**
* **역할**: 각 화면(`SCR_ID`)에 대해 접근 가능한 대상(`ACSR_ID`)과 권한 상세(`AUTH_CD`)를 정의합니다.
* **주요 컬럼**: `SCR_ID` (FK), `ACSR_GB_CD` (접근자 구분), `ACSR_ID`, `AUTH_CD_*` (권한 플래그)
* **ACSR_GB_CD 유형**:
* `'U'`: 사용자 (User)
* `'R'`: 역할 (Role)
* `'D'`: 부서 (Department - 하위 미포함)
* `'E'`: 부서 (Department - 하위 포함)
3. **`CMCVW_ROLE_EVCP` (역할 목록)**
* **역할**: 시스템에 존재하는 역할(Role)을 정의합니다.
* **주요 컬럼**: `ROLE_ID` (PK), `ROLE_NM`
4. **`CMCVW_ROLE_REL_EVCP` (역할-사용자 매핑)**
* **역할**: 각 역할(`ROLE_ID`)에 소속된 사용자(`EMPNO`)를 정의합니다.
* **주요 컬럼**: `ROLE_ID` (FK), `EMPNO`
## 3. 구현 상세
### 3.1 데이터 페칭 및 캐싱 전략
* **DB Client**: `lib/oracle-db/db.ts`의 `oracleKnex` 사용.
* **캐싱**: `unstable_cache` (Next.js)를 사용하여 DB 부하를 줄이고 응답 속도를 확보합니다. (TTL: 60초)
### 3.2 권한 검증 함수 설계
URL 인자를 선택적으로 받을 수 있도록 설계하여, 호출 편의성을 높입니다.
```typescript
// lib/nonsap/auth-service.ts
export type AuthAction = 'SEARCH' | 'ADD' | 'DEL' | 'SAVE' | 'PRINT' | 'DOWN' | 'UP' | 'APPROVAL' | 'PREV' | 'NEXT';
/**
* 위 Action의 의미는 아래와 같다.
* - 조회
* - 추가
* - 삭제
* - 저장
* - 출력
* - 파일받기
* - 파일올림
* - 결재상신
* - PrevPage
* - NextPage
*/
interface AuthCheckResult {
authorized: boolean;
message?: string;
}
/**
* NONSAP 권한 검증 함수
* @param empNo 사용자 사번
* @param requiredActions 필요한 권한 목록 (예: ['SEARCH'])
* @param url (Optional) 검증할 URL. 생략 시 현재 요청의 URL을 자동으로 감지 시도.
*/
export async function verifyNonsapPermission(
empNo: string,
requiredActions: AuthAction[],
url?: string,
options?: { logic?: 'AND' | 'OR' } // default: 'AND'
): Promise<AuthCheckResult> {
// 1. URL 결정
const targetUrl = url || await getCurrentUrlFromHeaders();
if (!targetUrl) {
throw new Error("URL을 확인할 수 없습니다. URL 인자를 명시해주세요.");
}
// 2. 권한 검증 로직 수행 (oracleKnex 사용)
// ...
}
// Helper: 헤더에서 URL 추출 (Middleware 사전 작업 필요)
import { headers } from 'next/headers';
async function getCurrentUrlFromHeaders(): Promise<string | null> {
const headerList = headers();
// Middleware에서 'x-pathname' 헤더를 심어주어야 함
return headerList.get('x-pathname') || null;
}
```
### 3.3 권한 검증 로직 (Core Logic)
1. **화면 식별**:
* `CMCVW_SCR_EVCP`에서 `SCR_URL`이 `targetUrl`과 일치하는 행 조회 (`oracleKnex` 사용).
* `DEL_YN` == 'N', `SCRT_CHK_YN` == 'Y' 확인.
* 일치 항목 없으면: 관리되지 않는 페이지로 간주 -> **Pass**.
2. **권한 확인 (ACSR_GB_CD 유형별 처리)**:
* `CMCVW_SCR_AUTH_EVCP` 조회 (`SCR_ID` 기준). 조회된 각 권한 레코드(`authRecord`)에 대해 다음 로직 수행:
* **Case U (User)**: `ACSR_GB_CD == 'U'`
* `authRecord.ACSR_ID`가 대상 사용자의 `nonsap_user_id` (Postgres `users` 테이블)와 일치하는지 확인.
* **Case R (Role)**: `ACSR_GB_CD == 'R'`
* `authRecord.ACSR_ID` (Role ID)가 `CMCVW_ROLE_REL_EVCP` 테이블에서 해당 사용자의 사번(`EMPNO`)과 매핑되어 있는지 확인.
* **Case D (Department - Exact)**: `ACSR_GB_CD == 'D'`
* `authRecord.ACSR_ID` (Dept Code)가 대상 사용자의 `deptCode` (Postgres `users` 테이블)와 정확히 일치하는지 확인.
* **Case E (Department - Recursive)**: `ACSR_GB_CD == 'E'`
* 사용자의 부서(`user.deptCode`)부터 시작하여 상위 부서로 거슬러 올라가며 확인.
* **데이터 소스**: `db/schema/knox/organization.ts`의 `organization` 테이블 (Knox 조직도).
* **Logic**:
1. `currentDeptCode = user.deptCode`
2. Loop:
* `currentDeptCode == authRecord.ACSR_ID` 이면 **Match (권한 있음)**.
* `organization` 테이블에서 `departmentCode == currentDeptCode`인 행 조회.
* 상위 부서 코드(`uprDepartmentCode`) 확인.
* **종료 조건 (권한 없음)**:
* `uprDepartmentCode` is NULL
* `uprDepartmentCode == currentDeptCode` (자기 참조)
* `uprDepartmentCode == 'TOP'`
* `currentDeptCode = uprDepartmentCode` 로 갱신하고 반복.
3. **Action 체크**:
* **Step 3-1. 권한 통합 (Merging)**:
* 위 단계에서 **Match**된 모든 권한 레코드들의 `AUTH_CD_*` 컬럼 값을 **OR 연산(Union)**하여 사용자의 '최종 보유 권한'을 산출합니다.
* 예: Role A(Search=Y, Save=N) + Role B(Search=N, Save=Y) => 최종(Search=Y, Save=Y).
* **Step 3-2. 요구사항 검증 (Checking)**:
* `requiredActions` 목록과 '최종 보유 권한'을 비교합니다.
* **AND 모드 (Default)**: `requiredActions`의 **모든** 항목에 대해 권한이 있어야 통과 (`every`). (보안상 안전)
* **OR 모드**: `requiredActions` 중 **하나라도** 권한이 있으면 통과 (`some`). (메뉴 노출 등 UI 제어용)
## 4. 단계별 실행 계획
### Step 1: Middleware 설정 (URL 감지용)
* `middleware.ts`에서 요청 URL의 pathname을 `x-pathname` 헤더에 담아 넘겨주는 로직 추가.
* 이 헤더는 Server Component/Action에서 `headers()`를 통해 접근 가능해짐.
### Step 2: 데이터 접근 레이어 구현 (`lib/nonsap/db.ts`)
* `oracleKnex`를 사용하여 4개 테이블을 조회하는 함수 구현.
* `unstable_cache` 적용.
### Step 3: 권한 서비스 구현 (`lib/nonsap/auth-service.ts`)
* `verifyNonsapPermission` 함수 구현.
* URL 자동 감지 로직 포함.
* 부서 계층 구조 조회 로직(Case E) 구현 (Knox Organization 테이블 연동).
### Step 4: 적용 (Layout/Page/Action)
* **페이지 접근 제어**: `app/[lng]/layout.tsx` 또는 각 Page 컴포넌트 상단에서 `verifyNonsapPermission(..., ['SEARCH'])` 호출.
* **기능 제어**: Server Action 내부에서 `verifyNonsapPermission(..., ['SAVE'])` 등 호출.
## 5. 이슈 및 고려사항
1. **URL 매핑 정규화**:
* DB의 `SCR_URL`은 `/partners/dashboard` 형태이나, 실제 요청은 `/[lng]/partners/dashboard` 일 수 있음.
* Middleware 또는 Helper 함수에서 `lng` 파트(예: `/en`, `/ko`)를 제거하고 매칭하는 로직 필요.
2. **Oracle DB 의존성**:
* `oracledb` 라이브러리는 Node.js 환경에서만 동작하므로, 권한 체크 로직은 반드시 **Server Component** 또는 **Server Action** 레벨에서 수행되어야 함. (Middleware에서 직접 DB 조회 불가)
3. **부서 계층 정보**:
* Knox Organization 테이블(`db/schema/knox/organization.ts`)을 사용하여 상위 부서 정보를 조회합니다.
## 구현 한 부분
evcp 경로의 layout.tsx에서 SEARCH 권한 있는지 authority check 함
없으면 toast로 없다고 경고만 해줌 (nonsap측에 모든 설정 다 한건 아닐 테니)
|