summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-09 10:40:11 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-09 10:40:11 +0000
commitcaa01b321311de3983fb8bcf65bb20a6c047cf57 (patch)
treea01fb9078aaa044f57d8cdf59a06bf18a6e3e7df
parent86b1fd1cc801f45642f84d24c0b5c84368454ff0 (diff)
(김준회) 자재그룹코드 및 자재그룹명에 대해 별도 테이블 생성, 동기화 로직 작성(일회성 사용이며 수신시점에는 자동저장하므로 추후 사용 불필요), 자재그룹 선택기를 변경사항에 맞춰 업데이트, 자재그룹명은 MAKTX 로 김학의 프로 답변에 따라 변경
-rw-r--r--app/[lng]/evcp/(evcp)/material-groups/page.tsx24
-rw-r--r--app/api/(S-ERP)/(MDG)/IF_MDZ_EVCP_MATERIAL_MASTER_PART/route.ts50
-rw-r--r--components/material-groups/sync-button.tsx240
-rw-r--r--db/schema/MDG/mdg.ts9
-rw-r--r--db/schema/items.ts25
-rw-r--r--lib/material-groups/services.ts4
-rw-r--r--lib/material-groups/sync-service.ts200
-rw-r--r--lib/material-groups/table/material-group-table-columns.tsx5
-rw-r--r--lib/material-groups/table/material-group-table.tsx6
-rw-r--r--lib/material/material-group-service.ts7
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,
};