diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/admin/approval-test/page.tsx | 8 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/approval/line/page.tsx | 68 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/approval/template/[id]/config.ts | 33 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/approval/template/[id]/page.tsx | 58 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/approval/template/page.tsx | 68 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PR_INFORMATION/route.ts | 42 |
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/', { |
