summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/admin/approval-test/page.tsx8
-rw-r--r--app/[lng]/evcp/(evcp)/approval/line/page.tsx68
-rw-r--r--app/[lng]/evcp/(evcp)/approval/template/[id]/config.ts33
-rw-r--r--app/[lng]/evcp/(evcp)/approval/template/[id]/page.tsx58
-rw-r--r--app/[lng]/evcp/(evcp)/approval/template/page.tsx68
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts42
6 files changed, 272 insertions, 5 deletions
diff --git a/app/[lng]/admin/approval-test/page.tsx b/app/[lng]/admin/approval-test/page.tsx
index ab5654f3..439c7ba8 100644
--- a/app/[lng]/admin/approval-test/page.tsx
+++ b/app/[lng]/admin/approval-test/page.tsx
@@ -1,12 +1,17 @@
import { Metadata } from 'next';
import ApprovalManager from '@/components/knox/approval/ApprovalManager';
+import { findUserByEmail } from '@/lib/users/service';
+import { getServerSession } from 'next-auth/next';
export const metadata: Metadata = {
title: 'Knox 결재 시스템 | Admin',
description: 'Knox API를 사용한 결재 시스템',
};
-export default function ApprovalTestPage() {
+export default async function ApprovalTestPage() {
+ const session = await getServerSession();
+ const currentUser = await findUserByEmail(session?.user?.email ?? '');
+
return (
<div className="container mx-auto py-8">
<div className="space-y-6">
@@ -21,6 +26,7 @@ export default function ApprovalTestPage() {
{/* 결재 관리자 컴포넌트 */}
<ApprovalManager
defaultTab="submit"
+ currentUser={currentUser}
/>
</div>
</div>
diff --git a/app/[lng]/evcp/(evcp)/approval/line/page.tsx b/app/[lng]/evcp/(evcp)/approval/line/page.tsx
new file mode 100644
index 00000000..435d1071
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/approval/line/page.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react';
+import { type Metadata } from 'next';
+import { Shell } from '@/components/shell';
+import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton';
+import { type SearchParams } from '@/types/table';
+import { getValidFilters } from '@/lib/data-table';
+
+import { getApprovalLineList } from '@/lib/approval-line/service';
+import { SearchParamsApprovalLineCache } from '@/lib/approval-line/validations';
+import { ApprovalLineTable } from '@/lib/approval-line/table/approval-line-table';
+
+export const metadata: Metadata = {
+ title: '결재선 관리',
+ description: '결재용 결재선을 관리합니다.',
+};
+
+interface PageProps {
+ searchParams: SearchParams;
+}
+
+export default async function ApprovalLinePage({ searchParams }: PageProps) {
+ const search = SearchParamsApprovalLineCache.parse(searchParams);
+ // getValidFilters 반환값이 undefined 인 경우 폴백
+ const validFilters = getValidFilters(search.filters) ?? [];
+
+ const promises = Promise.all([
+ getApprovalLineList({
+ ...search,
+ filters: validFilters,
+ }),
+ ]);
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">결재선 관리</h2>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 테이블 */}
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={[
+ '10rem',
+ '20rem',
+ '30rem',
+ '12rem',
+ '12rem',
+ '8rem',
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <ApprovalLineTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ );
+}
diff --git a/app/[lng]/evcp/(evcp)/approval/template/[id]/config.ts b/app/[lng]/evcp/(evcp)/approval/template/[id]/config.ts
new file mode 100644
index 00000000..1ad5b2e2
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/approval/template/[id]/config.ts
@@ -0,0 +1,33 @@
+// 결재 템플릿에서 사용할 변수(placeholder) 목록
+// DB를 거치지 않고 정적 파일에 정의하여 테스트/배포 리스크를 최소화합니다.
+// ApprovalTemplateEditor 컴포넌트는 `variableName` 값을 사용해 {{변수명}} 형식으로 본문에 삽입합니다.
+
+export const variables = [
+ {
+ variableName: "수신자 배열", // 예: ["홍길동", "김철수", ...]
+ variableType: "array",
+ description: "결재 수신자 목록 (이름/사번 등)",
+ },
+ {
+ variableName: "송신자 배열", // 예: 상신자 정보 배열
+ variableType: "array",
+ description: "결재 송신자(상신자) 목록",
+ },
+ {
+ variableName: "견적 RFQ 요약", // 예: RFQ 견적 요약 HTML 테이블
+ variableType: "html",
+ description: "견적 RFQ 요약표",
+ },
+ {
+ variableName: "RFQ(PR) 요약", // 예: 구매요청(PR) 요약 HTML 테이블
+ variableType: "html",
+ description: "RFQ(PR) 요약표",
+ },
+ {
+ variableName: "첨부문서 리스트 요약", // 예: 첨부 파일 리스트 HTML
+ variableType: "html",
+ description: "첨부문서 리스트 요약",
+ },
+] as const;
+
+
diff --git a/app/[lng]/evcp/(evcp)/approval/template/[id]/page.tsx b/app/[lng]/evcp/(evcp)/approval/template/[id]/page.tsx
new file mode 100644
index 00000000..136b09eb
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/approval/template/[id]/page.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { type Metadata } from "next"
+import { notFound } from "next/navigation"
+
+import { getApprovalTemplate } from "@/lib/approval-template/service"
+import { getApprovalLineOptions, getApprovalLineCategories } from "@/lib/approval-line/service"
+import { ApprovalTemplateEditor } from "@/lib/approval-template/editor/approval-template-editor"
+import { variables as configVariables } from "./config"
+
+interface ApprovalTemplateDetailPageProps {
+ params: Promise<{
+ id: string
+ }>
+}
+
+export async function generateMetadata({ params }: ApprovalTemplateDetailPageProps): Promise<Metadata> {
+ const { id } = await params
+ const template = await getApprovalTemplate(id)
+
+ if (!template) {
+ return {
+ title: "템플릿을 찾을 수 없음",
+ }
+ }
+
+ return {
+ title: `${template.name} - 템플릿 편집`,
+ description: template.description || `${template.name} 템플릿을 편집합니다.`,
+ }
+}
+
+export default async function ApprovalTemplateDetailPage({ params }: ApprovalTemplateDetailPageProps) {
+ const { id } = await params
+ const [template, approvalLineOptions, approvalLineCategories] = await Promise.all([
+ getApprovalTemplate(id),
+ getApprovalLineOptions(),
+ getApprovalLineCategories(),
+ ])
+
+ if (!template) {
+ notFound()
+ }
+
+ return (
+ <div className="flex flex-1 flex-col">
+ {template && (
+ <ApprovalTemplateEditor
+ templateId={id}
+ initialTemplate={template}
+ staticVariables={configVariables as unknown as Array<{ variableName: string }>}
+ approvalLineOptions={approvalLineOptions}
+ approvalLineCategories={approvalLineCategories}
+ />
+ )}
+ </div>
+ )
+}
+
diff --git a/app/[lng]/evcp/(evcp)/approval/template/page.tsx b/app/[lng]/evcp/(evcp)/approval/template/page.tsx
new file mode 100644
index 00000000..f475099c
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/approval/template/page.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react';
+import { type Metadata } from 'next';
+import { Shell } from '@/components/shell';
+import { DataTableSkeleton } from '@/components/data-table/data-table-skeleton';
+import { type SearchParams } from '@/types/table';
+import { getValidFilters } from '@/lib/data-table';
+
+import { getApprovalTemplateList } from '@/lib/approval-template/service';
+import { SearchParamsApprovalTemplateCache } from '@/lib/approval-template/validations';
+import { ApprovalTemplateTable } from '@/lib/approval-template/table/approval-template-table';
+
+export const metadata: Metadata = {
+ title: '결재 템플릿 관리',
+ description: '결재용 템플릿을 관리합니다.',
+};
+
+interface PageProps {
+ searchParams: SearchParams;
+}
+
+export default async function ApprovalTemplatePage({ searchParams }: PageProps) {
+ const search = SearchParamsApprovalTemplateCache.parse(searchParams);
+ // getValidFilters 반환값이 undefined 인 경우 폴백
+ const validFilters = getValidFilters(search.filters) ?? [];
+
+ const promises = Promise.all([
+ getApprovalTemplateList({
+ ...search,
+ filters: validFilters,
+ }),
+ ]);
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">결재 템플릿 관리</h2>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 테이블 */}
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={6}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={[
+ '10rem',
+ '40rem',
+ '12rem',
+ '12rem',
+ '8rem',
+ '8rem',
+ ]}
+ shrinkZero
+ />
+ }
+ >
+ <ApprovalTemplateTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ );
+} \ No newline at end of file
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts
index fdf0c8d4..4ae1bbda 100644
--- a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts
@@ -11,8 +11,6 @@ import {
extractRequestData,
convertXMLToDBData,
processNestedArray,
- createErrorResponse,
- createSuccessResponse,
createSoapResponse,
withSoapLogging,
} from '@/lib/soap/utils';
@@ -20,6 +18,9 @@ import {
bulkUpsert,
bulkReplaceSubTableData
} from "@/lib/soap/batch-utils";
+import {
+ mapAndSaveECCRfqData
+} from "@/lib/soap/ecc-mapper";
// 스키마에서 타입 추론
@@ -90,10 +91,43 @@ export async function POST(request: NextRequest) {
}
}
- // 5) 데이터베이스 저장
+ // 5) 원본 ECC 데이터 저장 (기존 로직 유지)
await saveToDatabase(processedData);
- console.log(`🎉 처리 완료: ${processedData.length}개 PR 데이터`);
+ // 6) ZBSART에 따라 비즈니스 테이블 분기 처리
+ const anHeaders: BidHeaderData[] = [];
+ const abHeaders: BidHeaderData[] = [];
+ const anItems: BidItemData[] = [];
+ const abItems: BidItemData[] = [];
+
+ // ZBSART에 따라 데이터 분류
+ for (const prData of processedData) {
+ if (prData.bidHeader.ZBSART === 'AN') {
+ anHeaders.push(prData.bidHeader);
+ anItems.push(...prData.bidItems);
+ } else if (prData.bidHeader.ZBSART === 'AB') {
+ abHeaders.push(prData.bidHeader);
+ abItems.push(...prData.bidItems);
+ }
+ }
+
+ // AN (RFQ) 데이터 처리 - procurementRfqs 테이블
+ let rfqMappingResult = null;
+ if (anHeaders.length > 0) {
+ rfqMappingResult = await mapAndSaveECCRfqData(anHeaders, anItems);
+ if (!rfqMappingResult.success) {
+ throw new Error(`RFQ 비즈니스 테이블 매핑 실패: ${rfqMappingResult.message}`);
+ }
+ }
+
+ // AB (Bidding) 데이터 처리 - TODO
+ if (abHeaders.length > 0) {
+ console.log(`⚠️ TODO: Bidding 데이터 처리 필요 - ${abHeaders.length}개 헤더, ${abItems.length}개 아이템`);
+ // TODO: mapAndSaveECCBiddingData 함수 구현 필요
+ // const biddingMappingResult = await mapAndSaveECCBiddingData(abHeaders, abItems);
+ }
+
+ console.log(`🎉 처리 완료: ${processedData.length}개 PR 데이터, ${rfqMappingResult?.processedCount || 0}개 RFQ 매핑, ${abHeaders.length}개 Bidding (TODO)`);
// 6) 성공 응답 반환
return createSoapResponse('http://60.101.108.100/', {