refactor: Remove debug logs and optimize toast animations

- Removed debug console logs from the UPSERT process in the DataService to clean up the code.
- Disabled animations for Sonner toast notifications to enhance performance and user experience.
- Simplified the alert and dialog components by removing unnecessary animation classes, ensuring a smoother transition.
- Updated the SelectedItemsDetailInputComponent to load all related table data in edit mode, improving data management and consistency.
This commit is contained in:
DDD1542
2026-02-09 15:37:28 +09:00
parent 423ef6231a
commit d7f900d8ae
11 changed files with 80 additions and 108 deletions

View File

@@ -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) {

View File

@@ -1354,7 +1354,8 @@ class DataService {
parentKeys: Record<string, any>,
records: Array<Record<string, any>>,
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<string>(); // 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<string, any> = {};

View File

@@ -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 {
* {

View File

@@ -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 && (
<div className="sticky top-0 z-30 flex items-center justify-center gap-2 border-b bg-amber-50 px-4 py-1.5 backdrop-blur-sm dark:bg-amber-950/30">
<div className="h-2 w-2 rounded-full bg-amber-500" />
<span className="text-xs font-medium">
{activeLayerId}
{layerRegions[activeLayerId] && (
<span className="ml-2 text-amber-600">
(: {layerRegions[activeLayerId].width} x {layerRegions[activeLayerId].height}px)
</span>
)}
{!layerRegions[activeLayerId] && (
<span className="ml-2 text-red-500">
( - )
</span>
)}
</span>
<span className="text-xs font-medium"> {activeLayerId} </span>
</div>
)}

View File

@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[999] bg-black/80",
"fixed inset-0 z-[999] bg-black/80",
className,
)}
{...props}
@@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-[1000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
"bg-background fixed top-[50%] left-[50%] z-[1000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
)}
{...props}

View File

@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[999] bg-black/60",
"fixed inset-0 z-[999] bg-black/60",
className,
)}
{...props}
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[1000] flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
"bg-background fixed top-[50%] left-[50%] z-[1000] flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
)}
{...props}

View File

@@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
"bg-popover text-popover-foreground z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
@@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
"bg-popover text-popover-foreground z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}

View File

@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[2000] w-72 rounded-md border p-4 shadow-md outline-none",
"bg-popover text-popover-foreground z-[2000] w-72 rounded-md border p-4 shadow-md outline-none",
className,
)}
{...props}

View File

@@ -16,16 +16,14 @@ const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
"fixed z-50 gap-4 bg-background p-6 shadow-lg",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
top: "inset-x-0 top-0 border-b",
bottom: "inset-x-0 bottom-0 border-t",
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
@@ -60,7 +58,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm",
"bg-background/80 fixed inset-0 z-50 backdrop-blur-sm",
className,
)}
{...props}

View File

@@ -236,7 +236,8 @@ export const dataApi = {
upsertGroupedRecords: async (
tableName: string,
parentKeys: Record<string, any>,
records: Array<Record<string, any>>
records: Array<Record<string, any>>,
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));

View File

@@ -223,33 +223,34 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const additionalFields = componentConfig.additionalFields || [];
const firstRecord = dataArray[0];
// 수정 모드: 다른 sourceTable의 데이터도 추가 로드 (예: customer_item_mapping)
let mappingData: Record<string, any> | 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<string, Record<string, any>[]> = {};
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<SelectedItemsDetailInpu
return;
}
// 이 그룹의 sourceTable에 따라 데이터 소스 결정
const editTableName = new URLSearchParams(window.location.search).get("tableName");
const isOtherTable = group.sourceTable && group.sourceTable !== editTableName;
// 이 그룹의 sourceTable 결정 → API에서 가져온 전체 데이터 사용
const groupTable = group.sourceTable || editTableName || "";
// 현재 테이블만 sourceData fallback 허용 (다른 테이블은 빈 배열 → id 크로스오염 방지)
const isCurrentTable = !group.sourceTable || group.sourceTable === editTableName;
const groupDataList = allTableData[groupTable] || (isCurrentTable ? dataArray : []);
if (isOtherTable && mappingData) {
// 다른 테이블 그룹 (예: customer_item_mapping) → mappingData에서 로드
const entryData: Record<string, any> = {};
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<string, GroupEntry>();
dataArray.forEach((record) => {
groupDataList.forEach((record) => {
const entryData: Record<string, any> = {};
groupFields.forEach((field: any) => {
@@ -355,8 +332,6 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const entryKey = JSON.stringify(entryData);
if (!entriesMap.has(entryKey)) {
// DEBUG: record.id 확인 (추후 삭제)
console.log("🔑 [LOAD] record.id:", record.id, "record keys:", Object.keys(record));
entriesMap.set(entryKey, {
id: `${group.id}_entry_${entriesMap.size + 1}`,
// DB 레코드의 고유 id(UUID PK) 보존 → 수정 시 이 id로 UPDATE
@@ -678,37 +653,36 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const itemParentKeys = { ...parentKeys, item_id: itemId };
// === Step 1: 메인 테이블(customer_item_mapping) 저장 ===
const mappingRecord: Record<string, any> = {};
// 여러 개의 매핑 레코드 지원 (거래처 품번/품명이 다중일 수 있음)
const mappingRecords: Record<string, any>[] = [];
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<string, any> = {};
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<SelectedItemsDetailInpu
const mappingResult = await dataApi.upsertGroupedRecords(
mainTable,
itemParentKeys,
[mappingRecord],
mappingRecords,
);
} catch (err) {
console.error(`${mainTable} 저장 실패:`, err);
@@ -757,8 +731,6 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
if (entry._dbRecordId) {
priceRecord.id = entry._dbRecordId;
}
// DEBUG: id 전달 확인용 (추후 삭제)
console.log("🔑 [SAVE] entry._dbRecordId:", entry._dbRecordId, "→ priceRecord.id:", priceRecord.id, "entry keys:", Object.keys(entry));
priceRecords.push(priceRecord);
}
});