From 649bd77bbb8c10709d88dfb5740d77dac0eb3b36 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 27 Feb 2026 13:09:20 +0900 Subject: [PATCH] feat: Enhance dynamic form and BOM item editor functionality - Added support for updating the `updated_date` field in the DynamicFormService, ensuring accurate timestamp management. - Refactored the BomItemEditorComponent to improve data handling by filtering valid fields before saving, enhancing data integrity. - Introduced a mechanism to track existing item IDs to prevent duplicates during item addition, improving user experience and data consistency. - Streamlined the save process in ButtonActionExecutor by reorganizing the event handling logic, ensuring better integration with EditModal components. --- .../src/services/dynamicFormService.ts | 3 + bom-save-console-logs.txt | 271 ++++++++++++++++++ .../BomItemEditorComponent.tsx | 52 ++-- frontend/lib/utils/buttonActions.ts | 42 +-- 4 files changed, 315 insertions(+), 53 deletions(-) create mode 100644 bom-save-console-logs.txt diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index e1242afd..4c24e206 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -1033,6 +1033,9 @@ export class DynamicFormService { if (tableColumns.includes("updated_at")) { dataToUpdate.updated_at = new Date(); } + if (tableColumns.includes("updated_date")) { + dataToUpdate.updated_date = new Date(); + } if (tableColumns.includes("regdate") && !dataToUpdate.regdate) { dataToUpdate.regdate = new Date(); } diff --git a/bom-save-console-logs.txt b/bom-save-console-logs.txt new file mode 100644 index 00000000..f962f536 --- /dev/null +++ b/bom-save-console-logs.txt @@ -0,0 +1,271 @@ +[info] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[warning] Image with src "/images/vexplor.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. +[log] 첫 번째 접근 가능한 메뉴로 이동: /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[log] 📦 메인 테이블 데이터 자동 로드: company_mng {company_code: COMPANY_7, company_name: 탑씰 테스트, writer: wace, regdate: 2026-02-27T09:28:35.342Z, status: active} +[log] 📦 메인 테이블 데이터 자동 로드: company_mng {company_code: COMPANY_7, company_name: 탑씰 테스트, writer: wace, regdate: 2026-02-27T09:28:35.342Z, status: active} +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/138) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 1030135068124796000, error: 404} +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 666667496384701400, error: 404} +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 88591267128165600, error: 404} +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 1030135068124796000, error: 404} +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 666667496384701400, error: 404} +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[error] Failed to load resource: the server responded with a status of 404 (Not Found) +[error] ❌ 대표 이미지 로드 실패: {file: clideo_editor_cb93b93c55584c3780a53ef149e62ee5.gif, objid: 88591267128165600, error: 404} +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/138 +[info] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 0 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 0 leftGroupSumConfig: null +[log] 📦 [SplitPanelLayout] Context에 분할 패널 등록: {splitPanelId: split-panel-comp_split_panel, panelInfo: Object} +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] 🔗 [분할패널] 좌측 additionalJoinColumns: [Object, Object, Object] +[log] 🔗 [분할패널] 좌측 additionalJoinColumns: [Object, Object, Object] +[log] 📦 [SplitPanelLayout] Context에서 분할 패널 해제: split-panel-comp_split_panel +[log] 📦 [SplitPanelLayout] Context에 분할 패널 등록: {splitPanelId: split-panel-comp_split_panel, panelInfo: Object} +[log] 🔗 [분할패널] 좌측 additionalJoinColumns: [Object, Object, Object] +[log] 🔗 [분할패널] 좌측 additionalJoinColumns: [Object, Object, Object] +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] ✅ 좌측 컬럼 라벨 로드: {id: ID, created_date: 생성일시, updated_date: 수정일시, writer: 작성자, company_code: 회사코드} +[log] ✅ 좌측 컬럼 라벨 로드: {id: ID, created_date: 생성일시, updated_date: 수정일시, writer: 작성자, company_code: 회사코드} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] ✅ 좌측 카테고리 매핑 로드 [status]: {CAT_MM3XFDT6_YULY: Object, CAT_MM3XFA7B_ZFD6: Object} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] ✅ 좌측 카테고리 매핑 로드 [status]: {CAT_MM3XFDT6_YULY: Object, CAT_MM3XFA7B_ZFD6: Object} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [분할패널] API 응답 첫 번째 데이터 키: [id, created_date, updated_date, writer, company_code, bom_number, item_id, item_code, item_name, item_type, base_qty, unit, version, revision, status, effective_date, expired_date, remark, current_version_id, writer_sabun, writer_user_id, writer_user_password, writer_user_name, writer_user_name_eng, writer_user_name_cn, writer_dept_code, writer_dept_name, writer_position_code, writer_position_name, writer_email, writer_tel, writer_cell_phone, writer_user_type, writer_user_type_name, writer_regdate, writer_status, writer_end_date, writer_fax_no, writer_partner_objid, writer_photo, writer_locale, writer_data_type, writer_license_number, writer_vehicle_number, writer_signup_type, writer_branch_name, writer_department_history, writer_label, item_id_id, item_id_status, item_id_item_name, item_id_size, item_id_material, item_id_inventory_unit, item_id_weight, item_id_unit, item_id_image, item_id_division, item_id_type, item_id_meno, item_id_item_number, item_id_selling_price, item_id_standard_price, item_id_currency_code, item_id_volum, item_id_specific_gravity, item_id_user_type01, item_id_user_type02, item_id_label] +[log] 🔗 [분할패널] API 응답 첫 번째 데이터: {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [분할패널] API 응답 첫 번째 데이터 키: [id, created_date, updated_date, writer, company_code, bom_number, item_id, item_code, item_name, item_type, base_qty, unit, version, revision, status, effective_date, expired_date, remark, current_version_id, writer_sabun, writer_user_id, writer_user_password, writer_user_name, writer_user_name_eng, writer_user_name_cn, writer_dept_code, writer_dept_name, writer_position_code, writer_position_name, writer_email, writer_tel, writer_cell_phone, writer_user_type, writer_user_type_name, writer_regdate, writer_status, writer_end_date, writer_fax_no, writer_partner_objid, writer_photo, writer_locale, writer_data_type, writer_license_number, writer_vehicle_number, writer_signup_type, writer_branch_name, writer_department_history, writer_label, item_id_id, item_id_status, item_id_item_name, item_id_size, item_id_material, item_id_inventory_unit, item_id_weight, item_id_unit, item_id_image, item_id_division, item_id_type, item_id_meno, item_id_item_number, item_id_selling_price, item_id_standard_price, item_id_currency_code, item_id_volum, item_id_specific_gravity, item_id_user_type01, item_id_user_type02, item_id_label] +[log] 🔗 [분할패널] API 응답 첫 번째 데이터: {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [분할패널] API 응답 첫 번째 데이터 키: [id, created_date, updated_date, writer, company_code, bom_number, item_id, item_code, item_name, item_type, base_qty, unit, version, revision, status, effective_date, expired_date, remark, current_version_id, writer_sabun, writer_user_id, writer_user_password, writer_user_name, writer_user_name_eng, writer_user_name_cn, writer_dept_code, writer_dept_name, writer_position_code, writer_position_name, writer_email, writer_tel, writer_cell_phone, writer_user_type, writer_user_type_name, writer_regdate, writer_status, writer_end_date, writer_fax_no, writer_partner_objid, writer_photo, writer_locale, writer_data_type, writer_license_number, writer_vehicle_number, writer_signup_type, writer_branch_name, writer_department_history, writer_label, item_id_id, item_id_status, item_id_item_name, item_id_size, item_id_material, item_id_inventory_unit, item_id_weight, item_id_unit, item_id_image, item_id_division, item_id_type, item_id_meno, item_id_item_number, item_id_selling_price, item_id_standard_price, item_id_currency_code, item_id_volum, item_id_specific_gravity, item_id_user_type01, item_id_user_type02, item_id_label] +[log] 🔗 [분할패널] API 응답 첫 번째 데이터: {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [분할패널] API 응답 첫 번째 데이터 키: [id, created_date, updated_date, writer, company_code, bom_number, item_id, item_code, item_name, item_type, base_qty, unit, version, revision, status, effective_date, expired_date, remark, current_version_id, writer_sabun, writer_user_id, writer_user_password, writer_user_name, writer_user_name_eng, writer_user_name_cn, writer_dept_code, writer_dept_name, writer_position_code, writer_position_name, writer_email, writer_tel, writer_cell_phone, writer_user_type, writer_user_type_name, writer_regdate, writer_status, writer_end_date, writer_fax_no, writer_partner_objid, writer_photo, writer_locale, writer_data_type, writer_license_number, writer_vehicle_number, writer_signup_type, writer_branch_name, writer_department_history, writer_label, item_id_id, item_id_status, item_id_item_name, item_id_size, item_id_material, item_id_inventory_unit, item_id_weight, item_id_unit, item_id_image, item_id_division, item_id_type, item_id_meno, item_id_item_number, item_id_selling_price, item_id_standard_price, item_id_currency_code, item_id_volum, item_id_specific_gravity, item_id_user_type01, item_id_user_type02, item_id_label] +[log] 🔗 [분할패널] API 응답 첫 번째 데이터: {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] ✅ 분할 패널 좌측 선택: bom {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔴 [ButtonPrimary] 저장 시 formData 디버그: {propsFormDataKeys: Array(70), screenContextFormDataKeys: Array(0), effectiveFormDataKeys: Array(70), process_code: undefined, equipment_code: undefined} +[log] [BomTree] openEditModal 가로채기 - editData 보정 {oldVersion: 1.0, newVersion: 1.0, oldCurrentVersionId: de575ae5-266c-42f0-be49-bcc65de89ebd, newCurrentVersionId: de575ae5-266c-42f0-be49-bcc65de89ebd} +[log] 🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침 +[log] 🔗 [분할패널] 좌측 additionalJoinColumns: [Object, Object, Object] +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] [EditModal] 모달 열림: {mode: UPDATE (수정), hasEditData: true, editDataId: 64617576-fec9-4caa-8e72-653f9e83ba45, isCreateMode: false} +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] ⚠️ [ScreenModal] getModalStyle: screenDimensions가 null - 기본 스타일 사용 +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] [EditModal] 화면 데이터 로드 완료, 조건부 레이어 로드 시작: 4154 +[log] [EditModal] loadConditionalLayersAndZones 호출됨: 4154 +[log] [EditModal] API 호출 시작: getScreenLayers, getScreenZones +[log] [EditModal] API 응답: {layers: 1, zones: 0} +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: 초기 인증 확인: 유효한 토큰 존재 (경로: /screens/4168) | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_START: refreshUserData: API로 인증 상태 확인 시작 | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[log] 🔗 [분할패널] API 응답 첫 번째 데이터 키: [id, created_date, updated_date, writer, company_code, bom_number, item_id, item_code, item_name, item_type, base_qty, unit, version, revision, status, effective_date, expired_date, remark, current_version_id, writer_sabun, writer_user_id, writer_user_password, writer_user_name, writer_user_name_eng, writer_user_name_cn, writer_dept_code, writer_dept_name, writer_position_code, writer_position_name, writer_email, writer_tel, writer_cell_phone, writer_user_type, writer_user_type_name, writer_regdate, writer_status, writer_end_date, writer_fax_no, writer_partner_objid, writer_photo, writer_locale, writer_data_type, writer_license_number, writer_vehicle_number, writer_signup_type, writer_branch_name, writer_department_history, writer_label, item_id_id, item_id_status, item_id_item_name, item_id_size, item_id_material, item_id_inventory_unit, item_id_weight, item_id_unit, item_id_image, item_id_division, item_id_type, item_id_meno, item_id_item_number, item_id_selling_price, item_id_standard_price, item_id_currency_code, item_id_volum, item_id_specific_gravity, item_id_user_type01, item_id_user_type02, item_id_label] +[log] 🔗 [분할패널] API 응답 첫 번째 데이터: {id: 64617576-fec9-4caa-8e72-653f9e83ba45, created_date: 2026-02-27 10:22:39.485, updated_date: 2026-02-27 03:15:10.819, writer: wace, company_code: COMPANY_7} +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [그룹합산] leftGroupSumConfig: null +[log] 🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환 +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[log] 🔗 [SplitPanelLayout] Context 연결 상태: {componentId: comp_split_panel, splitPanelId: split-panel-comp_split_panel, hasRegisterFunc: true, splitPanelsSize: 0} +[log] 🔍 [SplitPanel] 왼쪽 패널 displayMode: table isDesignMode: false +[log] 🔍 [테이블모드 렌더링] dataSource 개수: 9 leftGroupSumConfig: null +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 +[debug] [AuthLog] AUTH_CHECK_SUCCESS: 사용자: topseal_admin, 인증: true | 토큰: 유효(24h0m 남음, user:topseal_admin) | /screens/4168 \ No newline at end of file diff --git a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx index 75c1909b..fcb7b710 100644 --- a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx @@ -812,7 +812,7 @@ export function BomItemEditorComponent({ : null; if (node._isNew) { - const payload: Record = { + const raw: Record = { ...node.data, [fkColumn]: bomId, [parentKeyColumn]: realParentId, @@ -821,10 +821,16 @@ export function BomItemEditorComponent({ company_code: companyCode || undefined, version_id: saveVersionId || undefined, }; - delete payload.id; - delete payload.tempId; - delete payload._isNew; - delete payload._isDeleted; + // bom_detail에 유효한 필드만 남기기 (item_info 조인 필드 제거) + const payload: Record = {}; + const validKeys = new Set([ + fkColumn, parentKeyColumn, "seq_no", "level", "child_item_id", + "quantity", "unit", "loss_rate", "remark", "process_type", + "base_qty", "revision", "version_id", "company_code", "writer", + ]); + Object.keys(raw).forEach((k) => { + if (validKeys.has(k)) payload[k] = raw[k]; + }); const resp = await apiClient.post( `/table-management/tables/${mainTableName}/add`, @@ -835,17 +841,14 @@ export function BomItemEditorComponent({ savedCount++; } else if (node.id) { const updatedData: Record = { - ...node.data, id: node.id, + [fkColumn]: bomId, [parentKeyColumn]: realParentId, seq_no: String(seqNo), level: String(level), }; - delete updatedData.tempId; - delete updatedData._isNew; - delete updatedData._isDeleted; - Object.keys(updatedData).forEach((k) => { - if (k.startsWith(`${sourceFk}_`)) delete updatedData[k]; + ["quantity", "unit", "loss_rate", "remark", "process_type", "base_qty", "revision", "child_item_id", "version_id", "company_code"].forEach((k) => { + if (node.data[k] !== undefined) updatedData[k] = node.data[k]; }); await apiClient.put( @@ -934,6 +937,20 @@ export function BomItemEditorComponent({ setItemSearchOpen(true); }, []); + // 이미 추가된 품목 ID 목록 (중복 방지용) + const existingItemIds = useMemo(() => { + const ids = new Set(); + const collect = (nodes: BomItemNode[]) => { + for (const n of nodes) { + const fk = n.data[cfg.dataSource?.foreignKey || "child_item_id"]; + if (fk) ids.add(fk); + collect(n.children); + } + }; + collect(treeData); + return ids; + }, [treeData, cfg]); + // 루트 품목 추가 시작 const handleAddRoot = useCallback(() => { setAddTargetParentId(null); @@ -1353,18 +1370,7 @@ export function BomItemEditorComponent({ onClose={() => setItemSearchOpen(false)} onSelect={handleItemSelect} companyCode={companyCode} - existingItemIds={useMemo(() => { - const ids = new Set(); - const collect = (nodes: BomItemNode[]) => { - for (const n of nodes) { - const fk = n.data[cfg.dataSource?.foreignKey || "child_item_id"]; - if (fk) ids.add(fk); - collect(n.children); - } - }; - collect(treeData); - return ids; - }, [treeData, cfg])} + existingItemIds={existingItemIds} /> ); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 7f8514ab..054b257f 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -558,31 +558,7 @@ export class ButtonActionExecutor { return false; } - // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 - // EditModal이 내부에서 직접 repeaterSave 이벤트를 발행하고 완료를 기다림 - if (onSave) { - try { - await onSave(); - return true; - } catch (error) { - console.error("❌ [handleSave] onSave 콜백 실행 오류:", error); - throw error; - } - } - - console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행"); - - // 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집) - // context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함 - // skipDefaultSave 플래그를 통해 기본 저장 로직을 건너뛸 수 있음 - - // 🔧 디버그: beforeFormSave 이벤트 전 formData 확인 - console.log("🔍 [handleSave] beforeFormSave 이벤트 전:", { - keys: Object.keys(context.formData || {}), - hasCompanyImage: "company_image" in (context.formData || {}), - companyImageValue: context.formData?.company_image, - }); - + // beforeFormSave 이벤트 발송 (BomItemEditor 등 서브 컴포넌트의 저장 처리) const beforeSaveEventDetail = { formData: context.formData, skipDefaultSave: false, @@ -596,22 +572,28 @@ export class ButtonActionExecutor { }), ); - // 비동기 핸들러가 등록한 Promise들 대기 + 동기 핸들러를 위한 최소 대기 if (beforeSaveEventDetail.pendingPromises.length > 0) { - console.log( - `[handleSave] 비동기 beforeFormSave 핸들러 ${beforeSaveEventDetail.pendingPromises.length}건 대기 중...`, - ); await Promise.all(beforeSaveEventDetail.pendingPromises); } else { await new Promise((resolve) => setTimeout(resolve, 100)); } - // 검증 실패 시 저장 중단 if (beforeSaveEventDetail.validationFailed) { console.log("❌ [handleSave] 검증 실패로 저장 중단:", beforeSaveEventDetail.validationErrors); return false; } + // EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 + if (onSave) { + try { + await onSave(); + return true; + } catch (error) { + console.error("❌ [handleSave] onSave 콜백 실행 오류:", error); + throw error; + } + } + // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 // 단, _tableSection_ 데이터가 있으면 건너뛰기 (handleUniversalFormModalTableSectionSave가 처리) // EditModal이 내부에서 직접 repeaterSave 이벤트를 발행하고 완료를 기다림