From 1c1c1019b6af72771358d387a2ae70ca965cd9f9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 26 May 2025 04:25:47 +0000 Subject: (김준회) 아이템 리스트를 자재그룹으로 변경하고 PLM 인터페이스 처리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/admin/if/items/page.tsx | 371 ++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 app/[lng]/evcp/(evcp)/admin/if/items/page.tsx (limited to 'app') diff --git a/app/[lng]/evcp/(evcp)/admin/if/items/page.tsx b/app/[lng]/evcp/(evcp)/admin/if/items/page.tsx new file mode 100644 index 00000000..5fa788bd --- /dev/null +++ b/app/[lng]/evcp/(evcp)/admin/if/items/page.tsx @@ -0,0 +1,371 @@ +import { getOracleConnection } from "@/lib/oracle-db/db"; +import db from "@/db/db"; +import { items } from "@/db/schema/items"; +import { cache } from "react"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// Oracle 메타데이터와 행 타입 정의 +type OracleColumn = { + name: string; +}; + +type OracleRow = (string | number | null)[]; + +// PLM(?)의 Oracle DB 에 직접 연결해 데이터를 가져오는 페이지 +// 모든 데이터를 전부 가져오기는 힘들 수 있으므로 샘플 수준으로 가져오는 것을 우선 node로 처리한다. + +// Oracle에서 데이터를 가져오는 함수를 캐싱 +const fetchOracleData = cache(async (limit = 100, offset = 0) => { + const connection = await getOracleConnection(); + + try { + // 자재마스터 클래스 정보 테이블 조회 (CMCTB_MAT_CLAS) + const result = await connection.execute( + `SELECT + CLAS_CD, + CLAS_NM, + CLAS_DTL, + PRNT_CLAS_CD, + CLAS_LVL, + DEL_ORDR, + UOM, + STYPE, + GRD_MATL, + CHG_DT, + BSE_UOM + FROM SHI1.CMCTB_MAT_CLAS + WHERE ROWNUM <= :limit + :offset + OFFSET :offset ROWS`, + { limit, offset } + ); + + // 총 레코드 수 조회 + const countResult = await connection.execute( + `SELECT COUNT(*) AS TOTAL FROM SHI1.CMCTB_MAT_CLAS` + ); + + const totalCount = countResult.rows?.[0]?.[0] || 0; + + return { + rows: result.rows as OracleRow[] || [], + metadata: result.metaData as OracleColumn[] || [], + totalCount + }; + } catch (error) { + console.error("Oracle 데이터 조회 오류:", error); + return { rows: [], metadata: [], totalCount: 0 }; + } finally { + // 연결 종료 + if (connection) { + try { + await connection.close(); + } catch (err) { + console.error("Oracle 연결 종료 오류:", err); + } + } + } +}); + +// 전체 데이터를 가져와 Postgres에 삽입하는 함수 +const syncAllDataToPostgres = cache(async () => { + const BATCH_SIZE = 1000; // 한 번에 처리할 레코드 수 + const MAX_RECORDS = 50000; // 최대 처리할 레코드 수 + + try { + // 총 레코드 수 확인 + const { totalCount } = await fetchOracleData(1, 0); + const recordsToProcess = Math.min(totalCount, MAX_RECORDS); + + let processedCount = 0; + let currentOffset = 0; + + // 배치 단위로 처리 + while (processedCount < recordsToProcess) { + // 오라클에서 데이터 가져오기 + const { rows, metadata } = await fetchOracleData(BATCH_SIZE, currentOffset); + + if (!rows.length) break; + + // Postgres DB 트랜잭션 시작 + await db.transaction(async (tx) => { + // Oracle 데이터를 Postgres 스키마에 맞게 변환하여 삽입 (UPSERT) + for (const row of rows) { + // 배열 형태의 데이터를 객체로 변환 + const rowObj: Record = {}; + metadata.forEach((col: OracleColumn, index: number) => { + rowObj[col.name] = row[index]; + }); + + await tx + .insert(items) + .values({ + itemCode: String(rowObj.CLAS_CD || ''), + itemName: String(rowObj.CLAS_NM || ''), + description: rowObj.CLAS_DTL ? String(rowObj.CLAS_DTL) : null, + parentItemCode: rowObj.PRNT_CLAS_CD ? String(rowObj.PRNT_CLAS_CD) : null, + itemLevel: typeof rowObj.CLAS_LVL === 'number' ? rowObj.CLAS_LVL : null, + deleteFlag: rowObj.DEL_ORDR ? String(rowObj.DEL_ORDR) : null, + unitOfMeasure: rowObj.UOM ? String(rowObj.UOM) : null, + steelType: rowObj.STYPE ? String(rowObj.STYPE) : null, + gradeMaterial: rowObj.GRD_MATL ? String(rowObj.GRD_MATL) : null, + changeDate: rowObj.CHG_DT ? String(rowObj.CHG_DT) : null, + baseUnitOfMeasure: rowObj.BSE_UOM ? String(rowObj.BSE_UOM) : null + }) + .onConflictDoUpdate({ + target: items.itemCode, + set: { + itemName: String(rowObj.CLAS_NM || ''), + description: rowObj.CLAS_DTL ? String(rowObj.CLAS_DTL) : null, + parentItemCode: rowObj.PRNT_CLAS_CD ? String(rowObj.PRNT_CLAS_CD) : null, + itemLevel: typeof rowObj.CLAS_LVL === 'number' ? rowObj.CLAS_LVL : null, + deleteFlag: rowObj.DEL_ORDR ? String(rowObj.DEL_ORDR) : null, + unitOfMeasure: rowObj.UOM ? String(rowObj.UOM) : null, + steelType: rowObj.STYPE ? String(rowObj.STYPE) : null, + gradeMaterial: rowObj.GRD_MATL ? String(rowObj.GRD_MATL) : null, + changeDate: rowObj.CHG_DT ? String(rowObj.CHG_DT) : null, + baseUnitOfMeasure: rowObj.BSE_UOM ? String(rowObj.BSE_UOM) : null, + updatedAt: new Date() + } + }); + } + }); + + processedCount += rows.length; + currentOffset += BATCH_SIZE; + + // 진행 상황 업데이트 + const progress = Math.min(100, Math.round((processedCount / recordsToProcess) * 100)); + + // 임시 상태 저장 (실제 구현에서는 저장소나 상태 관리 도구를 사용) + console.log(`진행 상황: ${progress}% (${processedCount}/${recordsToProcess})`); + } + + return { + success: true, + message: `${processedCount}개의 자재마스터 클래스 정보가 items 테이블로 성공적으로 이관되었습니다.`, + count: processedCount + }; + } catch (error) { + console.error("Postgres 데이터 이관 오류:", error); + return { + success: false, + message: `데이터 이관 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + count: 0 + }; + } +}); + +// 샘플 데이터만 Postgres에 삽입하는 함수 +const syncSampleDataToPostgres = cache(async () => { + const { rows, metadata } = await fetchOracleData(100, 0); + + if (!rows.length) { + return { + success: false, + message: "Oracle에서 가져올 데이터가 없습니다.", + count: 0 + }; + } + + try { + // Postgres DB 트랜잭션 시작 + await db.transaction(async (tx) => { + // Oracle 데이터를 Postgres 스키마에 맞게 변환하여 삽입 (UPSERT) + for (const row of rows) { + // 배열 형태의 데이터를 객체로 변환 + const rowObj: Record = {}; + metadata.forEach((col: OracleColumn, index: number) => { + rowObj[col.name] = row[index]; + }); + + await tx + .insert(items) + .values({ + itemCode: String(rowObj.CLAS_CD || ''), + itemName: String(rowObj.CLAS_NM || ''), + description: rowObj.CLAS_DTL ? String(rowObj.CLAS_DTL) : null, + parentItemCode: rowObj.PRNT_CLAS_CD ? String(rowObj.PRNT_CLAS_CD) : null, + itemLevel: typeof rowObj.CLAS_LVL === 'number' ? rowObj.CLAS_LVL : null, + deleteFlag: rowObj.DEL_ORDR ? String(rowObj.DEL_ORDR) : null, + unitOfMeasure: rowObj.UOM ? String(rowObj.UOM) : null, + steelType: rowObj.STYPE ? String(rowObj.STYPE) : null, + gradeMaterial: rowObj.GRD_MATL ? String(rowObj.GRD_MATL) : null, + changeDate: rowObj.CHG_DT ? String(rowObj.CHG_DT) : null, + baseUnitOfMeasure: rowObj.BSE_UOM ? String(rowObj.BSE_UOM) : null + }) + .onConflictDoUpdate({ + target: items.itemCode, + set: { + itemName: String(rowObj.CLAS_NM || ''), + description: rowObj.CLAS_DTL ? String(rowObj.CLAS_DTL) : null, + parentItemCode: rowObj.PRNT_CLAS_CD ? String(rowObj.PRNT_CLAS_CD) : null, + itemLevel: typeof rowObj.CLAS_LVL === 'number' ? rowObj.CLAS_LVL : null, + deleteFlag: rowObj.DEL_ORDR ? String(rowObj.DEL_ORDR) : null, + unitOfMeasure: rowObj.UOM ? String(rowObj.UOM) : null, + steelType: rowObj.STYPE ? String(rowObj.STYPE) : null, + gradeMaterial: rowObj.GRD_MATL ? String(rowObj.GRD_MATL) : null, + changeDate: rowObj.CHG_DT ? String(rowObj.CHG_DT) : null, + baseUnitOfMeasure: rowObj.BSE_UOM ? String(rowObj.BSE_UOM) : null, + updatedAt: new Date() + } + }); + } + }); + + return { + success: true, + message: `${rows.length}개의 자재마스터 클래스 정보가 items 테이블로 성공적으로 이관되었습니다.`, + count: rows.length + }; + } catch (error) { + console.error("Postgres 데이터 삽입 오류:", error); + return { + success: false, + message: `데이터 이관 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`, + count: 0 + }; + } +}); + +// 현재 PostgreSQL DB에 저장된 데이터를 조회하는 함수 +const fetchCurrentPgData = cache(async () => { + try { + return await db.select().from(items).limit(100); + } catch (error) { + console.error("Postgres 데이터 조회 오류:", error); + return []; + } +}); + +export default async function ItemsAdminPage() { + // 데이터 초기 로드 + const { rows: oracleData, metadata, totalCount } = await fetchOracleData(100, 0); + const pgData = await fetchCurrentPgData(); + + // 서버 액션으로 샘플 데이터 동기화 수행 + async function handleSyncSample() { + "use server"; + await syncSampleDataToPostgres(); + // 반환 없이 void로 처리 + } + + // 서버 액션으로 전체 데이터 동기화 수행 + async function handleSyncAll() { + "use server"; + await syncAllDataToPostgres(); + // 반환 없이 void로 처리 + } + + return ( +
+

Items 테이블 데이터 관리

+

PLM의 Oracle DB에서 자재마스터 클래스 정보를 Items 테이블로 이관하는 페이지입니다.

+ +
+
+
+

Oracle DB 데이터

+

총 {totalCount}개 중 {oracleData.length}개의 레코드

+
+ +
+ + + + 클래스코드 + 클래스명 + 클래스내역 + 레벨 + + + + {oracleData.map((row: OracleRow, idx: number) => { + // 컬럼 인덱스 찾기 + const getColIndex = (name: string) => metadata.findIndex((col: OracleColumn) => col.name === name); + const classCdIdx = getColIndex('CLAS_CD'); + const clasNmIdx = getColIndex('CLAS_NM'); + const clasDtlIdx = getColIndex('CLAS_DTL'); + const clasLvlIdx = getColIndex('CLAS_LVL'); + + return ( + + {row[classCdIdx]} + {row[clasNmIdx]} + {row[clasDtlIdx]} + {row[clasLvlIdx]} + + ); + })} + +
+
+
+ +
+
+

Items 테이블 데이터

+

총 {pgData.length}개의 레코드

+
+ +
+ + + + ID + 아이템코드 + 아이템명 + 레벨 + + + + {pgData.map((row) => ( + + {row.id} + {row.itemCode} + {row.itemName} + {row.itemLevel} + + ))} + +
+
+
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+

데이터 이관 시 참고사항

+
    +
  • 샘플 데이터 이관은 100건의 데이터만 Items 테이블에 저장합니다.
  • +
  • 전체 데이터 이관은 1,000건씩 나누어 최대 50,000건까지 처리합니다.
  • +
  • 데이터 양이 많을 경우 이관 작업에 시간이 소요될 수 있습니다.
  • +
  • 기존 데이터는 유지하고 아이템코드가 같은 경우 업데이트합니다 (UPSERT).
  • +
  • Oracle의 CMCTB_MAT_CLAS 테이블 데이터를 Items 테이블로 매핑합니다.
  • +
+
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3