diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-18 09:39:14 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-18 09:39:14 +0000 |
| commit | aa71f75ace013b2fe982e5a104e61440458e0fd2 (patch) | |
| tree | 5270f8f0d4cf8f411c6bc1a9f0e0ca21b3003c8f /app/api/(S-ERP) | |
| parent | 13bc512bf26618d5c040fd9b19cc0afd7af7c55b (diff) | |
(김준회) PCR, PO, 변경PR 거절사유 수신 라우트 구현 (ECC 인터페이스), 세일즈포스 POC 테스트페이지 추가 (경로가 파트너 내부인 이유는 CORS 추가한 경로이기 때문이며, 수정될 수 있음), shi-api 유저 업데이트 로직 개선(분할정복패턴)
Diffstat (limited to 'app/api/(S-ERP)')
3 files changed, 570 insertions, 0 deletions
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts new file mode 100644 index 00000000..97ebdb4b --- /dev/null +++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PCR/route.ts @@ -0,0 +1,140 @@ +import { NextRequest } from 'next/server'; +import db from '@/db/db'; +import { ZMM_PCR } from '@/db/schema/ECC/ecc'; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + createSoapResponse, + withSoapLogging, +} from '@/lib/soap/utils'; +import { + bulkUpsert +} from "@/lib/soap/batch-utils"; + +type PCRData = typeof ZMM_PCR.$inferInsert; + +// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공 +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_PCR.wsdl'); + } + + return new Response('Method Not Allowed', { status: 405 }); +} + +// POST 요청이 데이터 적재 요구 (SOAP) +export async function POST(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_PCR.wsdl'); + } + + const body = await request.text(); + + // SOAP 로깅 래퍼 함수 사용 + return withSoapLogging( + 'INBOUND', + 'ECC', + 'IF_ECC_EVCP_PCR', + body, + async () => { + console.log('🚀 PCR 수신 시작, 데이터 길이:', body.length); + + // 1) XML 파싱 + const parser = createXMLParser(['T_PCR']); + const parsedData = parser.parse(body); + + // 2) SOAP Body 또는 루트에서 요청 데이터 추출 + const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PCRReq'); + if (!requestData) { + console.error('유효한 요청 데이터를 찾을 수 없습니다'); + throw new Error('Missing request data - IF_ECC_EVCP_PCRReq not found'); + } + + // 3) XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedData = transformPCRData(requestData as PCRRequestXML); + + // 4) 필수 필드 검증 + for (const pcrData of processedData) { + if (!pcrData.PCR_REQ || !pcrData.PCR_REQ_SEQ || !pcrData.EBELN || !pcrData.EBELP) { + throw new Error('Missing required fields: PCR_REQ, PCR_REQ_SEQ, EBELN, EBELP'); + } + } + + // 5) 데이터베이스 저장 + await saveToDatabase(processedData); + + console.log(`🎉 처리 완료: ${processedData.length}개 PCR 데이터`); + + // 6) 성공 응답 반환 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_PCRRes': { + EV_TYPE: 'S', + }, + }); + } + ).catch((error) => { + // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_PCRRes': { + EV_TYPE: 'E', + EV_MESSAGE: + error instanceof Error ? error.message.slice(0, 100) : 'Unknown error', + }, + }); + }); +} + +// ----------------------------------------------------------------------------- +// 데이터 변환 및 저장 관련 유틸리티 +// ----------------------------------------------------------------------------- + +// XML 구조 타입 정의 +type PCRDataXML = ToXMLFields<Omit<PCRData, 'id' | 'createdAt' | 'updatedAt'>>; + +// Root XML Request 타입 +type PCRRequestXML = { + CHG_GB?: string; + T_PCR?: PCRDataXML[]; +}; + +// XML -> DB 데이터 변환 함수 +function transformPCRData(requestData: PCRRequestXML): PCRData[] { + const pcrItems = requestData.T_PCR || []; + + return pcrItems.map((item) => { + // PCR 데이터 변환 (단일 테이블이므로 간단함) + const pcrDataConverted = convertXMLToDBData<PCRData>( + item as Record<string, string | undefined>, + undefined // PCR은 단일 테이블이므로 FK 데이터 불필요 + ); + + return pcrDataConverted; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedPCRs: PCRData[]) { + console.log(`데이터베이스(배치) 저장 시작: ${processedPCRs.length}개 PCR 데이터`); + + try { + await db.transaction(async (tx) => { + // 필수 키 필드가 있는 데이터만 필터링 (PCR_REQ가 unique key) + const validPCRRows = processedPCRs.filter((pcr): pcr is PCRData => !!pcr.PCR_REQ); + + // PCR 테이블에 UPSERT (배치) + // PCR_REQ가 unique 키이므로 이를 기준으로 upsert + await bulkUpsert(tx, ZMM_PCR, validPCRRows, 'PCR_REQ'); + }); + + console.log(`데이터베이스(배치) 저장 완료: ${processedPCRs.length}개 PCR`); + return true; + } catch (error) { + console.error('데이터베이스(배치) 저장 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts new file mode 100644 index 00000000..44ec3f36 --- /dev/null +++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts @@ -0,0 +1,287 @@ +import { NextRequest } from 'next/server'; +import db from '@/db/db'; +import { inArray } from 'drizzle-orm'; +import { + ZMM_HD, + ZMM_DT, + ZMM_PAY, + ZMM_KN, + ZMM_NOTE, + ZMM_NOTE2, +} from '@/db/schema/ECC/ecc'; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + processNestedArray, + createSoapResponse, + withSoapLogging, +} from '@/lib/soap/utils'; +import { + bulkUpsert, + bulkReplaceSubTableData +} from "@/lib/soap/batch-utils"; + +// 스키마에서 타입 추론 +type HeaderData = typeof ZMM_HD.$inferInsert; +type DetailData = typeof ZMM_DT.$inferInsert; +type PaymentData = typeof ZMM_PAY.$inferInsert; +type AccountData = typeof ZMM_KN.$inferInsert; +type NoteData = typeof ZMM_NOTE.$inferInsert; +type Note2Data = typeof ZMM_NOTE2.$inferInsert; + +// XML 구조 타입 정의 +type HeaderXML = ToXMLFields<Omit<HeaderData, 'id' | 'createdAt' | 'updatedAt'>>; +type DetailXML = ToXMLFields<Omit<DetailData, 'id' | 'createdAt' | 'updatedAt'>>; +type PaymentXML = ToXMLFields<Omit<PaymentData, 'id' | 'createdAt' | 'updatedAt'>>; +type AccountXML = ToXMLFields<Omit<AccountData, 'id' | 'createdAt' | 'updatedAt'>>; +type NoteXML = ToXMLFields<Omit<NoteData, 'id' | 'createdAt' | 'updatedAt'>>; +type Note2XML = ToXMLFields<Omit<Note2Data, 'id' | 'createdAt' | 'updatedAt'>>; + +// 처리된 데이터 구조 +interface ProcessedPOData { + header: HeaderData; + details: DetailData[]; + payments: PaymentData[]; + accounts: AccountData[]; + notes: NoteData[]; + notes2: Note2Data[]; +} + +// ZMM_DT와 연결된 데이터 구조 +interface DetailWithAccounts { + detail: DetailData; + accounts: AccountData[]; +} + +// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공 +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_PO_INFORMATION.wsdl'); + } + + return new Response('Method Not Allowed', { status: 405 }); +} + +// POST 요청이 데이터 적재 요구 (SOAP) +export async function POST(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_PO_INFORMATION.wsdl'); + } + + const body = await request.text(); + + // SOAP 로깅 래퍼 함수 사용 + return withSoapLogging( + 'INBOUND', + 'ECC', + 'IF_ECC_EVCP_PO_INFORMATION', + body, + async () => { + console.log('🚀 PO_INFORMATION 수신 시작, 데이터 길이:', body.length); + + // 1) XML 파싱 + const parser = createXMLParser(['T_HD', 'T_DT', 'T_PAY', 'T_KN', 'T_NOTE', 'T_NOTE2']); + const parsedData = parser.parse(body); + + // 2) SOAP Body 또는 루트에서 요청 데이터 추출 + const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_PO_INFORMATIONReq'); + if (!requestData) { + console.error('유효한 요청 데이터를 찾을 수 없습니다'); + throw new Error('Missing request data - IF_ECC_EVCP_PO_INFORMATIONReq not found'); + } + + // 3) XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedData = transformPOData(requestData as PORequestXML); + + // 4) 필수 필드 검증 + for (const poData of processedData) { + if (!poData.header.EBELN) { + throw new Error('Missing required field: EBELN in Header'); + } + } + + // 5) 데이터베이스 저장 + await saveToDatabase(processedData); + + console.log(`🎉 처리 완료: ${processedData.length}개 PO 데이터`); + + // 6) 성공 응답 반환 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': { + EV_TYPE: 'S', + }, + }); + } + ).catch((error) => { + // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_PO_INFORMATIONRes': { + EV_TYPE: 'E', + EV_MESSAGE: + error instanceof Error ? error.message.slice(0, 100) : 'Unknown error', + }, + }); + }); +} + +// ----------------------------------------------------------------------------- +// 데이터 변환 및 저장 관련 유틸리티 +// ----------------------------------------------------------------------------- + +// Root XML Request 타입 +type PORequestXML = { + CHG_GB?: string; + T_HD?: HeaderXML[]; + T_DT?: DetailXML[]; + T_PAY?: PaymentXML[]; + T_KN?: AccountXML[]; + T_NOTE?: NoteXML[]; + T_NOTE2?: Note2XML[]; +}; + +// XML -> DB 데이터 변환 함수 +function transformPOData(requestData: PORequestXML): ProcessedPOData[] { + const headers = requestData.T_HD || []; + const details = requestData.T_DT || []; + const payments = requestData.T_PAY || []; + const accounts = requestData.T_KN || []; + const notes = requestData.T_NOTE || []; + const notes2 = requestData.T_NOTE2 || []; + + return headers.map((header) => { + const headerKey = header.EBELN || ''; + const fkData = { EBELN: headerKey }; + + // Header 변환 + const headerConverted = convertXMLToDBData<HeaderData>( + header as Record<string, string | undefined>, + undefined // Header는 자체 필드만 사용 + ); + + // 해당 Header의 Detail들 필터 후 변환 (ZMM_KN도 함께 처리) + const relatedDetails = details.filter((detail) => detail.EBELN === headerKey); + const detailsWithAccounts: DetailWithAccounts[] = relatedDetails.map((detail) => { + const detailKey = detail.EBELP || ''; + const detailFkData = { EBELN: headerKey, EBELP: detailKey }; + + // Detail 변환 + const detailConverted = convertXMLToDBData<DetailData>( + detail as Record<string, string | undefined>, + fkData + ); + + // 해당 Detail의 Account들 필터 후 변환 (EBELN + EBELP로 매칭) + const relatedAccounts = accounts.filter( + (account) => account.EBELN === headerKey && account.EBELP === detailKey + ); + const accountsConverted = processNestedArray( + relatedAccounts, + (account) => + convertXMLToDBData<AccountData>(account as Record<string, string | undefined>, detailFkData), + detailFkData + ); + + return { + detail: detailConverted, + accounts: accountsConverted, + }; + }); + + // Detail들과 Account들을 분리 + const detailsConverted = detailsWithAccounts.map(d => d.detail); + const allAccountsConverted = detailsWithAccounts.flatMap(d => d.accounts); + + // 해당 Header의 Payment들 필터 후 변환 + const relatedPayments = payments.filter((payment) => payment.EBELN === headerKey); + const paymentsConverted = processNestedArray( + relatedPayments, + (payment) => + convertXMLToDBData<PaymentData>(payment as Record<string, string | undefined>, fkData), + fkData + ); + + // 해당 Header의 Note들 필터 후 변환 + const relatedNotes = notes.filter((note) => note.EBELN === headerKey); + const notesConverted = processNestedArray( + relatedNotes, + (note) => + convertXMLToDBData<NoteData>(note as Record<string, string | undefined>, fkData), + fkData + ); + + // 해당 Header의 Note2들 필터 후 변환 + const relatedNotes2 = notes2.filter((note2) => note2.EBELN === headerKey); + const notes2Converted = processNestedArray( + relatedNotes2, + (note2) => + convertXMLToDBData<Note2Data>(note2 as Record<string, string | undefined>, fkData), + fkData + ); + + return { + header: headerConverted, + details: detailsConverted, + payments: paymentsConverted, + notes: notesConverted, + notes2: notes2Converted, + // accounts는 이제 detail과 함께 처리되므로 별도로 저장하지 않음 + accounts: allAccountsConverted, + }; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedPOs: ProcessedPOData[]) { + console.log(`데이터베이스(배치) 저장 시작: ${processedPOs.length}개 PO 데이터`); + + try { + await db.transaction(async (tx) => { + // 1) 부모 테이블 데이터 준비 (키 없는 이상데이터 제거) + const headerRows = processedPOs + .map((po) => po.header) + .filter((h): h is HeaderData => !!h.EBELN); + + const headerKeys = headerRows.map((h) => h.EBELN as string); + + // 2) 하위 테이블 데이터 평탄화 + const detailRows = processedPOs.flatMap((po) => po.details); + const paymentRows = processedPOs.flatMap((po) => po.payments); + const accountRows = processedPOs.flatMap((po) => po.accounts); + const noteRows = processedPOs.flatMap((po) => po.notes); + const note2Rows = processedPOs.flatMap((po) => po.notes2); + + // 3) 부모 테이블 UPSERT (배치) + await bulkUpsert(tx, ZMM_HD, headerRows, 'EBELN'); + + // 4) 하위 테이블 교체 (배치) + await Promise.all([ + bulkReplaceSubTableData(tx, ZMM_DT, detailRows, ZMM_DT.EBELN, headerKeys), + bulkReplaceSubTableData(tx, ZMM_PAY, paymentRows, ZMM_PAY.EBELN, headerKeys), + bulkReplaceSubTableData(tx, ZMM_NOTE, noteRows, ZMM_NOTE.EBELN, headerKeys), + bulkReplaceSubTableData(tx, ZMM_NOTE2, note2Rows, ZMM_NOTE2.EBELN, headerKeys), + ]); + + // 5) ZMM_KN은 ZMM_DT의 서브테이블이므로 별도 처리 (EBELN + EBELP 조합으로 관리) + // 기존 ZMM_KN 데이터 삭제 (해당 EBELN의 모든 데이터) + await tx.delete(ZMM_KN).where( + inArray(ZMM_KN.EBELN, headerKeys) + ); + + // 새로운 ZMM_KN 데이터 삽입 + if (accountRows.length > 0) { + await tx.insert(ZMM_KN).values(accountRows); + } + }); + + console.log(`데이터베이스(배치) 저장 완료: ${processedPOs.length}개 PO`); + return true; + } catch (error) { + console.error('데이터베이스(배치) 저장 중 오류 발생:', error); + throw error; + } +} diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts new file mode 100644 index 00000000..daee219a --- /dev/null +++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_REJECT_FOR_REVISED_PR/route.ts @@ -0,0 +1,143 @@ +import { NextRequest } from 'next/server'; +import db from '@/db/db'; +import { T_CHANGE_PR } from '@/db/schema/ECC/ecc'; +import { + ToXMLFields, + serveWsdl, + createXMLParser, + extractRequestData, + convertXMLToDBData, + createSoapResponse, + withSoapLogging, +} from '@/lib/soap/utils'; +// 단일 테이블 insert이므로 batch-utils는 불필요 + +type ChangeData = typeof T_CHANGE_PR.$inferInsert; + +// GET 요청 처리는 ?wsdl 달고 있으면 WSDL 서비스 제공 +export async function GET(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl'); + } + + return new Response('Method Not Allowed', { status: 405 }); +} + +// POST 요청이 데이터 적재 요구 (SOAP) +export async function POST(request: NextRequest) { + const url = new URL(request.url); + if (url.searchParams.has('wsdl')) { + return serveWsdl('IF_ECC_EVCP_REJECT_FOR_REVISED_PR.wsdl'); + } + + const body = await request.text(); + + // SOAP 로깅 래퍼 함수 사용 + return withSoapLogging( + 'INBOUND', + 'ECC', + 'IF_ECC_EVCP_REJECT_FOR_REVISED_PR', + body, + async () => { + console.log('🚀 REJECT_FOR_REVISED_PR 수신 시작, 데이터 길이:', body.length); + + // 1) XML 파싱 + const parser = createXMLParser(['T_CHANGE_PR']); + const parsedData = parser.parse(body); + + // 2) SOAP Body 또는 루트에서 요청 데이터 추출 + const requestData = extractRequestData(parsedData, 'IF_ECC_EVCP_REJECT_FOR_REVISED_PRReq'); + if (!requestData) { + console.error('유효한 요청 데이터를 찾을 수 없습니다'); + throw new Error('Missing request data - IF_ECC_EVCP_REJECT_FOR_REVISED_PRReq not found'); + } + + // 3) XML 데이터를 DB 삽입 가능한 형태로 변환 + const processedData = transformRejectData(requestData as RejectRequestXML); + + // 4) 필수 필드 검증 + for (const changeData of processedData) { + if (!changeData.BANFN || !changeData.BANPO || !changeData.ZCHG_NO) { + throw new Error('Missing required fields: BANFN, BANPO, ZCHG_NO'); + } + } + + // 5) 데이터베이스 저장 + await saveToDatabase(processedData); + + console.log(`🎉 처리 완료: ${processedData.length}개 PR 거부 데이터`); + + // 6) 성공 응답 반환 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_REJECT_FOR_REVISED_PRRes': { + EV_TYPE: 'S', + }, + }); + } + ).catch((error) => { + // withSoapLogging에서 이미 에러 로그를 처리하므로, 여기서는 응답만 생성 + return createSoapResponse('http://60.101.108.100/', { + 'tns:IF_ECC_EVCP_REJECT_FOR_REVISED_PRRes': { + EV_TYPE: 'E', + EV_MESSAGE: + error instanceof Error ? error.message.slice(0, 100) : 'Unknown error', + }, + }); + }); +} + +// ----------------------------------------------------------------------------- +// 데이터 변환 및 저장 관련 유틸리티 +// ----------------------------------------------------------------------------- + +// XML 구조 타입 정의 +type ChangeDataXML = ToXMLFields<Omit<ChangeData, 'id' | 'createdAt' | 'updatedAt'>>; + +// Root XML Request 타입 +type RejectRequestXML = { + IV_ERDAT?: string; // Reject Date (메타데이터, 저장하지 않음) + IV_ERZET?: string; // Reject Time (메타데이터, 저장하지 않음) + T_CHANGE_PR?: ChangeDataXML[]; +}; + +// XML -> DB 데이터 변환 함수 +function transformRejectData(requestData: RejectRequestXML): ChangeData[] { + const changeItems = requestData.T_CHANGE_PR || []; + + return changeItems.map((item) => { + // Change 데이터 변환 (단일 테이블이므로 간단함) + const changeDataConverted = convertXMLToDBData<ChangeData>( + item as Record<string, string | undefined>, + undefined // 단일 테이블이므로 FK 데이터 불필요 + ); + + return changeDataConverted; + }); +} + +// 데이터베이스 저장 함수 +async function saveToDatabase(processedChanges: ChangeData[]) { + console.log(`데이터베이스(배치) 저장 시작: ${processedChanges.length}개 PR 거부 데이터`); + + try { + await db.transaction(async (tx) => { + // 필수 키 필드가 있는 데이터만 필터링 + const validChangeRows = processedChanges.filter((change): change is ChangeData => + !!change.BANFN && !!change.BANPO && !!change.ZCHG_NO + ); + + // T_CHANGE_PR 테이블에 UPSERT (배치) + // BANFN + BANPO + ZCHG_NO 조합을 unique 키로 사용 + if (validChangeRows.length > 0) { + await tx.insert(T_CHANGE_PR).values(validChangeRows); + } + }); + + console.log(`데이터베이스(배치) 저장 완료: ${processedChanges.length}개 PR 거부`); + return true; + } catch (error) { + console.error('데이터베이스(배치) 저장 중 오류 발생:', error); + throw error; + } +} |
