From 8fdf57bedd891b6f545ab938d3163f878bfbcddc Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 25 Nov 2025 16:56:50 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=EA=B3=BC=EB=8F=84=ED=95=9C=20?= =?UTF-8?q?=EC=BD=98=EC=86=94=20=EB=A1=9C=EA=B7=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModalRepeaterTableComponent: 반복 렌더링 로그 제거 - TableListComponent: 렌더링 조건 체크 IIFE 단순화 - ConditionalContainerComponent: 디버깅 로그 삭제 - DynamicComponentRenderer: value 설정 로그 제거 - resizable-dialog: userStyle 상세 로그 정리 - page.tsx: 반복 데이터 탐색 로그 삭제 에러 핸들링 및 주요 분기점 로그만 보존 --- .../app/(main)/screens/[screenId]/page.tsx | 46 +------------ frontend/components/ui/resizable-dialog.tsx | 22 ------- .../lib/registry/DynamicComponentRenderer.tsx | 11 +--- .../ConditionalContainerComponent.tsx | 23 ------- .../ModalRepeaterTableComponent.tsx | 65 +++---------------- .../table-list/TableListComponent.tsx | 16 +---- 6 files changed, 14 insertions(+), 169 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index ce99a685..5685d23a 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -356,17 +356,6 @@ function ScreenViewPage() { return isButton; }); - console.log( - "🔍 메뉴에서 발견된 전체 버튼:", - allButtons.map((b) => ({ - id: b.id, - label: b.label, - positionX: b.position.x, - positionY: b.position.y, - width: b.size?.width, - height: b.size?.height, - })), - ); topLevelComponents.forEach((component) => { const isButton = @@ -406,33 +395,13 @@ function ScreenViewPage() { (c) => (c as any).componentId === "table-search-widget", ); - // 디버그: 모든 컴포넌트 타입 확인 - console.log( - "🔍 전체 컴포넌트 타입:", - regularComponents.map((c) => ({ - id: c.id, - type: c.type, - componentType: (c as any).componentType, - componentId: (c as any).componentId, - })), - ); - - // 🆕 조건부 컨테이너들을 찾기 + // 조건부 컨테이너들을 찾기 const conditionalContainers = regularComponents.filter( (c) => (c as any).componentId === "conditional-container" || (c as any).componentType === "conditional-container", ); - console.log( - "🔍 조건부 컨테이너 발견:", - conditionalContainers.map((c) => ({ - id: c.id, - y: c.position.y, - size: c.size, - })), - ); - // TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정 const adjustedComponents = regularComponents.map((component) => { const isTableSearchWidget = (component as any).componentId === "table-search-widget"; @@ -520,12 +489,6 @@ function ScreenViewPage() { columnOrder={tableColumnOrder} tableDisplayData={tableDisplayData} onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => { - console.log("🔍 화면에서 선택된 행 데이터:", selectedData); - console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터:", { - count: tableDisplayData?.length, - firstRow: tableDisplayData?.[0], - }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); @@ -604,12 +567,6 @@ function ScreenViewPage() { columnOrder, tableDisplayData, ) => { - console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); - console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder }); - console.log("📊 화면 표시 데이터 (자식):", { - count: tableDisplayData?.length, - firstRow: tableDisplayData?.[0], - }); setSelectedRowsData(selectedData); setTableSortBy(sortBy); setTableSortOrder(sortOrder || "asc"); @@ -618,7 +575,6 @@ function ScreenViewPage() { }} refreshKey={tableRefreshKey} onRefresh={() => { - console.log("🔄 테이블 새로고침 요청됨 (자식)"); setTableRefreshKey((prev) => prev + 1); setSelectedRowsData([]); // 선택 해제 }} diff --git a/frontend/components/ui/resizable-dialog.tsx b/frontend/components/ui/resizable-dialog.tsx index 74a53411..cc28be85 100644 --- a/frontend/components/ui/resizable-dialog.tsx +++ b/frontend/components/ui/resizable-dialog.tsx @@ -122,10 +122,6 @@ const ResizableDialogContent = React.forwardRef< // 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용) if (userStyle) { - console.log("🔍 userStyle 감지:", userStyle); - console.log("🔍 userStyle.width 타입:", typeof userStyle.width, "값:", userStyle.width); - console.log("🔍 userStyle.height 타입:", typeof userStyle.height, "값:", userStyle.height); - const styleWidth = typeof userStyle.width === 'string' ? parseInt(userStyle.width) : userStyle.width; @@ -133,31 +129,15 @@ const ResizableDialogContent = React.forwardRef< ? parseInt(userStyle.height) : userStyle.height; - console.log("📏 파싱된 크기:", { - styleWidth, - styleHeight, - "styleWidth truthy?": !!styleWidth, - "styleHeight truthy?": !!styleHeight, - minWidth, - maxWidth, - minHeight, - maxHeight - }); - if (styleWidth && styleHeight) { const finalSize = { width: Math.max(minWidth, Math.min(maxWidth, styleWidth)), height: Math.max(minHeight, Math.min(maxHeight, styleHeight)), }; - console.log("✅ userStyle 크기 사용:", finalSize); return finalSize; - } else { - console.log("❌ styleWidth 또는 styleHeight가 falsy:", { styleWidth, styleHeight }); } } - console.log("⚠️ userStyle 없음, defaultWidth/defaultHeight 사용:", { defaultWidth, defaultHeight }); - // 2순위: 현재 렌더링된 크기 사용 (주석처리 - 모달이 열린 후 늘어나는 현상 방지) // if (contentRef.current) { // const rect = contentRef.current.getBoundingClientRect(); @@ -209,7 +189,6 @@ const ResizableDialogContent = React.forwardRef< // 사용자가 리사이징한 크기 우선 setSize({ width: savedSize.width, height: savedSize.height }); setUserResized(true); - console.log("✅ 사용자 리사이징 크기 적용:", savedSize); } else if (userStyle && userStyle.width && userStyle.height) { // 화면관리에서 설정한 크기 const styleWidth = typeof userStyle.width === 'string' @@ -224,7 +203,6 @@ const ResizableDialogContent = React.forwardRef< width: Math.max(minWidth, Math.min(maxWidth, styleWidth)), height: Math.max(minHeight, Math.min(maxHeight, styleHeight)), }; - console.log("🔄 userStyle 크기 적용:", newSize); setSize(newSize); } } diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index cf6037eb..245e2527 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -289,17 +289,8 @@ export const DynamicComponentRenderer: React.FC = // modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화 let currentValue; if (componentType === "modal-repeater-table") { - // 🆕 EditModal에서 전달된 groupedData가 있으면 우선 사용 + // EditModal에서 전달된 groupedData가 있으면 우선 사용 currentValue = props.groupedData || formData?.[fieldName] || []; - - // 디버깅 로그 - console.log("🔍 [DynamicComponentRenderer] ModalRepeaterTable value 설정:", { - hasGroupedData: !!props.groupedData, - groupedDataLength: props.groupedData?.length || 0, - fieldName, - formDataValue: formData?.[fieldName], - finalValueLength: Array.isArray(currentValue) ? currentValue.length : 0, - }); } else { currentValue = formData?.[fieldName] || ""; } diff --git a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx index 626ee137..6f2ab183 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx @@ -13,8 +13,6 @@ import { ConditionalContainerProps, ConditionalSection } from "./types"; import { ConditionalSectionViewer } from "./ConditionalSectionViewer"; import { cn } from "@/lib/utils"; -console.log("🚀 ConditionalContainerComponent 모듈 로드됨!"); - /** * 조건부 컨테이너 컴포넌트 * 상단 셀렉트박스 값에 따라 하단에 다른 UI를 표시 @@ -43,11 +41,6 @@ export function ConditionalContainerComponent({ groupedData, // 🆕 그룹 데이터 onSave, // 🆕 EditModal의 handleSave 콜백 }: ConditionalContainerProps) { - console.log("🎯 ConditionalContainerComponent 렌더링!", { - isDesignMode, - hasOnHeightChange: !!onHeightChange, - componentId, - }); // config prop 우선, 없으면 개별 prop 사용 const controlField = config?.controlField || propControlField || "condition"; @@ -86,24 +79,8 @@ export function ConditionalContainerComponent({ const containerRef = useRef(null); const previousHeightRef = useRef(0); - // 🔍 디버그: props 확인 - useEffect(() => { - console.log("🔍 ConditionalContainer props:", { - isDesignMode, - hasOnHeightChange: !!onHeightChange, - componentId, - selectedValue, - }); - }, [isDesignMode, onHeightChange, componentId, selectedValue]); - // 높이 변화 감지 및 콜백 호출 useEffect(() => { - console.log("🔍 ResizeObserver 등록 조건:", { - hasContainer: !!containerRef.current, - isDesignMode, - hasOnHeightChange: !!onHeightChange, - }); - if (!containerRef.current || isDesignMode || !onHeightChange) return; const resizeObserver = new ResizeObserver((entries) => { diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 59ce35a8..91d7b36f 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -224,13 +224,11 @@ export function ModalRepeaterTableComponent({ const configuredColumns = componentConfig?.columns || propColumns || []; if (configuredColumns.length > 0) { - console.log("✅ 설정된 columns 사용:", configuredColumns); return configuredColumns; } // columns가 비어있으면 sourceColumns로부터 자동 생성 if (sourceColumns.length > 0) { - console.log("🔄 sourceColumns로부터 자동 생성:", sourceColumns); const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({ field: field, label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능) @@ -238,85 +236,49 @@ export function ModalRepeaterTableComponent({ type: "text" as const, width: "150px", })); - console.log("📋 자동 생성된 columns:", autoColumns); return autoColumns; } - console.warn("⚠️ columns와 sourceColumns 모두 비어있음!"); + console.warn("⚠️ [ModalRepeaterTable] columns와 sourceColumns 모두 비어있음!"); return []; }, [componentConfig?.columns, propColumns, sourceColumns]); - // 초기 props 로깅 + // 초기 props 검증 useEffect(() => { if (rawSourceColumns.length !== sourceColumns.length) { - console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개 (빈 문자열 제거)`); + console.warn(`⚠️ [ModalRepeaterTable] sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개`); } if (rawUniqueField !== uniqueField) { - console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`); + console.warn(`⚠️ [ModalRepeaterTable] uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`); } - console.log("🎬 ModalRepeaterTableComponent 마운트:", { - columnsLength: columns.length, - sourceTable, - sourceColumns, - uniqueField, - }); - if (columns.length === 0) { - console.error("❌ columns가 비어있습니다! sourceColumns:", sourceColumns); - } else { - console.log("✅ columns 설정 완료:", columns.map(c => c.label || c.field).join(", ")); + console.error("❌ [ModalRepeaterTable] columns가 비어있습니다!", { sourceColumns }); } }, []); - // value 변경 감지 - useEffect(() => { - console.log("📦 ModalRepeaterTableComponent value 변경:", { - valueLength: value.length, - }); - }, [value]); - // 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너) useEffect(() => { const handleSaveRequest = async (event: Event) => { const componentKey = columnName || component?.id || "modal_repeater_data"; - console.log("🔔 [ModalRepeaterTable] beforeFormSave 이벤트 수신!", { - componentKey, - itemsCount: value.length, - hasOnFormDataChange: !!onFormDataChange, - columnName, - componentId: component?.id, - targetTable, - }); - if (value.length === 0) { console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음"); return; } - // 🔥 sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거) - console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", { - sourceColumns, - sourceTable, - targetTable, - sampleItem: value[0], - itemKeys: value[0] ? Object.keys(value[0]) : [], - }); - + // sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거) const filteredData = value.map((item: any) => { const filtered: Record = {}; Object.keys(item).forEach((key) => { // sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼) if (sourceColumns.includes(key)) { - console.log(` ⛔ ${key} 제외 (sourceColumn)`); return; } // 메타데이터 필드도 제외 if (key.startsWith("_")) { - console.log(` ⛔ ${key} 제외 (메타데이터)`); return; } filtered[key] = item[key]; @@ -325,12 +287,7 @@ export function ModalRepeaterTableComponent({ return filtered; }); - console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", { - filteredItemKeys: filteredData[0] ? Object.keys(filteredData[0]) : [], - sampleFilteredItem: filteredData[0], - }); - - // 🔥 targetTable 메타데이터를 배열 항목에 추가 + // targetTable 메타데이터를 배열 항목에 추가 const dataWithTargetTable = targetTable ? filteredData.map((item: any) => ({ ...item, @@ -338,21 +295,19 @@ export function ModalRepeaterTableComponent({ })) : filteredData; - // ✅ CustomEvent의 detail에 데이터 추가 + // CustomEvent의 detail에 데이터 추가 if (event instanceof CustomEvent && event.detail) { event.detail.formData[componentKey] = dataWithTargetTable; - console.log("✅ [ModalRepeaterTable] context.formData에 데이터 추가 완료:", { + console.log("✅ [ModalRepeaterTable] 저장 데이터 준비:", { key: componentKey, itemCount: dataWithTargetTable.length, - targetTable: targetTable || "미설정 (화면 설계에서 설정 필요)", - sampleItem: dataWithTargetTable[0], + targetTable: targetTable || "미설정", }); } // 기존 onFormDataChange도 호출 (호환성) if (onFormDataChange) { onFormDataChange(componentKey, dataWithTargetTable); - console.log("✅ [ModalRepeaterTable] onFormDataChange 호출 완료"); } }; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a8356721..76556ecb 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2404,18 +2404,9 @@ export const TableListComponent: React.FC = ({ - ) : (() => { - console.log("🔍 [TableList] 렌더링 조건 체크", { - groupByColumns: groupByColumns.length, - groupedDataLength: groupedData.length, - willRenderGrouped: groupByColumns.length > 0 && groupedData.length > 0, - dataLength: data.length, - }); - return groupByColumns.length > 0 && groupedData.length > 0; - })() ? ( + ) : groupByColumns.length > 0 && groupedData.length > 0 ? ( // 그룹화된 렌더링 groupedData.map((group) => { - console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count); const isCollapsed = collapsedGroups.has(group.groupKey); return ( @@ -2508,10 +2499,7 @@ export const TableListComponent: React.FC = ({ }) ) : ( // 일반 렌더링 (그룹 없음) - (() => { - console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행"); - return data; - })().map((row, index) => ( + data.map((row, index) => ( Date: Tue, 25 Nov 2025 17:32:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=EC=88=98=EC=A3=BC=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EB=82=A9=EA=B8=B0=EC=9D=BC=20DATE=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프론트엔드: EditModal에 날짜 정규화 함수 추가 (YYYY-MM-DD 형식) - 백엔드: convertValueForPostgreSQL에서 DATE 타입 문자열 그대로 유지 - 기존 TIMESTAMP 형식 변환을 DATE 타입 문자열 유지로 변경 - 날짜 변환 로직에서 YYYY-MM-DD 형식 문자열 변환 제거 closes #납기일-TIMESTAMP-형식-저장-이슈 --- .../src/services/dynamicFormService.ts | 22 +++-- frontend/components/screen/EditModal.tsx | 87 +++++++++++++++---- 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index e9485620..1ed28140 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -99,10 +99,18 @@ export class DynamicFormService { } try { - // YYYY-MM-DD 형식인 경우 시간 추가해서 Date 객체 생성 + // YYYY-MM-DD 형식인 경우 if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { - console.log(`📅 날짜 타입 변환: ${value} -> Date 객체`); - return new Date(value + "T00:00:00"); + // DATE 타입이면 문자열 그대로 유지 + if (lowerDataType === "date") { + console.log(`📅 날짜 문자열 유지: ${value} -> "${value}" (DATE 타입)`); + return value; // 문자열 그대로 반환 + } + // TIMESTAMP 타입이면 Date 객체로 변환 + else { + console.log(`📅 날짜시간 변환: ${value} -> Date 객체 (TIMESTAMP 타입)`); + return new Date(value + "T00:00:00"); + } } // 다른 날짜 형식도 Date 객체로 변환 else { @@ -300,13 +308,13 @@ export class DynamicFormService { ) { // YYYY-MM-DD HH:mm:ss 형태의 문자열을 Date 객체로 변환 if (value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) { - console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); + console.log(`📅 날짜시간 변환: ${key} = "${value}" -> Date 객체`); dataToInsert[key] = new Date(value); } - // YYYY-MM-DD 형태의 문자열을 Date 객체로 변환 + // YYYY-MM-DD 형태의 문자열은 그대로 유지 (DATE 타입으로 저장) else if (value.match(/^\d{4}-\d{2}-\d{2}$/)) { - console.log(`📅 날짜 변환: ${key} = "${value}" -> Date 객체`); - dataToInsert[key] = new Date(value + "T00:00:00"); + console.log(`📅 날짜 유지: ${key} = "${value}" -> 문자열 그대로 (DATE 타입)`); + // dataToInsert[key] = value; // 문자열 그대로 유지 (이미 올바른 형식) } } }); diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index f9b803b2..e44eef4e 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -316,6 +316,33 @@ export const EditModal: React.FC = ({ className }) => { screenId: modalState.screenId, }); + // 🆕 날짜 필드 정규화 함수 (YYYY-MM-DD 형식으로 변환) + const normalizeDateField = (value: any): string | null => { + if (!value) return null; + + // ISO 8601 형식 (2025-11-26T00:00:00.000Z) 또는 Date 객체 + if (value instanceof Date || typeof value === "string") { + try { + const date = new Date(value); + if (isNaN(date.getTime())) return null; + + // YYYY-MM-DD 형식으로 변환 + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } catch (error) { + console.warn("날짜 변환 실패:", value, error); + return null; + } + } + + return null; + }; + + // 날짜 필드 목록 + const dateFields = ["item_due_date", "delivery_date", "due_date", "order_date"]; + let insertedCount = 0; let updatedCount = 0; let deletedCount = 0; @@ -333,6 +360,17 @@ export const EditModal: React.FC = ({ className }) => { delete insertData.id; // id는 자동 생성되므로 제거 + // 🆕 날짜 필드 정규화 (YYYY-MM-DD 형식으로 변환) + dateFields.forEach((fieldName) => { + if (insertData[fieldName]) { + const normalizedDate = normalizeDateField(insertData[fieldName]); + if (normalizedDate) { + insertData[fieldName] = normalizedDate; + console.log(`📅 [날짜 정규화] ${fieldName}: ${currentData[fieldName]} → ${normalizedDate}`); + } + } + }); + // 🆕 groupByColumns의 값을 강제로 포함 (order_no 등) if (modalState.groupByColumns && modalState.groupByColumns.length > 0) { modalState.groupByColumns.forEach((colName) => { @@ -348,23 +386,32 @@ export const EditModal: React.FC = ({ className }) => { // 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등) // formData에서 품목별 필드가 아닌 공통 필드를 복사 const commonFields = [ - 'partner_id', // 거래처 - 'manager_id', // 담당자 - 'delivery_partner_id', // 납품처 - 'delivery_address', // 납품장소 - 'memo', // 메모 - 'order_date', // 주문일 - 'due_date', // 납기일 - 'shipping_method', // 배송방법 - 'status', // 상태 - 'sales_type', // 영업유형 + "partner_id", // 거래처 + "manager_id", // 담당자 + "delivery_partner_id", // 납품처 + "delivery_address", // 납품장소 + "memo", // 메모 + "order_date", // 주문일 + "due_date", // 납기일 + "shipping_method", // 배송방법 + "status", // 상태 + "sales_type", // 영업유형 ]; commonFields.forEach((fieldName) => { // formData에 값이 있으면 추가 if (formData[fieldName] !== undefined && formData[fieldName] !== null) { - insertData[fieldName] = formData[fieldName]; - console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]); + // 날짜 필드인 경우 정규화 + if (dateFields.includes(fieldName)) { + const normalizedDate = normalizeDateField(formData[fieldName]); + if (normalizedDate) { + insertData[fieldName] = normalizedDate; + console.log(`🔗 [공통 필드 - 날짜] ${fieldName} 값 추가:`, normalizedDate); + } + } else { + insertData[fieldName] = formData[fieldName]; + console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]); + } } }); @@ -404,8 +451,15 @@ export const EditModal: React.FC = ({ className }) => { } // 🆕 값 정규화 함수 (타입 통일) - const normalizeValue = (val: any): any => { + const normalizeValue = (val: any, fieldName?: string): any => { if (val === null || val === undefined || val === "") return null; + + // 날짜 필드인 경우 YYYY-MM-DD 형식으로 정규화 + if (fieldName && dateFields.includes(fieldName)) { + const normalizedDate = normalizeDateField(val); + return normalizedDate; + } + if (typeof val === "string" && !isNaN(Number(val))) { // 숫자로 변환 가능한 문자열은 숫자로 return Number(val); @@ -422,13 +476,14 @@ export const EditModal: React.FC = ({ className }) => { } // 🆕 타입 정규화 후 비교 - const currentValue = normalizeValue(currentData[key]); - const originalValue = normalizeValue(originalItemData[key]); + const currentValue = normalizeValue(currentData[key], key); + const originalValue = normalizeValue(originalItemData[key], key); // 값이 변경된 경우만 포함 if (currentValue !== originalValue) { console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`); - changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로) + // 날짜 필드는 정규화된 값 사용, 나머지는 원본 값 사용 + changedData[key] = dateFields.includes(key) ? currentValue : currentData[key]; } }); From c3872210438c9d61d82183306f88957c970d8ff7 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 26 Nov 2025 10:07:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=ED=92=88=EB=AA=A9=20=EB=82=A9?= =?UTF-8?q?=EA=B8=B0=EC=9D=BC=20=EC=9D=BC=EA=B4=84=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModalRepeaterTableComponent에 납기일 자동 일괄 적용 로직 구현 - 첫 납기일 선택 시 빈 행에 자동으로 동일 날짜 적용 - isDeliveryDateApplied 플래그로 중복 실행 방지 - ScreenModal 환경에서 onFormDataChange 경로 지원 --- .../order/OrderRegistrationModal.tsx | 44 ++++++++++++++++- .../ModalRepeaterTableComponent.tsx | 49 +++++++++++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/frontend/components/order/OrderRegistrationModal.tsx b/frontend/components/order/OrderRegistrationModal.tsx index bd780038..615f0426 100644 --- a/frontend/components/order/OrderRegistrationModal.tsx +++ b/frontend/components/order/OrderRegistrationModal.tsx @@ -64,6 +64,9 @@ export function OrderRegistrationModal({ // 선택된 품목 목록 const [selectedItems, setSelectedItems] = useState([]); + // 납기일 일괄 적용 플래그 (딱 한 번만 실행) + const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false); + // 저장 중 const [isSaving, setIsSaving] = useState(false); @@ -158,6 +161,45 @@ export function OrderRegistrationModal({ hsCode: "", }); setSelectedItems([]); + setIsDeliveryDateApplied(false); // 플래그 초기화 + }; + + // 품목 목록 변경 핸들러 (납기일 일괄 적용 로직 포함) + const handleItemsChange = (newItems: any[]) => { + // 1️⃣ 플래그가 이미 true면 그냥 업데이트만 (일괄 적용 완료 상태) + if (isDeliveryDateApplied) { + setSelectedItems(newItems); + return; + } + + // 2️⃣ 품목이 없으면 그냥 업데이트 + if (newItems.length === 0) { + setSelectedItems(newItems); + return; + } + + // 3️⃣ 현재 상태: 납기일이 있는 행과 없는 행 개수 체크 + const itemsWithDate = newItems.filter((item) => item.delivery_date); + const itemsWithoutDate = newItems.filter((item) => !item.delivery_date); + + // 4️⃣ 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용 + if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { + // 5️⃣ 전체 일괄 적용 + const selectedDate = itemsWithDate[0].delivery_date; + const updatedItems = newItems.map((item) => ({ + ...item, + delivery_date: selectedDate, // 모든 행에 동일한 납기일 적용 + })); + + setSelectedItems(updatedItems); + setIsDeliveryDateApplied(true); // 플래그 활성화 (다음부터는 일괄 적용 안 함) + + console.log("✅ 납기일 일괄 적용 완료:", selectedDate); + console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`); + } else { + // 그냥 업데이트 + setSelectedItems(newItems); + } }; // 전체 금액 계산 @@ -338,7 +380,7 @@ export function OrderRegistrationModal({ diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 91d7b36f..109c5c56 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -195,17 +195,57 @@ export function ModalRepeaterTableComponent({ const columnName = component?.columnName; const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; - // ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출) + // ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출 + 납기일 일괄 적용) const handleChange = (newData: any[]) => { + console.log("🔄 ModalRepeaterTableComponent.handleChange 호출:", { + dataLength: newData.length, + columnName, + hasExternalOnChange: !!(componentConfig?.onChange || propOnChange), + hasOnFormDataChange: !!(onFormDataChange && columnName), + }); + + // 🆕 납기일 일괄 적용 로직 (납기일 필드가 있는 경우만) + let processedData = newData; + + // 납기일 필드 찾기 (item_due_date, delivery_date, due_date 등) + const dateField = columns.find( + (col) => + col.field === "item_due_date" || + col.field === "delivery_date" || + col.field === "due_date" + ); + + if (dateField && !isDeliveryDateApplied && newData.length > 0) { + // 현재 상태: 납기일이 있는 행과 없는 행 개수 체크 + const itemsWithDate = newData.filter((item) => item[dateField.field]); + const itemsWithoutDate = newData.filter((item) => !item[dateField.field]); + + // 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용 + if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { + const selectedDate = itemsWithDate[0][dateField.field]; + processedData = newData.map((item) => ({ + ...item, + [dateField.field]: selectedDate, // 모든 행에 동일한 납기일 적용 + })); + + setIsDeliveryDateApplied(true); // 플래그 활성화 + + console.log("✅ 납기일 일괄 적용 완료:", selectedDate); + console.log(` - 대상: ${itemsWithoutDate.length}개 행에 ${selectedDate} 적용`); + } + } + // 기존 onChange 콜백 호출 (호환성) const externalOnChange = componentConfig?.onChange || propOnChange; if (externalOnChange) { - externalOnChange(newData); + console.log("📤 외부 onChange 호출"); + externalOnChange(processedData); } // 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트 if (onFormDataChange && columnName) { - onFormDataChange(columnName, newData); + console.log("📤 onFormDataChange 호출:", columnName); + onFormDataChange(columnName, processedData); } }; @@ -219,6 +259,9 @@ export function ModalRepeaterTableComponent({ const companyCode = componentConfig?.companyCode || propCompanyCode; const [modalOpen, setModalOpen] = useState(false); + // 🆕 납기일 일괄 적용 플래그 (딱 한 번만 실행) + const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false); + // columns가 비어있으면 sourceColumns로부터 자동 생성 const columns = React.useMemo((): RepeaterColumnConfig[] => { const configuredColumns = componentConfig?.columns || propColumns || []; From c0c81f20fcfdd47d50b3b6cf4f7e78d39ccd5c45 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 26 Nov 2025 10:33:42 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=ED=92=88=EB=AA=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=8B=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=BA=90=EC=8A=A4=ED=8C=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModalRepeaterTableComponent에 납기일 자동 일괄 적용 로직 구현 - 첫 납기일 선택 시 빈 행에 자동으로 동일 날짜 적용 - isDeliveryDateApplied 플래그로 중복 실행 방지 - ScreenModal 환경에서 onFormDataChange 경로 지원 - updateFormDataPartial에서 WHERE 조건의 PK 타입 동적 감지 - integer, numeric, uuid 등 다양한 타입에 대응 - ::text 하드코딩 제거하여 타입 불일치 에러 해결 --- backend-node/src/services/dynamicFormService.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 1ed28140..c40037bb 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -857,10 +857,22 @@ export class DynamicFormService { const values: any[] = Object.values(changedFields); values.push(id); // WHERE 조건용 ID 추가 + // 🔑 Primary Key 타입에 맞게 캐스팅 + const pkDataType = columnTypes[primaryKeyColumn]; + let pkCast = ''; + if (pkDataType === 'integer' || pkDataType === 'bigint' || pkDataType === 'smallint') { + pkCast = '::integer'; + } else if (pkDataType === 'numeric' || pkDataType === 'decimal') { + pkCast = '::numeric'; + } else if (pkDataType === 'uuid') { + pkCast = '::uuid'; + } + // text, varchar 등은 캐스팅 불필요 + const updateQuery = ` UPDATE ${tableName} SET ${setClause} - WHERE ${primaryKeyColumn} = $${values.length}::text + WHERE ${primaryKeyColumn} = $${values.length}${pkCast} RETURNING * `; From e4be76fe8d6f68833a9c1c35e1868d8e725206ce Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 26 Nov 2025 11:02:31 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=EC=88=98=EC=A3=BC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20=EC=9E=AC=EC=A7=88=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ModalRepeaterTableComponent의 저장 필터링 로직 개선 - columnMappings에 정의된 필드는 sourceColumns에 있어도 저장 - mappedFields 우선순위로 필터링 순서 변경 - 조인 전용 컬럼과 복사 저장 컬럼 구분 가능 --- .../12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json | 19 ------------- .../1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json | 16 ----------- .../375f2326-ca86-468a-bfc3-2d4c3825577b.json | 19 ------------- .../386e334a-df76-440c-ae8a-9bf06982fdc8.json | 16 ----------- .../3d411dc4-69a6-4236-b878-9693dff881be.json | 18 ------------ .../3e30a264-8431-44c7-96ef-eed551e66a11.json | 16 ----------- .../4a32bab5-364e-4037-bb00-31d2905824db.json | 16 ----------- .../5bfb2acd-023a-4865-a738-2900179db5fb.json | 16 ----------- .../683c1323-1895-403a-bb9a-4e111a8909f6.json | 18 ------------ .../7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json | 16 ----------- .../8990ea86-3112-4e7c-b3e0-8b494181c4e0.json | 13 --------- .../99703f2c-740c-492e-a866-a04289a9b699.json | 13 --------- .../9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json | 19 ------------- .../9d0b9fcf-cabf-4053-b6b6-6e110add22de.json | 18 ------------ .../b293e530-2b2d-4b8a-8081-d103fab5a13f.json | 18 ------------ .../cf892a77-1998-4165-bb9d-b390451465b2.json | 16 ----------- .../e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json | 13 --------- .../eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json | 16 ----------- .../fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json | 28 ------------------- .../ModalRepeaterTableComponent.tsx | 24 ++++++++++++---- 20 files changed, 19 insertions(+), 329 deletions(-) delete mode 100644 backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json delete mode 100644 backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json delete mode 100644 backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json delete mode 100644 backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json delete mode 100644 backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json delete mode 100644 backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json delete mode 100644 backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json delete mode 100644 backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json delete mode 100644 backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json delete mode 100644 backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json delete mode 100644 backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json delete mode 100644 backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json delete mode 100644 backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json delete mode 100644 backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json delete mode 100644 backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json delete mode 100644 backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json delete mode 100644 backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json delete mode 100644 backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json delete mode 100644 backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json diff --git a/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json b/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json deleted file mode 100644 index 9e7a209c..00000000 --- a/backend-node/data/mail-sent/12b583c9-a6b2-4c7f-8340-fd0e700aa32e.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "12b583c9-a6b2-4c7f-8340-fd0e700aa32e", - "sentAt": "2025-10-22T05:17:38.303Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "subject": "Fwd: ㅏㅣ", - "htmlContent": "\r\n
\r\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹㅇ리'ㅐㅔ'ㅑ678463ㅎㄱ휼췇흍츄

\r\n
\r\n

\r\n
\r\n

---------- 전달된 메시지 ----------

\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:32:34

\r\n

제목: ㅏㅣ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "<74dbd467-6185-024d-dd60-bf4459ff9ea4@wace.me>", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T06:36:10.876Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json b/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json deleted file mode 100644 index 2f624e9c..00000000 --- a/backend-node/data/mail-sent/1bb5ebfe-3f6c-4884-a043-161ae3f74f75.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "1bb5ebfe-3f6c-4884-a043-161ae3f74f75", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴㅇㄹㅇㄴㄴㄹ 테스트트트", - "htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" \n날짜: 2025. 10. 22. 오후 4:24:54\n제목: ㄴㅇㄹㅇㄴㄴㄹ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄹㅇㄴㄹㅇㄴㄹㅇㄴ\n", - "sentAt": "2025-10-22T07:49:50.811Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:49:50.811Z", - "deletedAt": "2025-10-22T07:50:14.211Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json b/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json deleted file mode 100644 index c142808d..00000000 --- a/backend-node/data/mail-sent/375f2326-ca86-468a-bfc3-2d4c3825577b.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "375f2326-ca86-468a-bfc3-2d4c3825577b", - "sentAt": "2025-10-22T04:57:39.706Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅏㅣ", - "htmlContent": "\r\n
\r\n

ㅁㄴㅇㄹㅁㅇㄴㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅁㅇㄹ

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:32:34

\r\n

제목: ㅏㅣ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:04.666Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json b/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json deleted file mode 100644 index 31da5552..00000000 --- a/backend-node/data/mail-sent/386e334a-df76-440c-ae8a-9bf06982fdc8.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "386e334a-df76-440c-ae8a-9bf06982fdc8", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\" <zian9227@naver.com>

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n

\n
\n ", - "sentAt": "2025-10-22T07:04:27.192Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:04:57.280Z", - "deletedAt": "2025-10-22T07:50:17.136Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json b/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json deleted file mode 100644 index aa107de7..00000000 --- a/backend-node/data/mail-sent/3d411dc4-69a6-4236-b878-9693dff881be.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "3d411dc4-69a6-4236-b878-9693dff881be", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㄴ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:56:51.060Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:56:51.060Z", - "deletedAt": "2025-10-22T07:50:22.989Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json b/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json deleted file mode 100644 index d824d67b..00000000 --- a/backend-node/data/mail-sent/3e30a264-8431-44c7-96ef-eed551e66a11.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "3e30a264-8431-44c7-96ef-eed551e66a11", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

\n
\n ", - "sentAt": "2025-10-22T06:57:53.335Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:00:23.394Z", - "deletedAt": "2025-10-22T07:50:20.510Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json b/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json deleted file mode 100644 index 92de4a0c..00000000 --- a/backend-node/data/mail-sent/4a32bab5-364e-4037-bb00-31d2905824db.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "4a32bab5-364e-4037-bb00-31d2905824db", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "테스트 마지가", - "htmlContent": "ㅁㄴㅇㄹ", - "sentAt": "2025-10-22T07:49:29.948Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:49:29.948Z", - "deletedAt": "2025-10-22T07:50:12.374Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json b/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json deleted file mode 100644 index 5f5a5cfc..00000000 --- a/backend-node/data/mail-sent/5bfb2acd-023a-4865-a738-2900179db5fb.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "5bfb2acd-023a-4865-a738-2900179db5fb", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

ㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n

\n
\n ", - "sentAt": "2025-10-22T07:03:09.080Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:03:39.150Z", - "deletedAt": "2025-10-22T07:50:19.035Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json b/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json deleted file mode 100644 index b3c3259f..00000000 --- a/backend-node/data/mail-sent/683c1323-1895-403a-bb9a-4e111a8909f6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "683c1323-1895-403a-bb9a-4e111a8909f6", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㄴ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 12:58:15

\n

제목:

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:54:55.097Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:54:55.097Z", - "deletedAt": "2025-10-22T07:50:24.672Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json b/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json deleted file mode 100644 index d9edbdeb..00000000 --- a/backend-node/data/mail-sent/7bed27d5-dae4-4ba8-85d0-c474c4fb907a.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "7bed27d5-dae4-4ba8-85d0-c474c4fb907a", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㅏㅣ", - "htmlContent": "\n

\n
\n

---------- 전달된 메일 ----------

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 1:32:34

\n

제목: ㅏㅣ

\n
\n undefined\n
\n ", - "sentAt": "2025-10-22T06:41:52.984Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:46:23.051Z", - "deletedAt": "2025-10-22T07:50:29.124Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json b/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json deleted file mode 100644 index f0ed2dcf..00000000 --- a/backend-node/data/mail-sent/8990ea86-3112-4e7c-b3e0-8b494181c4e0.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "8990ea86-3112-4e7c-b3e0-8b494181c4e0", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:17:31.379Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:17:31.379Z", - "deletedAt": "2025-10-22T07:50:30.736Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json b/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json deleted file mode 100644 index 1c6dc41f..00000000 --- a/backend-node/data/mail-sent/99703f2c-740c-492e-a866-a04289a9b699.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "99703f2c-740c-492e-a866-a04289a9b699", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:20:08.450Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:20:08.450Z", - "deletedAt": "2025-10-22T06:36:07.797Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json b/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json deleted file mode 100644 index 31bde67a..00000000 --- a/backend-node/data/mail-sent/9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "9ab1e5ee-4f5e-4b79-9769-5e2a1e1ffc8e", - "sentAt": "2025-10-22T04:31:17.175Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅅㄷㄴㅅ", - "htmlContent": "\r\n
\r\n

배불르고 졸린데 커피먹으니깐 졸린건 괜찮아졋고 배불러서 물배찼당아아아아

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:03:03

\r\n

제목: ㅅㄷㄴㅅ

\r\n
\r\n undefined\r\n
\r\n ", - "status": "success", - "messageId": "<0f215ba8-a1e4-8c5a-f43f-962f0717c161@wace.me>", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:10.245Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json b/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json deleted file mode 100644 index 2ace7d67..00000000 --- a/backend-node/data/mail-sent/9d0b9fcf-cabf-4053-b6b6-6e110add22de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "9d0b9fcf-cabf-4053-b6b6-6e110add22de", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: ㅏㅣ", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 22. 오후 1:32:34

\n

제목: ㅏㅣ

\n
\n

undefined

\n
\n ", - "sentAt": "2025-10-22T06:50:04.224Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:50:04.224Z", - "deletedAt": "2025-10-22T07:50:26.224Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json b/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json deleted file mode 100644 index 77d9053f..00000000 --- a/backend-node/data/mail-sent/b293e530-2b2d-4b8a-8081-d103fab5a13f.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "b293e530-2b2d-4b8a-8081-d103fab5a13f", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "zian9227@naver.com" - ], - "cc": [], - "bcc": [], - "subject": "Re: 수신메일확인용", - "htmlContent": "\n

\n
\n

원본 메일:

\n

보낸사람: \"이희진\"

\n

날짜: 2025. 10. 13. 오전 10:40:30

\n

제목: 수신메일확인용

\n
\n undefined\n
\n ", - "sentAt": "2025-10-22T06:47:53.815Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:48:53.876Z", - "deletedAt": "2025-10-22T07:50:27.706Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json b/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json deleted file mode 100644 index 426f81fb..00000000 --- a/backend-node/data/mail-sent/cf892a77-1998-4165-bb9d-b390451465b2.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "cf892a77-1998-4165-bb9d-b390451465b2", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "Fwd: ㄴ", - "htmlContent": "\n\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n전달된 메일:\n\n보낸사람: \"이희진\" \n날짜: 2025. 10. 22. 오후 12:58:15\n제목: ㄴ\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nㄴㅇㄹㄴㅇㄹㄴㅇㄹ\n", - "sentAt": "2025-10-22T07:06:11.620Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T07:07:11.749Z", - "deletedAt": "2025-10-22T07:50:15.739Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json b/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json deleted file mode 100644 index cf31f7dc..00000000 --- a/backend-node/data/mail-sent/e3501abc-cd31-4b20-bb02-3c7ddbe54eb8.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "e3501abc-cd31-4b20-bb02-3c7ddbe54eb8", - "accountName": "", - "accountEmail": "", - "to": [], - "subject": "", - "htmlContent": "", - "sentAt": "2025-10-22T06:15:02.128Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:15:02.128Z", - "deletedAt": "2025-10-22T07:08:43.543Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json b/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json deleted file mode 100644 index 0c19dc0c..00000000 --- a/backend-node/data/mail-sent/eb92ed00-cc4f-4cc8-94c9-9bef312d16db.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "eb92ed00-cc4f-4cc8-94c9-9bef312d16db", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [], - "cc": [], - "bcc": [], - "subject": "메일 임시저장 테스트 4", - "htmlContent": "asd", - "sentAt": "2025-10-22T06:21:40.019Z", - "status": "draft", - "isDraft": true, - "updatedAt": "2025-10-22T06:21:40.019Z", - "deletedAt": "2025-10-22T06:36:05.306Z" -} \ No newline at end of file diff --git a/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json b/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json deleted file mode 100644 index 073c20f0..00000000 --- a/backend-node/data/mail-sent/fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "fd2a8b41-2e6e-4e5e-b8e8-63d31efc5082", - "sentAt": "2025-10-22T04:29:14.738Z", - "accountId": "account-1759310844272", - "accountName": "이희진", - "accountEmail": "hjlee@wace.me", - "to": [ - "\"이희진\" " - ], - "subject": "Re: ㅅㄷㄴㅅ", - "htmlContent": "\r\n
\r\n

ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅇㄹㄴㅇㄹ

\r\n
\r\n

\r\n
\r\n

보낸 사람: \"이희진\"

\r\n

날짜: 2025. 10. 22. 오후 1:03:03

\r\n

제목: ㅅㄷㄴㅅ

\r\n
\r\n undefined\r\n
\r\n ", - "attachments": [ - { - "filename": "test용 이미지2.png", - "originalName": "test용 이미지2.png", - "size": 0, - "path": "/app/uploads/mail-attachments/1761107350246-298369766.png", - "mimetype": "image/png" - } - ], - "status": "success", - "messageId": "", - "accepted": [ - "zian9227@naver.com" - ], - "rejected": [], - "deletedAt": "2025-10-22T07:11:12.907Z" -} \ No newline at end of file diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 109c5c56..56f04d26 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -312,18 +312,32 @@ export function ModalRepeaterTableComponent({ } // sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거) + // 단, columnMappings에 정의된 컬럼은 저장해야 하므로 제외하지 않음 + const mappedFields = columns + .filter(col => col.mapping?.type === "source" && col.mapping?.sourceField) + .map(col => col.field); + const filteredData = value.map((item: any) => { const filtered: Record = {}; Object.keys(item).forEach((key) => { - // sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼) - if (sourceColumns.includes(key)) { - return; - } - // 메타데이터 필드도 제외 + // 메타데이터 필드 제외 if (key.startsWith("_")) { return; } + + // sourceColumns에 포함되어 있지만 columnMappings에도 정의된 경우 → 저장함 + if (mappedFields.includes(key)) { + filtered[key] = item[key]; + return; + } + + // sourceColumns에만 있고 매핑 안 된 경우 → 제외 (조인 전용) + if (sourceColumns.includes(key)) { + return; + } + + // 나머지는 모두 저장 filtered[key] = item[key]; }); From acc2a6169d86d43fc41b49ee6ffad1b182ab8e79 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 26 Nov 2025 14:05:22 +0900 Subject: [PATCH 6/6] =?UTF-8?q?style:=20EditModal=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - trailing whitespace 정리 - 들여쓰기 일관성 유지 - 그룹 편집 안내 메시지 UI 제거 --- frontend/components/screen/EditModal.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index e44eef4e..9945a19c 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -686,13 +686,6 @@ export const EditModal: React.FC = ({ className }) => { maxHeight: "100%", }} > - {/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */} - {groupData.length > 1 && ( -
- {groupData.length}개의 관련 품목을 함께 수정합니다 -
- )} - {screenData.components.map((component) => { // 컴포넌트 위치를 offset만큼 조정 const offsetX = screenDimensions?.offsetX || 0;