summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-09 07:15:46 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-09 07:15:46 +0000
commit542398c8c7b24cf1a364d9cff6743f6708c5dc24 (patch)
tree0368a6ea15238be1fe6aeeb31345fe28aff078dc
parent53bb65444c6b43dc76577db887585ba857f8f418 (diff)
[김준회] S-ERP SOAP 연결 (BIDDING PROJECT)
-rw-r--r--app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts206
-rw-r--r--db/schema/projects.ts11
2 files changed, 185 insertions, 32 deletions
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts
index e942cbc5..ae34e7b6 100644
--- a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts
@@ -1,6 +1,48 @@
// /app/api/soap/route.js
import { NextRequest, NextResponse } from 'next/server';
import { headers } from 'next/headers';
+import { XMLParser } from 'fast-xml-parser';
+import { z } from 'zod';
+import db from '@/db/db';
+import { biddingProjects, projectSeries, NewBiddingProject, NewProjectSeries } from '@/db/schema/projects';
+import { eq, and } from 'drizzle-orm';
+
+// Zod schema for validating the incoming data
+const BiddingProjectSchema = z.object({
+ pspid: z.string(),
+ projNm: z.string().optional(),
+ sector: z.string().optional(),
+ projMsrm: z.coerce.number().optional(),
+ kunnr: z.string().optional(),
+ kunnrNm: z.string().optional(),
+ cls1: z.string().optional(),
+ cls1Nm: z.string().optional(),
+ ptype: z.string().optional(),
+ ptypeNm: z.string().optional(),
+ pmodelCd: z.string().optional(),
+ pmodelNm: z.string().optional(),
+ pmodelSz: z.string().optional(),
+ pmodelUom: z.string().optional(),
+ txt04: z.string().optional(),
+ txt30: z.string().optional(),
+ estmPm: z.string().optional(),
+});
+
+const ProjectSeriesSchema = z.object({
+ pspid: z.string(),
+ sersNo: z.string(),
+ scDt: z.string().optional(),
+ klDt: z.string().optional(),
+ lcDt: z.string().optional(),
+ dlDt: z.string().optional(),
+ dockNo: z.string().optional(),
+ dockNm: z.string().optional(),
+});
+
+const RequestDataSchema = z.object({
+ biddingProjects: z.array(BiddingProjectSchema).optional().default([]),
+ projectSeries: z.array(ProjectSeriesSchema).optional().default([]),
+});
export async function POST(request: NextRequest) {
try {
@@ -9,30 +51,66 @@ export async function POST(request: NextRequest) {
const headersList = headers();
// 요청 로깅
- console.log('SOAP Request:', body);
- console.log('Headers:', headersList);
+ console.log('SOAP Request Headers:', headersList);
+
+ // XML 파서 설정
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ isArray: (name: string) => {
+ // biddingProjects와 projectSeries는 항상 배열로 처리
+ return ['biddingProjects', 'projectSeries'].includes(name);
+ }
+ });
+
+ // XML 파싱
+ const parsedData = parser.parse(body);
+
+ // SOAP Envelope 구조에서 실제 데이터 추출
+ const soapBody = parsedData?.['soap:Envelope']?.['soap:Body'];
+ if (!soapBody) {
+ throw new Error('Invalid SOAP message structure');
+ }
+
+ // IF_ECC_EVCP_BIDDING_PROJECTReq에서 데이터 추출
+ const requestData = soapBody['IF_ECC_EVCP_BIDDING_PROJECTReq'];
+ if (!requestData) {
+ throw new Error('Missing request data');
+ }
+
+ // 데이터 유효성 검증
+ const validationResult = RequestDataSchema.safeParse({
+ biddingProjects: requestData.biddingProjects || [],
+ projectSeries: requestData.projectSeries || []
+ });
- // 요청 처리 로직
- // SAP에서 보낸 데이터를 파싱하고 DB에 저장
- const data = parseSoapMessage(body);
- await saveToDatabase(data);
+ if (!validationResult.success) {
+ console.error('Validation error:', validationResult.error);
+ throw new Error(`Invalid data format: ${validationResult.error.message}`);
+ }
- // SOAP 응답 생성
+ const validatedData = validationResult.data;
+
+ // 데이터베이스 저장
+ await saveToDatabase(validatedData);
+
+ console.log(`Processed ${validatedData.biddingProjects.length} bidding projects and ${validatedData.projectSeries.length} project series`);
+
+ // SOAP 응답 생성 (WSDL에 따라 빈 응답)
const soapResponse = `<?xml version="1.0" encoding="UTF-8"?>
-<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+<soap:Envelope
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:tns="http://60.101.108.100/">
<soap:Body>
- <ns1:receiveDataResponse xmlns:ns1="http://60.101.108.100/soap">
- <result>success</result>
- </ns1:receiveDataResponse>
+ <tns:processResponse/>
</soap:Body>
</soap:Envelope>`;
return new NextResponse(soapResponse, {
headers: {
- 'Content-Type': 'application/xml',
+ 'Content-Type': 'application/soap+xml; charset=utf-8',
},
});
- } catch (error) {
+ } catch (error: unknown) {
console.error('SOAP Error:', error);
// 에러 응답
@@ -41,7 +119,7 @@ export async function POST(request: NextRequest) {
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
- <faultstring>${error.message}</faultstring>
+ <faultstring>${error instanceof Error ? error.message : 'Unknown error'}</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>`;
@@ -49,22 +127,96 @@ export async function POST(request: NextRequest) {
return new NextResponse(errorResponse, {
status: 500,
headers: {
- 'Content-Type': 'application/xml',
+ 'Content-Type': 'application/soap+xml; charset=utf-8',
},
});
}
}
-// SOAP 메시지 파싱 함수
-function parseSoapMessage(soapMessage) {
- // XML 파싱 로직 구현
- // 라이브러리 사용 예: fast-xml-parser, xml2js 등
- // 실제 구현은 SAP 메시지 형식에 따라 달라짐
- return { /* 파싱된 데이터 */ };
-}
-
-// DB 저장 함수
-async function saveToDatabase(data) {
- // 데이터베이스 저장 로직
- // Prisma, Mongoose 등 사용
+// 데이터베이스 저장 함수
+async function saveToDatabase(data: z.infer<typeof RequestDataSchema>) {
+ // 트랜잭션으로 처리
+ try {
+ // bidding projects 처리
+ for (const project of data.biddingProjects) {
+ // 기존 프로젝트 확인
+ const existingProject = await db.query.biddingProjects.findFirst({
+ where: eq(biddingProjects.pspid, project.pspid)
+ });
+
+ const projectValues: NewBiddingProject = {
+ pspid: project.pspid,
+ projNm: project.projNm,
+ sector: project.sector,
+ // decimal 타입은 문자열로 변환하여 전달
+ projMsrm: project.projMsrm !== undefined ? String(project.projMsrm) : null,
+ kunnr: project.kunnr,
+ kunnrNm: project.kunnrNm,
+ cls1: project.cls1,
+ cls1Nm: project.cls1Nm,
+ ptype: project.ptype,
+ ptypeNm: project.ptypeNm,
+ pmodelCd: project.pmodelCd,
+ pmodelNm: project.pmodelNm,
+ pmodelSz: project.pmodelSz,
+ pmodelUom: project.pmodelUom,
+ txt04: project.txt04,
+ txt30: project.txt30,
+ estmPm: project.estmPm
+ };
+
+ if (existingProject) {
+ // 업데이트
+ await db
+ .update(biddingProjects)
+ .set({
+ ...projectValues,
+ updatedAt: new Date()
+ })
+ .where(eq(biddingProjects.pspid, project.pspid));
+ } else {
+ // 신규 등록
+ await db.insert(biddingProjects).values(projectValues);
+ }
+ }
+
+ // project series 처리
+ for (const series of data.projectSeries) {
+ // 기존 시리즈 확인
+ const existingSeries = await db.query.projectSeries.findFirst({
+ where: and(
+ eq(projectSeries.pspid, series.pspid),
+ eq(projectSeries.sersNo, series.sersNo)
+ )
+ });
+
+ const seriesValues: NewProjectSeries = {
+ pspid: series.pspid,
+ sersNo: series.sersNo,
+ scDt: series.scDt,
+ klDt: series.klDt,
+ lcDt: series.lcDt,
+ dlDt: series.dlDt,
+ dockNo: series.dockNo,
+ dockNm: series.dockNm
+ };
+
+ if (existingSeries) {
+ // 업데이트
+ await db
+ .update(projectSeries)
+ .set(seriesValues)
+ .where(and(
+ eq(projectSeries.pspid, series.pspid),
+ eq(projectSeries.sersNo, series.sersNo)
+ ));
+ } else {
+ // 신규 등록
+ await db.insert(projectSeries).values(seriesValues);
+ }
+ }
+ } catch (error: unknown) {
+ console.error('Database operation failed:', error);
+ throw new Error(`Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
} \ No newline at end of file
diff --git a/db/schema/projects.ts b/db/schema/projects.ts
index 1b989d23..8401709a 100644
--- a/db/schema/projects.ts
+++ b/db/schema/projects.ts
@@ -1,4 +1,4 @@
-import { pgTable, varchar, text, timestamp,char, decimal, serial,uniqueIndex } from "drizzle-orm/pg-core"
+import { pgTable, varchar, text, timestamp, char, decimal, serial, uniqueIndex } from "drizzle-orm/pg-core"
export const projects = pgTable("projects", {
id: serial("id").primaryKey(),
@@ -40,8 +40,6 @@ export const biddingProjects = pgTable("bidding_projects", {
export const projectSeries = pgTable('project_series', {
pspid: char('pspid', { length: 24 }).notNull().references(() => biddingProjects.pspid), // 견적프로젝트번호
sersNo: char('sers_no', { length: 3 }).notNull(), // 시리즈번호
- // 받은 인터페이스 정의서에 따라 수정
- // klQtr: char('kl_qtr', { length: 10 }), // K/L 연도분기(YYYY_nQ)
scDt: char('sc_dt', {length: 8}), // Steel Cutting Date
klDt: char('kl_dt', {length: 8}), // Keel Laying Date
lcDt: char('lc_dt', {length: 8}), // Launching Date
@@ -52,7 +50,7 @@ export const projectSeries = pgTable('project_series', {
post1: varchar('post1', { length: 40 }), // SN공사명(계약후)
}, (table) => {
return {
- uniqueIdx: uniqueIndex("project_sersNo_unique").on(
+ pk: uniqueIndex("project_sersNo_unique").on(
table.pspid,
table.sersNo
)
@@ -60,4 +58,7 @@ export const projectSeries = pgTable('project_series', {
});
export type BiddingProjects = typeof biddingProjects.$inferSelect
-export type ProjectSeries = typeof projectSeries.$inferSelect \ No newline at end of file
+export type ProjectSeries = typeof projectSeries.$inferSelect
+// 새로 데이터 수신 시 구분을 위해 사용
+export type NewBiddingProject = typeof biddingProjects.$inferInsert
+export type NewProjectSeries = typeof projectSeries.$inferInsert \ No newline at end of file