diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-09 07:15:46 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-09 07:15:46 +0000 |
| commit | 542398c8c7b24cf1a364d9cff6743f6708c5dc24 (patch) | |
| tree | 0368a6ea15238be1fe6aeeb31345fe28aff078dc | |
| parent | 53bb65444c6b43dc76577db887585ba857f8f418 (diff) | |
[김준회] S-ERP SOAP 연결 (BIDDING PROJECT)
| -rw-r--r-- | app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_BIDDING_PROJECT/route.ts | 206 | ||||
| -rw-r--r-- | db/schema/projects.ts | 11 |
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 |
