diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/admin/if/items/page.tsx | 371 |
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 |
