diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-09 10:40:11 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-09 10:40:11 +0000 |
| commit | caa01b321311de3983fb8bcf65bb20a6c047cf57 (patch) | |
| tree | a01fb9078aaa044f57d8cdf59a06bf18a6e3e7df | |
| parent | 86b1fd1cc801f45642f84d24c0b5c84368454ff0 (diff) | |
(김준회) 자재그룹코드 및 자재그룹명에 대해 별도 테이블 생성, 동기화 로직 작성(일회성 사용이며 수신시점에는 자동저장하므로 추후 사용 불필요), 자재그룹 선택기를 변경사항에 맞춰 업데이트, 자재그룹명은 MAKTX 로 김학의 프로 답변에 따라 변경
| -rw-r--r-- | app/[lng]/evcp/(evcp)/material-groups/page.tsx | 24 | ||||
| -rw-r--r-- | app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts | 50 | ||||
| -rw-r--r-- | components/material-groups/sync-button.tsx | 240 | ||||
| -rw-r--r-- | db/schema/MDG/mdg.ts | 9 | ||||
| -rw-r--r-- | db/schema/items.ts | 25 | ||||
| -rw-r--r-- | lib/material-groups/services.ts | 4 | ||||
| -rw-r--r-- | lib/material-groups/sync-service.ts | 200 | ||||
| -rw-r--r-- | lib/material-groups/table/material-group-table-columns.tsx | 5 | ||||
| -rw-r--r-- | lib/material-groups/table/material-group-table.tsx | 6 | ||||
| -rw-r--r-- | lib/material/material-group-service.ts | 7 |
10 files changed, 519 insertions, 51 deletions
diff --git a/app/[lng]/evcp/(evcp)/material-groups/page.tsx b/app/[lng]/evcp/(evcp)/material-groups/page.tsx index 3acd11b9..468e3412 100644 --- a/app/[lng]/evcp/(evcp)/material-groups/page.tsx +++ b/app/[lng]/evcp/(evcp)/material-groups/page.tsx @@ -13,6 +13,7 @@ import { Shell } from "@/components/shell" import { getMaterialGroups } from "@/lib/material-groups/services" import { MaterialGroupTable } from "@/lib/material-groups/table/material-group-table" import { InformationButton } from "@/components/information/information-button" +import { MaterialGroupSyncButton } from "@/components/material-groups/sync-button" import { searchParamsCache } from "@/lib/material-groups/validations" interface MaterialGroupPageProps { @@ -39,18 +40,19 @@ export default async function MaterialGroupPage(props: MaterialGroupPageProps) { return ( <Shell className="gap-2"> <div className="flex items-center justify-between space-y-2"> - <div className="flex items-center justify-between space-y-2"> - <div> - <div className="flex items-center gap-2"> - <h2 className="text-2xl font-bold tracking-tight"> - 자재그룹 - </h2> - <InformationButton pagePath="evcp/material-groups" /> - </div> - <p className="text-muted-foreground"> - MDG로부터 수신된 자재그룹 정보 - </p> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight"> + 자재그룹 + </h2> + <InformationButton pagePath="evcp/material-groups" /> </div> + <p className="text-muted-foreground"> + MDG로부터 수신된 자재그룹 정보 + </p> + </div> + <div className="flex items-center gap-2"> + <MaterialGroupSyncButton /> </div> </div> diff --git a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts index 2c2ab0fc..80bc85d3 100644 --- a/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts +++ b/app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts @@ -6,7 +6,8 @@ import { MATERIAL_MASTER_PART_MATL_PLNT, MATERIAL_MASTER_PART_MATL_UNIT, MATERIAL_MASTER_PART_MATL_CLASSASGN, - MATERIAL_MASTER_PART_MATL_CHARASGN + MATERIAL_MASTER_PART_MATL_CHARASGN, + MATERIAL_GROUP_MASTER } from "@/db/schema/MDG/mdg"; import { @@ -32,6 +33,7 @@ type MatlPlntData = typeof MATERIAL_MASTER_PART_MATL_PLNT.$inferInsert; type MatlUnitData = typeof MATERIAL_MASTER_PART_MATL_UNIT.$inferInsert; type MatlClassAsgnData = typeof MATERIAL_MASTER_PART_MATL_CLASSASGN.$inferInsert; type MatlCharAsgnData = typeof MATERIAL_MASTER_PART_MATL_CHARASGN.$inferInsert; +type MaterialGroupData = typeof MATERIAL_GROUP_MASTER.$inferInsert; // XML에서 받는 데이터 구조 (스키마와 동일한 구조, string 타입) type MatlXML = ToXMLFields<Omit<MatlData, 'id' | 'createdAt' | 'updatedAt'>> & { @@ -56,6 +58,7 @@ interface ProcessedMaterialData { units: MatlUnitData[]; classAssignments: MatlClassAsgnData[]; characteristicAssignments: MatlCharAsgnData[]; + materialGroup?: MaterialGroupData; // 자재그룹코드 데이터 (옵셔널) } export async function GET(request: NextRequest) { @@ -138,6 +141,12 @@ export async function POST(request: NextRequest) { * - 별도 필수 필드 없음 (스키마에서 notNull() 제거 예정) * - 전체 데이터셋 기반 삭제 후 재삽입 처리 * + * 3. 자재그룹코드 마스터 (MATERIAL_GROUP_MASTER) - 신규 추가 + * - MATKL(자재그룹코드), MAKTX(자재그룹명) 분리 저장 + * - MATKL이 unique 필드로 충돌 시 upsert 처리 + * - DESC 테이블에서 SPRAS='E'인 MAKTX만 매칭 + * - 중복 제거를 통한 성능 최적화 + * * XML 패턴: * - MATERIAL 인터페이스는 XML에 MATNR이 이미 포함된 패턴 * - 하위 테이블에도 MATNR 필드가 있어서 XML 값 우선 사용됨 @@ -191,13 +200,29 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { fkData ); + // 3단계: 자재그룹코드 마스터 데이터 생성 (MATKL 존재 시) + let materialGroup: MaterialGroupData | undefined; + + if (material.MATKL) { + // DESC 테이블에서 SPRAS='E'인 MAKTX 찾기 + const englishDescription = descriptions.find(desc => desc.SPRAS === 'E'); + + if (englishDescription && englishDescription.MAKTX) { + materialGroup = { + MATKL: material.MATKL, + MAKTX: englishDescription.MAKTX + }; + } + } + return { material, descriptions, plants, units, classAssignments, - characteristicAssignments + characteristicAssignments, + materialGroup }; }); } @@ -208,7 +233,10 @@ function transformMatlData(matlData: MatlXML[]): ProcessedMaterialData[] { * * 저장 전략: * 1. 최상위 테이블: MATNR 기준 upsert (충돌 시 업데이트) - * 2. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 + * 2. 자재그룹코드 마스터: MATKL 기준 upsert (충돌 시 업데이트) + * - 중복된 MATKL 제거하여 성능 최적화 + * - 영어(SPRAS='E') DESC만 처리 + * 3. 하위 테이블들: FK(MATNR) 기준 전체 삭제 후 재삽입 * - 송신 XML이 전체 데이터셋을 포함하므로 부분 업데이트 불필요 * - 데이터 일관성과 단순성 확보 * @@ -231,11 +259,25 @@ async function saveToDatabase(processedMaterials: ProcessedMaterialData[]) { const units = processedMaterials.flatMap((m) => m.units); const classAssignments = processedMaterials.flatMap((m) => m.classAssignments); const characteristicAssignments = processedMaterials.flatMap((m) => m.characteristicAssignments); + + // 3) 자재그룹코드 데이터 수집 (중복 제거) + const materialGroups = processedMaterials + .filter((m) => m.materialGroup) + .map((m) => m.materialGroup!) + .filter((group, index, self) => + self.findIndex(g => g.MATKL === group.MATKL) === index // MATKL 기준 중복 제거 + ); // 3) 부모 테이블 UPSERT (배치) await bulkUpsert(tx, MATERIAL_MASTER_PART_MATL, materialRows, 'MATNR'); - // 4) 하위 테이블 교체 (배치) + // 4) 자재그룹코드 마스터 UPSERT (별도 처리) + if (materialGroups.length > 0) { + console.log(`자재그룹코드 마스터 저장: ${materialGroups.length}개 그룹`); + await bulkUpsert(tx, MATERIAL_GROUP_MASTER, materialGroups, 'MATKL'); + } + + // 5) 하위 테이블 교체 (배치) await Promise.all([ bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_DESC, descriptions, MATERIAL_MASTER_PART_MATL_DESC.MATNR, matnrs), bulkReplaceSubTableData(tx, MATERIAL_MASTER_PART_MATL_PLNT, plants, MATERIAL_MASTER_PART_MATL_PLNT.MATNR, matnrs), diff --git a/components/material-groups/sync-button.tsx b/components/material-groups/sync-button.tsx new file mode 100644 index 00000000..7dc7da8f --- /dev/null +++ b/components/material-groups/sync-button.tsx @@ -0,0 +1,240 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from "@/components/ui/dialog"; +import { + RefreshCw, + Database, + CheckCircle, + AlertCircle, + Clock, + TrendingUp +} from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { + syncMaterialGroupMaster, + getMaterialGroupSyncStatus, + type SyncResult +} from "@/lib/material-groups/sync-service"; + +export function MaterialGroupSyncButton() { + const [isOpen, setIsOpen] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + const [syncStatus, setSyncStatus] = React.useState<{ + totalMDGRecords: number; + totalMasterRecords: number; + lastSyncDate?: string; + } | null>(null); + const [lastSyncResult, setLastSyncResult] = React.useState<SyncResult | null>(null); + const { toast } = useToast(); + + // 다이얼로그 열릴 때 상태 정보 로드 + React.useEffect(() => { + if (isOpen) { + loadSyncStatus(); + } + }, [isOpen]); + + const loadSyncStatus = async () => { + try { + const status = await getMaterialGroupSyncStatus(); + if (status.success && status.data) { + setSyncStatus(status.data); + } + } catch (error) { + console.error("동기화 상태 로드 실패:", error); + } + }; + + const handleSync = async () => { + setIsLoading(true); + try { + const result = await syncMaterialGroupMaster(); + setLastSyncResult(result); + + if (result.success) { + toast({ + title: "동기화 완료", + description: result.message, + variant: "default", + }); + // 상태 다시 로드 + await loadSyncStatus(); + } else { + toast({ + title: "동기화 실패", + description: result.message, + variant: "destructive", + }); + } + } catch (error) { + toast({ + title: "동기화 오류", + description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const getSyncBadgeVariant = () => { + if (!syncStatus) return "secondary"; + if (syncStatus.totalMDGRecords === syncStatus.totalMasterRecords) { + return "default"; // 동기화됨 + } + return "destructive"; // 동기화 필요 + }; + + return ( + <Dialog open={isOpen} onOpenChange={setIsOpen}> + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Database className="h-4 w-4" /> + 동기화 + {syncStatus && ( + <Badge variant={getSyncBadgeVariant()} className="ml-1"> + {syncStatus.totalMasterRecords} + </Badge> + )} + </Button> + </DialogTrigger> + + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Database className="h-5 w-5" /> + 자재그룹 마스터 동기화 + </DialogTitle> + <DialogDescription> + MDG 테이블로부터 자재그룹 마스터 데이터를 동기화합니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 현재 상태 */} + {syncStatus && ( + <div className="rounded-lg border p-4 space-y-3"> + <h4 className="font-medium flex items-center gap-2"> + <TrendingUp className="h-4 w-4" /> + 현재 상태 + </h4> + + <div className="grid grid-cols-2 gap-4 text-sm"> + <div> + <span className="text-muted-foreground">MDG 자재그룹:</span> + <div className="font-medium">{syncStatus.totalMDGRecords.toLocaleString()}개</div> + </div> + <div> + <span className="text-muted-foreground">마스터 테이블:</span> + <div className="font-medium">{syncStatus.totalMasterRecords.toLocaleString()}개</div> + </div> + </div> + + {syncStatus.lastSyncDate && ( + <div className="text-sm"> + <span className="text-muted-foreground flex items-center gap-1"> + <Clock className="h-3 w-3" /> + 최근 동기화: + </span> + <div className="font-medium">{formatDate(syncStatus.lastSyncDate)}</div> + </div> + )} + + <div className="flex items-center gap-2"> + <Badge variant={getSyncBadgeVariant()}> + {syncStatus.totalMDGRecords === syncStatus.totalMasterRecords + ? "동기화됨" + : "동기화 필요"} + </Badge> + </div> + </div> + )} + + {/* 마지막 동기화 결과 */} + {lastSyncResult && ( + <div className="rounded-lg border p-4 space-y-3"> + <h4 className="font-medium flex items-center gap-2"> + {lastSyncResult.success ? ( + <CheckCircle className="h-4 w-4 text-green-600" /> + ) : ( + <AlertCircle className="h-4 w-4 text-red-600" /> + )} + 마지막 동기화 결과 + </h4> + + <p className="text-sm text-muted-foreground"> + {lastSyncResult.message} + </p> + + {lastSyncResult.data && ( + <div className="grid grid-cols-3 gap-2 text-sm"> + <div className="text-center"> + <div className="font-medium text-green-600"> + {lastSyncResult.data.newRecords} + </div> + <div className="text-muted-foreground">신규</div> + </div> + <div className="text-center"> + <div className="font-medium text-blue-600"> + {lastSyncResult.data.updatedRecords} + </div> + <div className="text-muted-foreground">업데이트</div> + </div> + <div className="text-center"> + <div className="font-medium text-gray-600"> + {lastSyncResult.data.skippedRecords} + </div> + <div className="text-muted-foreground">건너뜀</div> + </div> + </div> + )} + </div> + )} + + {/* 동기화 실행 버튼 */} + <Button + onClick={handleSync} + disabled={isLoading} + className="w-full" + > + {isLoading ? ( + <> + <RefreshCw className="mr-2 h-4 w-4 animate-spin" /> + 동기화 중... + </> + ) : ( + <> + <RefreshCw className="mr-2 h-4 w-4" /> + 동기화 실행 + </> + )} + </Button> + + <p className="text-xs text-muted-foreground text-center"> + * 동기화는 기존 데이터를 업데이트하며 새로운 자재그룹을 추가합니다. + </p> + </div> + </DialogContent> + </Dialog> + ); +} diff --git a/db/schema/MDG/mdg.ts b/db/schema/MDG/mdg.ts index 60f94793..7dd2db88 100644 --- a/db/schema/MDG/mdg.ts +++ b/db/schema/MDG/mdg.ts @@ -1037,6 +1037,15 @@ export const MATERIAL_MASTER_PART_MATL_UNIT = mdgSchema.table('MATERIAL_MASTER_P updatedAt: timestamp('updated_at').defaultNow().notNull(), }); +// [별도 테이블] 자재그룹코드 마스터 - MATKL, MAKTX 중복 최소화 +export const MATERIAL_GROUP_MASTER = mdgSchema.table('MATERIAL_GROUP_MASTER', { + MATKL: varchar('MATKL', { length: 1000 }).primaryKey(), // Material Group Code (자재그룹코드) - 기본 키 + MAKTX: varchar('MAKTX', { length: 1000 }), // Material Group Description (자재그룹명) - 영어(SPRAS='E') 기준 + + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + // [root] export const MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE = mdgSchema.table('MATERIAL_MASTER_PART_RETURN_CMCTB_MAT_BSE', { id: integer('id').primaryKey().generatedByDefaultAsIdentity(), diff --git a/db/schema/items.ts b/db/schema/items.ts index ef19a276..e87f256e 100644 --- a/db/schema/items.ts +++ b/db/schema/items.ts @@ -1,6 +1,5 @@ import { pgTable, varchar, text, timestamp, serial, integer, unique, pgView } from "drizzle-orm/pg-core" -import { sql, eq } from "drizzle-orm" -import { MATERIAL_MASTER_PART_MATL, MATERIAL_MASTER_PART_MATL_CHARASGN } from "./MDG/mdg" +import { MATERIAL_GROUP_MASTER } from "./MDG/mdg" // 자재 아이템 정보 테이블 (items) - 기존 CMCTB_MAT_CLAS 테이블 매핑 (SOAP 연결 시 컬럼이 추가/삭제될 수 있음) export const items = pgTable("items", { @@ -89,27 +88,15 @@ export type ItemOffshoreHull = typeof itemOffshoreHull.$inferSelect; //각 테이블별 컬럼 변경(itemid -> itemCode) -// 자재그룹 검색용 뷰 - MATKL(자재그룹코드)별 자재그룹 설명 조회 +// 자재그룹 검색용 뷰 - 새로운 MATERIAL_GROUP_MASTER 테이블 기반 export const materialSearchView = pgView("material_search_view").as((qb) => { return qb .select({ - materialGroupCode: sql<string>`${MATERIAL_MASTER_PART_MATL.MATKL}`.as("material_group_code"), - materialGroupDesc: sql<string>`${MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB}`.as("material_group_desc"), - displayText: sql<string>`COALESCE(${MATERIAL_MASTER_PART_MATL.MATKL}, '') || ' - ' || COALESCE(${MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB}, '')`.as("display_text") + materialGroupCode: MATERIAL_GROUP_MASTER.MATKL, + materialGroupDesc: MATERIAL_GROUP_MASTER.MAKTX }) - .from(MATERIAL_MASTER_PART_MATL) - .innerJoin( - MATERIAL_MASTER_PART_MATL_CHARASGN, - eq(MATERIAL_MASTER_PART_MATL.MATNR, MATERIAL_MASTER_PART_MATL_CHARASGN.MATNR) - ) - .where(sql` - ${MATERIAL_MASTER_PART_MATL.MATKL} IS NOT NULL - AND ${MATERIAL_MASTER_PART_MATL.MATKL} != '' - AND ${MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB} IS NOT NULL - AND ${MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB} != '' - `) - .groupBy(MATERIAL_MASTER_PART_MATL.MATKL, MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB) - .orderBy(MATERIAL_MASTER_PART_MATL.MATKL, MATERIAL_MASTER_PART_MATL_CHARASGN.ATWTB); + .from(MATERIAL_GROUP_MASTER) + .orderBy(MATERIAL_GROUP_MASTER.MATKL); }); export type MaterialSearchView = typeof materialSearchView.$inferSelect; diff --git a/lib/material-groups/services.ts b/lib/material-groups/services.ts index ff2b7f70..ecbbc9e8 100644 --- a/lib/material-groups/services.ts +++ b/lib/material-groups/services.ts @@ -44,7 +44,6 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { globalWhere = or( ilike(materialSearchView.materialGroupCode, s), // 자재그룹코드 ilike(materialSearchView.materialGroupDesc, s), // 자재그룹 설명 - ilike(materialSearchView.displayText, s), // 표시 텍스트 ); } @@ -64,7 +63,6 @@ export async function getMaterialGroups(input: GetMaterialGroupsInput) { .select({ materialGroupCode: materialSearchView.materialGroupCode, materialGroupDesc: materialSearchView.materialGroupDesc, - displayText: materialSearchView.displayText, }) .from(materialSearchView) .where(finalWhere) @@ -114,7 +112,6 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite globalWhere = or( ilike(materialSearchView.materialGroupCode, s), ilike(materialSearchView.materialGroupDesc, s), - ilike(materialSearchView.displayText, s), ); } @@ -133,7 +130,6 @@ export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfinite .select({ materialGroupCode: materialSearchView.materialGroupCode, materialGroupDesc: materialSearchView.materialGroupDesc, - displayText: materialSearchView.displayText, }) .from(materialSearchView) .where(finalWhere) diff --git a/lib/material-groups/sync-service.ts b/lib/material-groups/sync-service.ts new file mode 100644 index 00000000..eda5dba5 --- /dev/null +++ b/lib/material-groups/sync-service.ts @@ -0,0 +1,200 @@ +"use server"; + +import { sql } from "drizzle-orm"; +import db from "@/db/db"; +import { + MATERIAL_MASTER_PART_MATL, + MATERIAL_MASTER_PART_MATL_DESC, + MATERIAL_GROUP_MASTER +} from "@/db/schema/MDG/mdg"; + +export interface SyncResult { + success: boolean; + message: string; + data?: { + totalProcessed: number; + newRecords: number; + updatedRecords: number; + skippedRecords: number; + }; +} + +/** + * 기존 MDG 데이터로부터 MATERIAL_GROUP_MASTER 테이블을 동기화하는 함수 + * + * 로직: + * 1. MATERIAL_MASTER_PART_MATL에서 MATKL(자재그룹코드)가 있는 레코드 조회 + * 2. 각 MATKL에 대해 MATERIAL_MASTER_PART_MATL_DESC에서 SPRAS='E'인 MAKTX 찾기 + * 3. MATERIAL_GROUP_MASTER에 UPSERT (MATKL 기준으로 중복 제거) + */ +export async function syncMaterialGroupMaster(): Promise<SyncResult> { + try { + console.log("📊 자재그룹 마스터 동기화 시작..."); + + const result = await db.transaction(async (tx) => { + // 1단계: 기존 MDG 테이블에서 MATKL별 영어 설명 조회 + const materialGroupData = await tx + .select({ + MATKL: MATERIAL_MASTER_PART_MATL.MATKL, + MAKTX: sql<string>` + ( + SELECT ${MATERIAL_MASTER_PART_MATL_DESC.MAKTX} + FROM ${MATERIAL_MASTER_PART_MATL_DESC} + WHERE ${MATERIAL_MASTER_PART_MATL_DESC.MATNR} = ${MATERIAL_MASTER_PART_MATL.MATNR} + AND ${MATERIAL_MASTER_PART_MATL_DESC.SPRAS} = 'E' + AND ${MATERIAL_MASTER_PART_MATL_DESC.MAKTX} IS NOT NULL + AND ${MATERIAL_MASTER_PART_MATL_DESC.MAKTX} != '' + LIMIT 1 + ) + `.as("MAKTX") + }) + .from(MATERIAL_MASTER_PART_MATL) + .where(sql` + ${MATERIAL_MASTER_PART_MATL.MATKL} IS NOT NULL + AND ${MATERIAL_MASTER_PART_MATL.MATKL} != '' + `) + .groupBy(MATERIAL_MASTER_PART_MATL.MATKL); // MATKL별 중복 제거 + + console.log(`🔍 발견된 고유 자재그룹코드: ${materialGroupData.length}개`); + + // 2단계: MAKTX가 있는 레코드만 필터링 + const validData = materialGroupData.filter(item => + item.MAKTX && item.MAKTX.trim() !== '' + ); + + console.log(`✅ 유효한 자재그룹 데이터: ${validData.length}개`); + + if (validData.length === 0) { + return { + totalProcessed: 0, + newRecords: 0, + updatedRecords: 0, + skippedRecords: materialGroupData.length + }; + } + + // 3단계: 기존 MATERIAL_GROUP_MASTER의 모든 MATKL 조회 + const existingRecords = await tx + .select({ MATKL: MATERIAL_GROUP_MASTER.MATKL }) + .from(MATERIAL_GROUP_MASTER); + + const existingMatkls = new Set(existingRecords.map(r => r.MATKL)); + + // 4단계: 신규/업데이트 데이터 분류 + const newRecords = validData.filter(item => !existingMatkls.has(item.MATKL)); + const updateRecords = validData.filter(item => existingMatkls.has(item.MATKL)); + + let insertedCount = 0; + let updatedCount = 0; + + // 5단계: 신규 레코드 삽입 (배치 처리) + if (newRecords.length > 0) { + await tx + .insert(MATERIAL_GROUP_MASTER) + .values( + newRecords.map(item => ({ + MATKL: item.MATKL, + MAKTX: item.MAKTX + })) + ); + insertedCount = newRecords.length; + console.log(`➕ 신규 삽입: ${insertedCount}개`); + } + + // 6단계: 기존 레코드 업데이트 (배치 처리) + if (updateRecords.length > 0) { + // PostgreSQL의 UPDATE ... FROM 구문 사용 + await tx.execute(sql` + UPDATE ${MATERIAL_GROUP_MASTER} + SET + ${MATERIAL_GROUP_MASTER.MAKTX} = temp_data.maktx, + ${MATERIAL_GROUP_MASTER.updatedAt} = NOW() + FROM ( + VALUES ${sql.join( + updateRecords.map(item => + sql`(${item.MATKL}, ${item.MAKTX})` + ), + sql`, ` + )} + ) AS temp_data(matkl, maktx) + WHERE ${MATERIAL_GROUP_MASTER.MATKL} = temp_data.matkl + `); + updatedCount = updateRecords.length; + console.log(`🔄 업데이트: ${updatedCount}개`); + } + + return { + totalProcessed: validData.length, + newRecords: insertedCount, + updatedRecords: updatedCount, + skippedRecords: materialGroupData.length - validData.length + }; + }); + + console.log("✅ 자재그룹 마스터 동기화 완료"); + + return { + success: true, + message: `동기화 완료: 신규 ${result.newRecords}개, 업데이트 ${result.updatedRecords}개, 건너뜀 ${result.skippedRecords}개`, + data: result + }; + + } catch (error) { + console.error("❌ 자재그룹 마스터 동기화 오류:", error); + + return { + success: false, + message: `동기화 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}` + }; + } +} + +/** + * 동기화 상태 확인 함수 + */ +export async function getMaterialGroupSyncStatus(): Promise<{ + success: boolean; + data?: { + totalMDGRecords: number; + totalMasterRecords: number; + lastSyncDate?: string; + }; +}> { + try { + const [mdgCount, masterCount] = await Promise.all([ + // MDG에서 유효한 MATKL 개수 + db + .select({ count: sql<number>`count(distinct ${MATERIAL_MASTER_PART_MATL.MATKL})` }) + .from(MATERIAL_MASTER_PART_MATL) + .where(sql` + ${MATERIAL_MASTER_PART_MATL.MATKL} IS NOT NULL + AND ${MATERIAL_MASTER_PART_MATL.MATKL} != '' + `), + + // MATERIAL_GROUP_MASTER 개수 + db + .select({ count: sql<number>`count(*)` }) + .from(MATERIAL_GROUP_MASTER) + ]); + + // 최근 업데이트 시간 조회 + const lastUpdated = await db + .select({ + lastUpdate: sql<string>`max(${MATERIAL_GROUP_MASTER.updatedAt})` + }) + .from(MATERIAL_GROUP_MASTER); + + return { + success: true, + data: { + totalMDGRecords: mdgCount[0]?.count || 0, + totalMasterRecords: masterCount[0]?.count || 0, + lastSyncDate: lastUpdated[0]?.lastUpdate || undefined + } + }; + + } catch (error) { + console.error("동기화 상태 확인 오류:", error); + return { success: false }; + } +} diff --git a/lib/material-groups/table/material-group-table-columns.tsx b/lib/material-groups/table/material-group-table-columns.tsx index f958104c..3d304325 100644 --- a/lib/material-groups/table/material-group-table-columns.tsx +++ b/lib/material-groups/table/material-group-table-columns.tsx @@ -9,7 +9,6 @@ import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table- type MaterialGroup = { materialGroupCode: string | null; materialGroupDesc: string | null; - displayText: string | null; } /** @@ -28,7 +27,7 @@ export function getColumns(): ColumnDef<MaterialGroup>[] { cell: ({ row }) => { const value = row.getValue("materialGroupCode") as string | null return ( - <div className="font-medium"> + <div className="font-medium w-[100px]"> {value || "-"} </div> ) @@ -44,7 +43,7 @@ export function getColumns(): ColumnDef<MaterialGroup>[] { cell: ({ row }) => { const value = row.getValue("materialGroupDesc") as string | null return ( - <div className="max-w-[300px] truncate"> + <div className="max-w-[400px] truncate"> {value || "-"} </div> ) diff --git a/lib/material-groups/table/material-group-table.tsx b/lib/material-groups/table/material-group-table.tsx index e38ccf70..766b5054 100644 --- a/lib/material-groups/table/material-group-table.tsx +++ b/lib/material-groups/table/material-group-table.tsx @@ -21,7 +21,6 @@ import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" type MaterialGroup = { materialGroupCode: string | null; materialGroupDesc: string | null; - displayText: string | null; } interface MaterialGroupTableProps { @@ -67,11 +66,6 @@ export function MaterialGroupTable({ promises }: MaterialGroupTableProps) { label: "자재그룹명", type: "text", }, - { - id: "displayText", - label: "표시텍스트", - type: "text", - }, ] // 확장된 useDataTable 훅 사용 (pageSize 기반 자동 전환) diff --git a/lib/material/material-group-service.ts b/lib/material/material-group-service.ts index 50ab5164..ae0ca725 100644 --- a/lib/material/material-group-service.ts +++ b/lib/material/material-group-service.ts @@ -7,7 +7,7 @@ import { materialSearchView } from "@/db/schema/items"; export interface MaterialSearchItem { materialGroupCode: string; materialGroupDesc: string; - displayText: string; + displayText: string; // 애플리케이션 레벨에서 계산된 필드 } export interface MaterialSearchResult { @@ -40,8 +40,7 @@ export async function searchMaterialsForSelector( const searchPattern = `%${query.trim()}%`; searchWhere = sql`( ${materialSearchView.materialGroupCode} ILIKE ${searchPattern} OR - ${materialSearchView.materialGroupDesc} ILIKE ${searchPattern} OR - ${materialSearchView.displayText} ILIKE ${searchPattern} + ${materialSearchView.materialGroupDesc} ILIKE ${searchPattern} )`; } @@ -67,7 +66,7 @@ export async function searchMaterialsForSelector( data: data.map((row) => ({ materialGroupCode: row.materialGroupCode, materialGroupDesc: row.materialGroupDesc, - displayText: row.displayText, + displayText: `${row.materialGroupCode || ''} - ${row.materialGroupDesc || ''}`, // 애플리케이션 레벨에서 생성 })), total, }; |
