summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/admin/if/items/page.tsx371
1 files changed, 371 insertions, 0 deletions
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<string, string | number | null> = {};
+ 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<string, string | number | null> = {};
+ 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 (
+ <div className="p-8 space-y-6">
+ <h1 className="text-2xl font-bold">Items 테이블 데이터 관리</h1>
+ <p className="text-muted-foreground">PLM의 Oracle DB에서 자재마스터 클래스 정보를 Items 테이블로 이관하는 페이지입니다.</p>
+
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
+ <div className="border rounded-lg">
+ <div className="p-4 border-b bg-muted/50">
+ <h2 className="text-xl font-semibold">Oracle DB 데이터</h2>
+ <p className="text-sm text-muted-foreground">총 {totalCount}개 중 {oracleData.length}개의 레코드</p>
+ </div>
+
+ <div className="overflow-auto max-h-80 p-4">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>클래스코드</TableHead>
+ <TableHead>클래스명</TableHead>
+ <TableHead>클래스내역</TableHead>
+ <TableHead>레벨</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {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 (
+ <TableRow key={idx}>
+ <TableCell>{row[classCdIdx]}</TableCell>
+ <TableCell>{row[clasNmIdx]}</TableCell>
+ <TableCell>{row[clasDtlIdx]}</TableCell>
+ <TableCell>{row[clasLvlIdx]}</TableCell>
+ </TableRow>
+ );
+ })}
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+
+ <div className="border rounded-lg">
+ <div className="p-4 border-b bg-muted/50">
+ <h2 className="text-xl font-semibold">Items 테이블 데이터</h2>
+ <p className="text-sm text-muted-foreground">총 {pgData.length}개의 레코드</p>
+ </div>
+
+ <div className="overflow-auto max-h-80 p-4">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>ID</TableHead>
+ <TableHead>아이템코드</TableHead>
+ <TableHead>아이템명</TableHead>
+ <TableHead>레벨</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {pgData.map((row) => (
+ <TableRow key={row.id}>
+ <TableCell>{row.id}</TableCell>
+ <TableCell>{row.itemCode}</TableCell>
+ <TableCell>{row.itemName}</TableCell>
+ <TableCell>{row.itemLevel}</TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex flex-col gap-6 mt-8">
+ <div className="flex flex-wrap gap-4">
+ <form action={handleSyncSample}>
+ <Button type="submit" variant="default">
+ 샘플 데이터 이관 (100건)
+ </Button>
+ </form>
+
+ <form action={handleSyncAll}>
+ <Button type="submit" variant="secondary">
+ 전체 데이터 이관 (최대 50,000건)
+ </Button>
+ </form>
+ </div>
+
+ <div className="border rounded-lg p-4">
+ <h3 className="text-lg font-semibold mb-2">데이터 이관 시 참고사항</h3>
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
+ <li>샘플 데이터 이관은 100건의 데이터만 Items 테이블에 저장합니다.</li>
+ <li>전체 데이터 이관은 1,000건씩 나누어 최대 50,000건까지 처리합니다.</li>
+ <li>데이터 양이 많을 경우 이관 작업에 시간이 소요될 수 있습니다.</li>
+ <li>기존 데이터는 유지하고 아이템코드가 같은 경우 업데이트합니다 (UPSERT).</li>
+ <li>Oracle의 CMCTB_MAT_CLAS 테이블 데이터를 Items 테이블로 매핑합니다.</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file