summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.development7
-rw-r--r--.env.production9
-rw-r--r--app/[lng]/page.tsx9
-rw-r--r--db/schema/rfqLast.ts2
-rw-r--r--lib/nonsap/README.md109
-rw-r--r--lib/nonsap/action-utils.ts85
6 files changed, 212 insertions, 9 deletions
diff --git a/.env.development b/.env.development
index 29063c3c..30a88aa9 100644
--- a/.env.development
+++ b/.env.development
@@ -194,5 +194,8 @@ REVALIDATION_SECRET="biwjeofijosdkfjoiwejfksdjf1"
# 오픈 전, 벤더에게 특정 메뉴 보이지 않기, 운영 배포시 true로 설정할 것 (나준규프로 요청사항)
NEXT_PUBLIC_HIDE_PARTNERS_MENU_BEFORE_OPEN=false
-# DOLCE Local Uplaod Directory
-DOLCE_LOCAL_UPLOAD_ABSOLUTE_DIRECTORY="/evcp/data/dolce" \ No newline at end of file
+# DOLCE Local Uplaod Directory (only for v3. currently not used)
+DOLCE_LOCAL_UPLOAD_ABSOLUTE_DIRECTORY="/evcp/data/dolce"
+
+# 서버액션 고차컴포넌트가 실제로 인가에 따라 실행을 막을지를 결정하는 환경변수 (권한 셋업이 되기 전까지는 false)
+CHECK_NONSAP_AUTH_HOC=false \ No newline at end of file
diff --git a/.env.production b/.env.production
index 6e29b4dc..a8121cf6 100644
--- a/.env.production
+++ b/.env.production
@@ -1,4 +1,4 @@
-# === DB 설정 ===
+n# === DB 설정 ===
DATABASE_URL=postgresql://dts:dujinDTS2@localhost:5432/evcp
DB_POOL_MAX=4 # 풀 개수로, 일반적으로 (코어개수 * 1~2) 개 설정, 기본값=4
@@ -196,5 +196,8 @@ REVALIDATION_SECRET="biwjeofijosdkfjoiwejfksdjf1"
# 오픈 전, 벤더에게 특정 메뉴 보이지 않기, 운영 배포시 true로 설정할 것 (나준규프로 요청사항)
NEXT_PUBLIC_HIDE_PARTNERS_MENU_BEFORE_OPEN=false
-# DOLCE Local Uplaod Directory
-DOLCE_LOCAL_UPLOAD_ABSOLUTE_DIRECTORY="/evcp/data/dolce" \ No newline at end of file
+# DOLCE Local Uplaod Directory (only for v3. currently not used)
+DOLCE_LOCAL_UPLOAD_ABSOLUTE_DIRECTORY="/evcp/data/dolce"
+
+# 서버액션 고차컴포넌트가 실제로 인가에 따라 실행을 막을지를 결정하는 환경변수 (권한 셋업이 되기 전까지는 false)
+CHECK_NONSAP_AUTH_HOC=false \ No newline at end of file
diff --git a/app/[lng]/page.tsx b/app/[lng]/page.tsx
index d0018f40..3b7e5dc9 100644
--- a/app/[lng]/page.tsx
+++ b/app/[lng]/page.tsx
@@ -13,7 +13,8 @@ export default function LandingPage() {
description: '기술 영업 단계에서의 RFQ를 관리할 수 있는 통합 플랫폼',
icon: Briefcase,
color: 'from-green-500 to-emerald-500',
- href: '/sales',
+ // href: '/sales',
+ href: '/evcp',
features: ['벤더 관리', '기술 영업 RFQ'],
},
{
@@ -22,7 +23,8 @@ export default function LandingPage() {
description: '협력 업체에서 부터 마지막 발주까지 원스톱 구매 솔루션',
icon: Package,
color: 'from-blue-500 to-indigo-500',
- href: '/procurement',
+ // href: '/procurement',
+ href: '/evcp',
features: ['협력 업체 관리', '구매 관리'],
},
{
@@ -31,7 +33,8 @@ export default function LandingPage() {
description: '벤더가 플랫폼을 통해 데이터와 문서를 제출할 수 있게 하고 TBE를 처리할 수 있는 플랫폼',
icon: Settings,
color: 'from-purple-500 to-violet-500',
- href: '/engineering',
+ // href: '/engineering',
+ href: '/evcp',
features: ['설계 기준정보 관리', 'TBE'],
}
];
diff --git a/db/schema/rfqLast.ts b/db/schema/rfqLast.ts
index 325942f4..98348439 100644
--- a/db/schema/rfqLast.ts
+++ b/db/schema/rfqLast.ts
@@ -264,7 +264,7 @@ export const rfqPrItems = pgTable(
size: varchar("size", { length: 255 }),
deliveryDate: date("delivery_date", { mode: "date" })
.$type<Date>(),
- quantity: numeric("quantity", { precision: 12, scale: 2 })
+ quantity: numeric("quantity", { precision: 12, scale: 3 })
.$type<number>()
.default(1),
uom: varchar("uom", { length: 50 }), // 단위
diff --git a/lib/nonsap/README.md b/lib/nonsap/README.md
new file mode 100644
index 00000000..4b595e00
--- /dev/null
+++ b/lib/nonsap/README.md
@@ -0,0 +1,109 @@
+# NONSAP Server Action 권한 관리 가이드
+
+이 디렉토리는 NONSAP 시스템의 권한 인증 및 Server Action 보안을 위한 유틸리티를 포함하고 있습니다.
+
+## 핵심 유틸리티: `withNonsapAuth`
+
+`withNonsapAuth`는 Server Action을 생성할 때 사용하는 고차 함수(Higher-Order Function)입니다.
+반복적인 세션 체크, 권한 검증, 에러 핸들링 코드를 제거하고 비즈니스 로직에 집중할 수 있게 해줍니다.
+
+### 파일 위치
+- `lib/nonsap/action-utils.ts`
+
+### 주요 기능
+1. **세션 검증**: 사용자가 로그인되어 있는지 확인합니다.
+2. **권한 검증**: `verifyNonsapPermission`을 사용하여 현재 페이지(URL)에서 해당 동작(Action)을 수행할 권한이 있는지 확인합니다.
+3. **User ID 주입**: 검증된 사용자 ID를 액션 함수의 두 번째 인자로 전달합니다.
+4. **에러 핸들링**: 예기치 않은 에러를 포착하여 일관된 포맷으로 반환합니다.
+
+---
+
+## 사용 방법
+
+### 1. Server Action 정의하기
+
+기존의 Server Action 함수를 `withNonsapAuth`로 감싸서 내보냅니다.
+
+```typescript
+// lib/example/service.ts
+'use server'
+
+import { withNonsapAuth } from "@/lib/nonsap/action-utils";
+
+// Input 타입 정의
+interface CreateItemInput {
+ name: string;
+ price: number;
+}
+
+// withNonsapAuth 사용
+// 첫 번째 인자: 필요한 권한 목록 (예: ['ADD'], ['SAVE'], ['DEL'] 등)
+// 두 번째 인자: 실제 로직 함수 (input, userId) => Promise<Result>
+// 세 번째 인자 (Optional): 옵션 객체 { url?: string, logic?: 'AND' | 'OR' }
+export const createItemAction = withNonsapAuth<CreateItemInput, any>(
+ ['ADD'], // 'ADD' 권한 필요
+ async (input, userId) => {
+
+ // ... 비즈니스 로직 ...
+
+ return {
+ success: true,
+ data: { id: 1, ...input },
+ message: "아이템이 생성되었습니다."
+ };
+ },
+ // 옵션 예시: 특정 관리 화면의 권한을 강제로 적용
+ {
+ url: '/evcp/master-data/items',
+ logic: 'AND'
+ }
+);
+```
+
+### 2. 클라이언트에서 사용하기
+
+클라이언트 컴포넌트에서는 일반적인 Server Action처럼 호출하면 됩니다.
+
+```tsx
+// app/example/page.tsx
+'use client'
+
+import { createItemAction } from "@/lib/example/service";
+
+export default function Page() {
+ const handleSubmit = async () => {
+ const result = await createItemAction({ name: "Test Item", price: 1000 });
+
+ if (result.success) {
+ alert(result.message);
+ } else {
+ // 권한이 없거나 에러가 발생한 경우
+ alert(result.error);
+ }
+ };
+
+ return <button onClick={handleSubmit}>생성</button>;
+}
+```
+
+---
+
+## 권한 타입 (AuthAction)
+
+`lib/nonsap/auth-service.ts`에 정의된 다음 권한 타입들을 사용할 수 있습니다.
+
+- `'SEARCH'`: 조회
+- `'ADD'`: 추가/신규작성
+- `'DEL'`: 삭제
+- `'SAVE'`: 저장/수정
+- `'PRINT'`: 출력
+- `'DOWN'`: 다운로드
+- `'UP'`: 업로드
+- `'APPROVAL'`: 승인
+- `'PREV'`: 이전
+- `'NEXT'`: 다음
+
+## 주의사항
+
+- **URL 감지**: 권한 체크는 현재 요청의 헤더(`x-pathname`)를 기반으로 URL을 감지하여 수행됩니다. 미들웨어에서 `x-pathname` 헤더를 설정해주어야 정상 동작합니다. (작업해뒀습니다)
+- **Input 타입**: `withNonsapAuth`의 제네릭 타입으로 Input 타입을 명시해주면 타입 안전성을 확보할 수 있습니다.
diff --git a/lib/nonsap/action-utils.ts b/lib/nonsap/action-utils.ts
new file mode 100644
index 00000000..731df1f5
--- /dev/null
+++ b/lib/nonsap/action-utils.ts
@@ -0,0 +1,85 @@
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { verifyNonsapPermission, AuthAction } from "./auth-service";
+import 'server-only'; // 클라이언트 번들에 포함되는 걸 방지하기 위함
+
+export type ActionResponse<T> = {
+ success: boolean;
+ data?: T;
+ error?: string;
+ message?: string;
+};
+
+/**
+ * 권한 체크와 세션 확인을 수행하는 고차 함수 (Action Factory)
+ *
+ * 이 유틸리티는 Server Action을 감싸서 다음 기능을 자동으로 수행합니다:
+ * 1. 사용자 세션 확인 (로그인 여부)
+ * 2. NONSAP 권한 검증 (verifyNonsapPermission 사용)
+ * 3. 에러 핸들링 (try-catch)
+ *
+ * @param requiredActions - 해당 액션을 수행하기 위해 필요한 권한 목록 (예: ['ADD'], ['SEARCH', 'DOWN'])
+ * @param action - 실제 비즈니스 로직을 수행하는 함수.
+ * 첫 번째 인자로 input을, 두 번째 인자로 검증된 userId를 받습니다.
+ * @param options - (Optional) 추가 옵션
+ * @param options.url - 권한 체크를 위한 명시적 URL (생략 시 x-pathname 헤더 자동 감지).
+ * 특정 화면의 권한을 강제로 적용하고 싶을 때 사용합니다.
+ * @param options.logic - 여러 권한이 필요할 때의 논리 연산 ('AND' | 'OR'). 기본값은 'AND'.
+ * @returns 권한 검증 로직이 포함된 새로운 비동기 함수
+ */
+export function withNonsapAuth<TInput, TOutput>(
+ requiredActions: AuthAction[],
+ action: (input: TInput, userId: number) => Promise<ActionResponse<TOutput>>,
+ options?: {
+ url?: string;
+ logic?: 'AND' | 'OR';
+ }
+) {
+ return async (input: TInput): Promise<ActionResponse<TOutput>> => {
+ try {
+ // 1. 세션 확인
+ const session = await getServerSession(authOptions);
+ const userId = Number(session?.user?.id);
+
+ if (!userId || isNaN(userId)) {
+ return {
+ success: false,
+ error: "로그인이 필요하거나 세션이 만료되었습니다."
+ };
+ }
+
+ // 2. 권한 검증
+ // options.url이 있으면 그것을 사용하고, 없으면 headers()를 통해 자동 감지
+ const authResult = await verifyNonsapPermission(
+ userId,
+ requiredActions,
+ options?.url,
+ { logic: options?.logic }
+ );
+
+ if (!authResult.authorized) {
+ // 환경변수 체크: CHECK_NONSAP_AUTH_HOC가 'true'일 때만 권한 체크를 강제함
+ // 그 외의 경우(설정되지 않았거나 false 등)에는 경고만 남기고 실행 허용
+ if (process.env.CHECK_NONSAP_AUTH_HOC === 'true') {
+ return {
+ success: false,
+ error: authResult.message || "이 작업을 수행할 권한이 없습니다."
+ };
+ } else {
+ console.warn(`[withNonsapAuth] 권한 체크 실패했으나 실행 허용됨 (CHECK_NONSAP_AUTH_HOC != true). User: ${userId}, Actions: ${requiredActions.join(',')}`);
+ }
+ }
+
+ // 3. 실제 액션 실행
+ // userId를 주입하여 액션 내부에서 다시 세션을 조회할 필요가 없도록 합니다.
+ return await action(input, userId);
+
+ } catch (error) {
+ console.error("[withNonsapAuth] Server Action Error:", error);
+ return {
+ success: false,
+ error: "서버 내부 오류가 발생했습니다."
+ };
+ }
+ };
+}