diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index a7757397..0abc6793 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -688,7 +688,7 @@ router.post( authenticateToken, async (req: AuthenticatedRequest, res) => { try { - const { tableName, parentKeys, records } = req.body; + const { tableName, parentKeys, records, deleteOrphans = true } = req.body; // 입력값 검증 if (!tableName || !parentKeys || !records || !Array.isArray(records)) { @@ -722,7 +722,8 @@ router.post( parentKeys, records, req.user?.companyCode, - req.user?.userId + req.user?.userId, + deleteOrphans ); if (!result.success) { diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index ff3b502a..2c7e7865 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -1354,7 +1354,8 @@ class DataService { parentKeys: Record, records: Array>, userCompany?: string, - userId?: string + userId?: string, + deleteOrphans: boolean = true ): Promise< ServiceResponse<{ inserted: number; updated: number; deleted: number }> > { @@ -1422,11 +1423,6 @@ class DataService { const existingIds = new Set(existingRecords.rows.map((r: any) => r[pkColumn])); const processedIds = new Set(); // UPDATE 처리된 id 추적 - // DEBUG: 수신된 레코드와 기존 레코드 id 확인 - console.log(`🔑 [UPSERT DEBUG] pkColumn: ${pkColumn}`); - console.log(`🔑 [UPSERT DEBUG] existingIds:`, Array.from(existingIds)); - console.log(`🔑 [UPSERT DEBUG] records received:`, records.map((r: any) => ({ id: r[pkColumn], keys: Object.keys(r) }))); - for (const newRecord of records) { // 날짜 필드 정규화 const normalizedRecord: Record = {}; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b8e7a178..7276f5b0 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -289,6 +289,20 @@ select { } } +/* ===== Sonner 토스트 애니메이션 완전 제거 ===== */ +[data-sonner-toaster] [data-sonner-toast] { + animation: none !important; + transition: none !important; + opacity: 1 !important; + transform: none !important; +} +[data-sonner-toaster] [data-sonner-toast][data-mounted="true"] { + animation: none !important; +} +[data-sonner-toaster] [data-sonner-toast][data-removed="true"] { + animation: none !important; +} + /* ===== Print Styles ===== */ @media print { * { diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 442b51fd..a530c024 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { cn } from "@/lib/utils"; import { Database, Cog } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; @@ -6389,19 +6390,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU {activeLayerId > 1 && (
- - 레이어 {activeLayerId} 편집 중 - {layerRegions[activeLayerId] && ( - - (캔버스: {layerRegions[activeLayerId].width} x {layerRegions[activeLayerId].height}px) - - )} - {!layerRegions[activeLayerId] && ( - - (조건부 영역 미설정 - 기본 레이어에서 영역을 먼저 배치하세요) - - )} - + 레이어 {activeLayerId} 편집 중
)} diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx index b4d0cae5..3c7a9239 100644 --- a/frontend/components/ui/alert-dialog.tsx +++ b/frontend/components/ui/alert-dialog.tsx @@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( (({ className, ...props }, ref) => ( , - records: Array> + records: Array>, + options?: { deleteOrphans?: boolean } ): Promise<{ success: boolean; inserted?: number; updated?: number; deleted?: number; message?: string; error?: string }> => { try { console.log("📡 [dataApi.upsertGroupedRecords] 요청 데이터:", { @@ -251,6 +252,7 @@ export const dataApi = { tableName, parentKeys, records, + deleteOrphans: options?.deleteOrphans ?? true, // 기본값: true (기존 동작 유지) }; console.log("📦 [dataApi.upsertGroupedRecords] 요청 본문 (JSON):", JSON.stringify(requestBody, null, 2)); diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 034c3b41..3a2b83f9 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -223,33 +223,34 @@ export const SelectedItemsDetailInputComponent: React.FC | null = null; - - // URL의 tableName = 이미 데이터가 로드된 테이블. 그 외 sourceTable은 추가 조회 필요 + // 수정 모드: 모든 관련 테이블의 데이터를 API로 전체 로드 + // sourceData는 클릭한 1개 레코드만 포함할 수 있으므로, API로 전체를 다시 가져옴 const editTableName = new URLSearchParams(window.location.search).get("tableName"); - const otherTables = groups - .filter((g) => g.sourceTable && g.sourceTable !== editTableName) - .map((g) => g.sourceTable!) - .filter((v, i, a) => a.indexOf(v) === i); // 중복 제거 + const allTableData: Record[]> = {}; - if (otherTables.length > 0 && firstRecord.customer_id && firstRecord.item_id) { + if (firstRecord.customer_id && firstRecord.item_id) { try { const { dataApi } = await import("@/lib/api/data"); - for (const otherTable of otherTables) { - // getTableData 반환: { data: any[], total, page, size } (success 필드 없음) - const response = await dataApi.getTableData(otherTable, { + // 모든 sourceTable의 데이터를 API로 전체 로드 (중복 테이블 제거) + const allTables = groups + .map((g) => g.sourceTable || editTableName) + .filter((v, i, a) => v && a.indexOf(v) === i) as string[]; + + for (const table of allTables) { + const response = await dataApi.getTableData(table, { filters: { customer_id: firstRecord.customer_id, item_id: firstRecord.item_id, }, + sortBy: "created_date", + sortOrder: "desc", }); if (response.data && response.data.length > 0) { - mappingData = response.data[0]; + allTableData[table] = response.data; } } } catch (err) { - console.error("❌ 매핑 데이터 로드 실패:", err); + console.error("❌ 편집 데이터 전체 로드 실패:", err); } } @@ -263,41 +264,17 @@ export const SelectedItemsDetailInputComponent: React.FC = {}; - groupFields.forEach((field: any) => { - let fieldValue = mappingData![field.name]; - - // autoFillFrom 로직 - if ((fieldValue === undefined || fieldValue === null) && field.autoFillFrom) { - fieldValue = firstRecord[field.autoFillFrom] || firstRecord.item_id; - } - - if (fieldValue !== undefined && fieldValue !== null) { - entryData[field.name] = fieldValue; - } - }); - - if (Object.keys(entryData).length > 0) { - mainFieldGroups[group.id] = [{ - id: `${group.id}_entry_1`, - // DB 레코드의 고유 id(UUID PK) 보존 → 수정 시 이 id로 UPDATE - _dbRecordId: mappingData!.id || null, - ...entryData, - }]; - } else { - mainFieldGroups[group.id] = []; - } - } else { - // 현재 테이블 그룹 (예: customer_item_prices) → dataArray에서 로드 + { + // 모든 테이블 그룹: API에서 가져온 전체 레코드를 entry로 변환 const entriesMap = new Map(); - dataArray.forEach((record) => { + groupDataList.forEach((record) => { const entryData: Record = {}; groupFields.forEach((field: any) => { @@ -355,8 +332,6 @@ export const SelectedItemsDetailInputComponent: React.FC = {}; + // 여러 개의 매핑 레코드 지원 (거래처 품번/품명이 다중일 수 있음) + const mappingRecords: Record[] = []; mainGroups.forEach((group) => { const entries = item.fieldGroups[group.id] || []; const groupFields = additionalFields.filter((f) => f.groupId === group.id); - if (entries.length > 0) { + entries.forEach((entry) => { + const record: Record = {}; groupFields.forEach((field) => { - const val = entries[0][field.name]; - // 사용자가 실제 입력한 값만 포함 (빈 문자열, null 제외) + const val = entry[field.name]; if (val !== undefined && val !== null && val !== "") { - mappingRecord[field.name] = val; + record[field.name] = val; } }); // 기존 DB 레코드의 고유 id(PK)가 있으면 포함 → 백엔드에서 이 id로 UPDATE - if (entries[0]._dbRecordId) { - mappingRecord.id = entries[0]._dbRecordId; + if (entry._dbRecordId) { + record.id = entry._dbRecordId; } - } - - // autoFillFrom 필드 처리 (item_id 등) - // 단, item_id는 이미 정확한 itemId 변수를 사용 (autoFillFrom:"id"가 수정 모드에서 오작동 방지) - groupFields.forEach((field) => { - if (field.name === "item_id") { - // item_id는 위에서 계산된 정확한 itemId 사용 - mappingRecord.item_id = itemId; - } else if (field.autoFillFrom && item.originalData) { - const value = item.originalData[field.autoFillFrom]; - if (value !== undefined && value !== null) { - mappingRecord[field.name] = value; + // item_id는 정확한 itemId 변수 사용 (autoFillFrom:"id" 오작동 방지) + record.item_id = itemId; + // 나머지 autoFillFrom 필드 처리 + groupFields.forEach((field) => { + if (field.name !== "item_id" && field.autoFillFrom && item.originalData) { + const value = item.originalData[field.autoFillFrom]; + if (value !== undefined && value !== null && !record[field.name]) { + record[field.name] = value; + } } - } + }); + mappingRecords.push(record); }); }); @@ -716,7 +690,7 @@ export const SelectedItemsDetailInputComponent: React.FC