Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
commit
b1831ada04
|
|
@ -59,7 +59,10 @@ export async function getBomHeader(bomId: string, tableName?: string) {
|
||||||
const table = safeTableName(tableName || "", "bom");
|
const table = safeTableName(tableName || "", "bom");
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT b.*,
|
SELECT b.*,
|
||||||
i.item_name, i.item_number, i.division as item_type, i.unit
|
i.item_name, i.item_number, i.division as item_type,
|
||||||
|
COALESCE(b.unit, i.unit) as unit,
|
||||||
|
i.unit as item_unit,
|
||||||
|
i.division, i.size, i.material
|
||||||
FROM ${table} b
|
FROM ${table} b
|
||||||
LEFT JOIN item_info i ON b.item_id = i.id
|
LEFT JOIN item_info i ON b.item_id = i.id
|
||||||
WHERE b.id = $1
|
WHERE b.id = $1
|
||||||
|
|
|
||||||
|
|
@ -1082,6 +1082,9 @@ export class DynamicFormService {
|
||||||
if (tableColumns.includes("updated_at")) {
|
if (tableColumns.includes("updated_at")) {
|
||||||
dataToUpdate.updated_at = new Date();
|
dataToUpdate.updated_at = new Date();
|
||||||
}
|
}
|
||||||
|
if (tableColumns.includes("updated_date")) {
|
||||||
|
dataToUpdate.updated_date = new Date();
|
||||||
|
}
|
||||||
if (tableColumns.includes("regdate") && !dataToUpdate.regdate) {
|
if (tableColumns.includes("regdate") && !dataToUpdate.regdate) {
|
||||||
dataToUpdate.regdate = new Date();
|
dataToUpdate.regdate = new Date();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -275,7 +275,26 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 편집 데이터로 폼 데이터 초기화
|
// 편집 데이터로 폼 데이터 초기화
|
||||||
setFormData(editData || {});
|
// entity join 필드(xxx_yyy)를 dot notation(table.column)으로도 매핑
|
||||||
|
const enriched = { ...(editData || {}) };
|
||||||
|
if (editData) {
|
||||||
|
Object.keys(editData).forEach((key) => {
|
||||||
|
// item_id_item_name → item_info.item_name 패턴 변환
|
||||||
|
const match = key.match(/^(.+?)_([a-z_]+)$/);
|
||||||
|
if (match && editData[key] != null) {
|
||||||
|
const [, fkCol, fieldName] = match;
|
||||||
|
// FK가 _id로 끝나면 참조 테이블명 추론 (item_id → item_info)
|
||||||
|
if (fkCol.endsWith("_id")) {
|
||||||
|
const refTable = fkCol.replace(/_id$/, "_info");
|
||||||
|
const dotKey = `${refTable}.${fieldName}`;
|
||||||
|
if (!(dotKey in enriched)) {
|
||||||
|
enriched[dotKey] = editData[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setFormData(enriched);
|
||||||
// originalData: changedData 계산(PATCH)에만 사용
|
// originalData: changedData 계산(PATCH)에만 사용
|
||||||
// INSERT/UPDATE 판단에는 사용하지 않음
|
// INSERT/UPDATE 판단에는 사용하지 않음
|
||||||
setOriginalData(isCreateMode ? {} : editData || {});
|
setOriginalData(isCreateMode ? {} : editData || {});
|
||||||
|
|
|
||||||
|
|
@ -245,23 +245,29 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨 렌더링
|
// 라벨 렌더링
|
||||||
|
const labelPos = widget.style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
|
|
||||||
const renderLabel = () => {
|
const renderLabel = () => {
|
||||||
if (hideLabel) return null;
|
if (hideLabel) return null;
|
||||||
|
|
||||||
const labelStyle = widget.style || {};
|
const ls = widget.style || {};
|
||||||
const labelElement = (
|
const labelElement = (
|
||||||
<label
|
<label
|
||||||
className={`mb-2 block text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${hasError ? "text-destructive" : ""}`}
|
className={`text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${hasError ? "text-destructive" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: labelStyle.labelFontSize || "14px",
|
fontSize: ls.labelFontSize || "14px",
|
||||||
color: hasError ? "hsl(var(--destructive))" : labelStyle.labelColor || undefined,
|
color: hasError ? "hsl(var(--destructive))" : ls.labelColor || undefined,
|
||||||
fontWeight: labelStyle.labelFontWeight || "500",
|
fontWeight: ls.labelFontWeight || "500",
|
||||||
fontFamily: labelStyle.labelFontFamily,
|
fontFamily: ls.labelFontFamily,
|
||||||
textAlign: labelStyle.labelTextAlign || "left",
|
textAlign: ls.labelTextAlign || "left",
|
||||||
backgroundColor: labelStyle.labelBackgroundColor,
|
backgroundColor: ls.labelBackgroundColor,
|
||||||
padding: labelStyle.labelPadding,
|
padding: ls.labelPadding,
|
||||||
borderRadius: labelStyle.labelBorderRadius,
|
borderRadius: ls.labelBorderRadius,
|
||||||
marginBottom: labelStyle.labelMarginBottom || "8px",
|
...(isHorizLabel
|
||||||
|
? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" }
|
||||||
|
: { marginBottom: labelPos === "top" ? (ls.labelMarginBottom || "8px") : undefined,
|
||||||
|
marginTop: labelPos === "bottom" ? (ls.labelMarginBottom || "8px") : undefined }),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{widget.label}
|
{widget.label}
|
||||||
|
|
@ -332,11 +338,28 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelElement = renderLabel();
|
||||||
|
const widgetElement = renderByWebType();
|
||||||
|
const validationElement = renderFieldValidation();
|
||||||
|
|
||||||
|
if (isHorizLabel && labelElement) {
|
||||||
|
return (
|
||||||
|
<div key={comp.id}>
|
||||||
|
<div style={{ display: "flex", flexDirection: labelPos === "left" ? "row" : "row-reverse", alignItems: "center", gap: widget.style?.labelGap || "8px" }}>
|
||||||
|
{labelElement}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>{widgetElement}</div>
|
||||||
|
</div>
|
||||||
|
{validationElement}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={comp.id} className="space-y-2">
|
<div key={comp.id}>
|
||||||
{renderLabel()}
|
{labelPos === "top" && labelElement}
|
||||||
{renderByWebType()}
|
{widgetElement}
|
||||||
{renderFieldValidation()}
|
{labelPos === "bottom" && labelElement}
|
||||||
|
{validationElement}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2208,15 +2208,21 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 스타일 적용
|
// 라벨 위치 및 스타일
|
||||||
const labelStyle = {
|
const labelPosition = component.style?.labelPosition || "top";
|
||||||
|
const isHorizontalLabel = labelPosition === "left" || labelPosition === "right";
|
||||||
|
const labelGap = component.style?.labelGap || "8px";
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#212121",
|
color: component.style?.labelColor || "#212121",
|
||||||
fontWeight: component.style?.labelFontWeight || "500",
|
fontWeight: component.style?.labelFontWeight || "500",
|
||||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||||
padding: component.style?.labelPadding || "0",
|
padding: component.style?.labelPadding || "0",
|
||||||
borderRadius: component.style?.labelBorderRadius || "0",
|
borderRadius: component.style?.labelBorderRadius || "0",
|
||||||
marginBottom: component.style?.labelMarginBottom || "4px",
|
...(isHorizontalLabel
|
||||||
|
? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" }
|
||||||
|
: { marginBottom: component.style?.labelMarginBottom || "4px" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2452,18 +2458,45 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
{/* 테이블 옵션 툴바 */}
|
{/* 테이블 옵션 툴바 */}
|
||||||
<TableOptionsToolbar />
|
<TableOptionsToolbar />
|
||||||
|
|
||||||
{/* 메인 컨텐츠 */}
|
{/* 메인 컨텐츠 - 라벨 위치에 따라 flex 방향 변경 */}
|
||||||
<div className="h-full flex-1" style={{ width: '100%' }}>
|
<div
|
||||||
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
|
className="h-full flex-1"
|
||||||
{shouldShowLabel && (
|
style={{
|
||||||
<label className="mb-2 block text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
width: '100%',
|
||||||
|
...(shouldShowLabel && isHorizontalLabel
|
||||||
|
? { display: 'flex', flexDirection: labelPosition === 'left' ? 'row' : 'row-reverse', alignItems: 'center', gap: labelGap }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 라벨: top 또는 left일 때 위젯보다 먼저 렌더링 */}
|
||||||
|
{shouldShowLabel && (labelPosition === "top" || labelPosition === "left") && (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
style={labelStyle}
|
||||||
|
>
|
||||||
{labelText}
|
{labelText}
|
||||||
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */}
|
{/* 실제 위젯 */}
|
||||||
<div className="h-full" style={{ width: '100%', height: '100%' }}>{renderInteractiveWidget(componentForRendering)}</div>
|
<div className="h-full" style={{ width: '100%', height: '100%', ...(isHorizontalLabel ? { flex: 1, minWidth: 0 } : {}) }}>
|
||||||
|
{renderInteractiveWidget(componentForRendering)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 라벨: bottom 또는 right일 때 위젯 뒤에 렌더링 */}
|
||||||
|
{shouldShowLabel && (labelPosition === "bottom" || labelPosition === "right") && (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
style={{
|
||||||
|
...labelStyle,
|
||||||
|
...(labelPosition === "bottom" ? { marginBottom: 0, marginTop: component.style?.labelMarginBottom || "4px" } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelText}
|
||||||
|
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1103,17 +1103,21 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
// TableSearchWidget의 경우 높이를 자동으로 설정
|
// TableSearchWidget의 경우 높이를 자동으로 설정
|
||||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||||
|
|
||||||
// 🆕 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
// 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
||||||
// labelDisplay가 false가 아니고, labelText 또는 label이 있으면 라벨 표시
|
const compType = (component as any).componentType || "";
|
||||||
const isV2InputComponent = type === "v2-input" || type === "v2-select" || type === "v2-date";
|
const isV2InputComponent =
|
||||||
|
type === "v2-input" || type === "v2-select" || type === "v2-date" ||
|
||||||
|
compType === "v2-input" || compType === "v2-select" || compType === "v2-date";
|
||||||
const hasVisibleLabel = isV2InputComponent &&
|
const hasVisibleLabel = isV2InputComponent &&
|
||||||
style?.labelDisplay !== false &&
|
style?.labelDisplay !== false &&
|
||||||
(style?.labelText || (component as any).label);
|
(style?.labelText || (component as any).label);
|
||||||
|
|
||||||
// 라벨이 있는 경우 상단 여백 계산 (라벨 폰트크기 + 여백)
|
// 라벨 위치에 따라 오프셋 계산 (좌/우 배치 시 세로 오프셋 불필요)
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isVerticalLabel = labelPos === "top" || labelPos === "bottom";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const labelOffset = hasVisibleLabel ? (labelFontSize + labelMarginBottom + 2) : 0;
|
const labelOffset = (hasVisibleLabel && isVerticalLabel) ? (labelFontSize + labelMarginBottom + 2) : 0;
|
||||||
|
|
||||||
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
||||||
const compType = (component as any).componentType || "";
|
const compType = (component as any).componentType || "";
|
||||||
|
|
@ -1263,10 +1267,56 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [component.id, position?.x, size?.width, type]);
|
}, [component.id, position?.x, size?.width, type]);
|
||||||
|
|
||||||
|
// 라벨 위치가 top이 아닌 경우: 외부에서 라벨을 렌더링하고 내부 라벨은 숨김
|
||||||
|
const needsExternalLabel = hasVisibleLabel && labelPos !== "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
|
const labelText = style?.labelText || (component as any).label || "";
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
|
const externalLabelComponent = needsExternalLabel ? (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none"
|
||||||
|
style={{
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#212121",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
...(isHorizLabel ? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" } : {}),
|
||||||
|
...(labelPos === "bottom" ? { marginTop: style?.labelMarginBottom || "4px" } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelText}
|
||||||
|
{((component as any).required || (component as any).componentConfig?.required) && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const componentToRender = needsExternalLabel
|
||||||
|
? { ...splitAdjustedComponent, style: { ...splitAdjustedComponent.style, labelDisplay: false } }
|
||||||
|
: splitAdjustedComponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={elRef} id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
<div ref={elRef} id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
||||||
{renderInteractiveWidget(splitAdjustedComponent)}
|
{needsExternalLabel ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: isHorizLabel ? (labelPos === "left" ? "row" : "row-reverse") : "column-reverse",
|
||||||
|
alignItems: isHorizLabel ? "center" : undefined,
|
||||||
|
gap: isHorizLabel ? labelGapValue : undefined,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{externalLabelComponent}
|
||||||
|
<div style={{ flex: 1, minWidth: 0, height: isHorizLabel ? "100%" : undefined }}>
|
||||||
|
{renderInteractiveWidget(componentToRender)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderInteractiveWidget(componentToRender)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 팝업 화면 렌더링 */}
|
{/* 팝업 화면 렌더링 */}
|
||||||
|
|
|
||||||
|
|
@ -841,6 +841,44 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">위치</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedComponent.style?.labelPosition || "top"}
|
||||||
|
onValueChange={(value) => handleUpdate("style.labelPosition", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="top">위</SelectItem>
|
||||||
|
<SelectItem value="bottom">아래</SelectItem>
|
||||||
|
<SelectItem value="left">왼쪽</SelectItem>
|
||||||
|
<SelectItem value="right">오른쪽</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">간격</Label>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
(selectedComponent.style?.labelPosition === "left" || selectedComponent.style?.labelPosition === "right")
|
||||||
|
? (selectedComponent.style?.labelGap || "8px")
|
||||||
|
: (selectedComponent.style?.labelMarginBottom || "4px")
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const pos = selectedComponent.style?.labelPosition;
|
||||||
|
if (pos === "left" || pos === "right") {
|
||||||
|
handleUpdate("style.labelGap", e.target.value);
|
||||||
|
} else {
|
||||||
|
handleUpdate("style.labelMarginBottom", e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">크기</Label>
|
<Label className="text-xs">크기</Label>
|
||||||
|
|
@ -862,12 +900,21 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">여백</Label>
|
<Label className="text-xs">굵기</Label>
|
||||||
<Input
|
<Select
|
||||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
value={selectedComponent.style?.labelFontWeight || "500"}
|
||||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
onValueChange={(value) => handleUpdate("style.labelFontWeight", value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
>
|
||||||
/>
|
<SelectTrigger className="h-6 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="400">보통</SelectItem>
|
||||||
|
<SelectItem value="500">중간</SelectItem>
|
||||||
|
<SelectItem value="600">굵게</SelectItem>
|
||||||
|
<SelectItem value="700">매우 굵게</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-5">
|
<div className="flex items-center space-x-2 pt-5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -704,10 +704,56 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const dateContent = (
|
||||||
|
<div className={isHorizLabel ? "min-w-0 flex-1" : "h-full w-full"} style={isHorizLabel ? { height: "100%" } : undefined}>
|
||||||
|
{renderDatePicker()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{dateContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -719,27 +765,8 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
|
{labelElement}
|
||||||
{showLabel && (
|
{dateContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div className="h-full w-full">
|
|
||||||
{renderDatePicker()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -961,36 +961,83 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨이 표시될 때 입력 필드가 차지할 높이 계산
|
|
||||||
// 🔧 label prop이 없어도 style.labelText에서 가져올 수 있도록 수정
|
|
||||||
const actualLabel = label || style?.labelText;
|
const actualLabel = label || style?.labelText;
|
||||||
const showLabel = actualLabel && style?.labelDisplay === true;
|
const showLabel = actualLabel && style?.labelDisplay === true;
|
||||||
// size에서 우선 가져오고, 없으면 style에서 가져옴
|
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; // 라벨 높이 + 여백
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
// 커스텀 스타일 감지
|
||||||
// RealtimePreview 래퍼가 외부 div에 스타일을 적용하지만,
|
|
||||||
// 내부 input/textarea가 자체 Tailwind 테두리를 가지므로 이를 제거하여 외부 스타일이 보이도록 함
|
|
||||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||||
const hasCustomBackground = !!style?.backgroundColor;
|
const hasCustomBackground = !!style?.backgroundColor;
|
||||||
const hasCustomRadius = !!style?.borderRadius;
|
const hasCustomRadius = !!style?.borderRadius;
|
||||||
|
|
||||||
// 텍스트 스타일 오버라이드 (내부 input/textarea에 직접 전달)
|
|
||||||
const customTextStyle: React.CSSProperties = {};
|
const customTextStyle: React.CSSProperties = {};
|
||||||
if (style?.color) customTextStyle.color = style.color;
|
if (style?.color) customTextStyle.color = style.color;
|
||||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||||
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
||||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||||
// 내부 input에 직접 적용할 텍스트 스타일 (fontSize, color, fontWeight, textAlign)
|
|
||||||
const inputTextStyle: React.CSSProperties | undefined = hasCustomText ? customTextStyle : undefined;
|
const inputTextStyle: React.CSSProperties | undefined = hasCustomText ? customTextStyle : undefined;
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{actualLabel}
|
||||||
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const inputContent = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isHorizLabel ? "min-w-0 flex-1" : "h-full w-full",
|
||||||
|
hasCustomBorder && "[&_input]:border-0! [&_textarea]:border-0! [&_.border]:border-0!",
|
||||||
|
(hasCustomBorder || hasCustomRadius) && "[&_input]:rounded-none! [&_textarea]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||||
|
hasCustomBackground && "[&_input]:bg-transparent! [&_textarea]:bg-transparent!",
|
||||||
|
)}
|
||||||
|
style={{ ...(hasCustomText ? customTextStyle : {}), ...(isHorizLabel ? { height: "100%" } : {}) }}
|
||||||
|
>
|
||||||
|
{renderInput()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{inputContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -1001,38 +1048,8 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 (높이에 포함되지 않음) */}
|
{labelElement}
|
||||||
{showLabel && (
|
{inputContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{actualLabel}
|
|
||||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full w-full",
|
|
||||||
// 커스텀 테두리 설정 시, 내부 input/textarea의 기본 테두리 제거 (외부 래퍼 스타일이 보이도록)
|
|
||||||
hasCustomBorder && "[&_input]:border-0! [&_textarea]:border-0! [&_.border]:border-0!",
|
|
||||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거 (외부 래퍼가 처리)
|
|
||||||
(hasCustomBorder || hasCustomRadius) && "[&_input]:rounded-none! [&_textarea]:rounded-none! [&_.rounded-md]:rounded-none!",
|
|
||||||
// 커스텀 배경 설정 시, 내부 input을 투명하게 (외부 배경이 보이도록)
|
|
||||||
hasCustomBackground && "[&_input]:bg-transparent! [&_textarea]:bg-transparent!",
|
|
||||||
)}
|
|
||||||
style={hasCustomText ? customTextStyle : undefined}
|
|
||||||
>
|
|
||||||
{renderInput()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -892,6 +892,42 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
loadOptions();
|
loadOptions();
|
||||||
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue, resolvedFiltersJson]);
|
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue, resolvedFiltersJson]);
|
||||||
|
|
||||||
|
// 레거시 평문값 → 카테고리 코드 자동 정규화 (한글 텍스트로 저장된 데이터 대응)
|
||||||
|
const resolvedValue = useMemo(() => {
|
||||||
|
if (!value || options.length === 0) return value;
|
||||||
|
|
||||||
|
const resolveOne = (v: string): string => {
|
||||||
|
if (options.some(o => o.value === v)) return v;
|
||||||
|
const trimmed = v.trim();
|
||||||
|
const match = options.find(o => {
|
||||||
|
const cleanLabel = o.label.replace(/^[\s└]+/, '').trim();
|
||||||
|
return cleanLabel === trimmed;
|
||||||
|
});
|
||||||
|
return match ? match.value : v;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const resolved = value.map(resolveOne);
|
||||||
|
return resolved.every((v, i) => v === value[i]) ? value : resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 콤마 구분 복합값 처리 (e.g., "구매품,판매품,CAT_xxx")
|
||||||
|
if (typeof value === "string" && value.includes(",")) {
|
||||||
|
const parts = value.split(",");
|
||||||
|
const resolved = parts.map(p => resolveOne(p.trim()));
|
||||||
|
const result = resolved.join(",");
|
||||||
|
return result === value ? value : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveOne(value);
|
||||||
|
}, [value, options]);
|
||||||
|
|
||||||
|
// 정규화 결과가 원본과 다르면 onChange로 자동 업데이트 (저장 시 코드 변환)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onChange || options.length === 0 || !value || value === resolvedValue) return;
|
||||||
|
onChange(resolvedValue as string | string[]);
|
||||||
|
}, [resolvedValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// 같은 폼에서 참조 테이블(entityTable) 컬럼을 사용하는 다른 컴포넌트 자동 감지
|
// 같은 폼에서 참조 테이블(entityTable) 컬럼을 사용하는 다른 컴포넌트 자동 감지
|
||||||
const autoFillTargets = useMemo(() => {
|
const autoFillTargets = useMemo(() => {
|
||||||
if (source !== "entity" || !entityTable || !allComponents) return [];
|
if (source !== "entity" || !entityTable || !allComponents) return [];
|
||||||
|
|
@ -1017,7 +1053,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={resolvedValue}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
placeholder="선택"
|
placeholder="선택"
|
||||||
searchable={config.mode === "combobox" ? true : config.searchable}
|
searchable={config.mode === "combobox" ? true : config.searchable}
|
||||||
|
|
@ -1033,7 +1069,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<RadioSelect
|
<RadioSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={typeof value === "string" ? value : value?.[0]}
|
value={typeof resolvedValue === "string" ? resolvedValue : resolvedValue?.[0]}
|
||||||
onChange={(v) => handleChangeWithAutoFill(v)}
|
onChange={(v) => handleChangeWithAutoFill(v)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1044,7 +1080,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<CheckSelect
|
<CheckSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
maxSelect={config.maxSelect}
|
maxSelect={config.maxSelect}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|
@ -1055,7 +1091,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<TagSelect
|
<TagSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
maxSelect={config.maxSelect}
|
maxSelect={config.maxSelect}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|
@ -1066,7 +1102,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<TagboxSelect
|
<TagboxSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
placeholder={config.placeholder || "선택하세요"}
|
placeholder={config.placeholder || "선택하세요"}
|
||||||
maxSelect={config.maxSelect}
|
maxSelect={config.maxSelect}
|
||||||
|
|
@ -1079,7 +1115,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<ToggleSelect
|
<ToggleSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={typeof value === "string" ? value : value?.[0]}
|
value={typeof resolvedValue === "string" ? resolvedValue : resolvedValue?.[0]}
|
||||||
onChange={(v) => handleChangeWithAutoFill(v)}
|
onChange={(v) => handleChangeWithAutoFill(v)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1089,7 +1125,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<SwapSelect
|
<SwapSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={Array.isArray(value) ? value : value ? [value] : []}
|
value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
maxSelect={config.maxSelect}
|
maxSelect={config.maxSelect}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|
@ -1100,7 +1136,7 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
return (
|
return (
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={resolvedValue}
|
||||||
onChange={handleChangeWithAutoFill}
|
onChange={handleChangeWithAutoFill}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
style={heightStyle}
|
style={heightStyle}
|
||||||
|
|
@ -1113,17 +1149,19 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
// 커스텀 스타일 감지
|
||||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||||
const hasCustomBackground = !!style?.backgroundColor;
|
const hasCustomBackground = !!style?.backgroundColor;
|
||||||
const hasCustomRadius = !!style?.borderRadius;
|
const hasCustomRadius = !!style?.borderRadius;
|
||||||
|
|
||||||
// 텍스트 스타일 오버라이드 (CSS 상속)
|
|
||||||
const customTextStyle: React.CSSProperties = {};
|
const customTextStyle: React.CSSProperties = {};
|
||||||
if (style?.color) customTextStyle.color = style.color;
|
if (style?.color) customTextStyle.color = style.color;
|
||||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||||
|
|
@ -1131,6 +1169,58 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const selectContent = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isHorizLabel ? "min-w-0 flex-1" : "h-full w-full",
|
||||||
|
hasCustomBorder && "[&_button]:border-0! **:data-[slot=select-trigger]:border-0! [&_.border]:border-0!",
|
||||||
|
(hasCustomBorder || hasCustomRadius) && "[&_button]:rounded-none! **:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||||
|
hasCustomBackground && "[&_button]:bg-transparent! **:data-[slot=select-trigger]:bg-transparent!",
|
||||||
|
)}
|
||||||
|
style={{ ...(hasCustomText ? customTextStyle : {}), ...(isHorizLabel ? { height: "100%" } : {}) }}
|
||||||
|
>
|
||||||
|
{renderSelect()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
className={cn(isDesignMode && "pointer-events-none")}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{selectContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -1141,38 +1231,8 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
|
{labelElement}
|
||||||
{showLabel && (
|
{selectContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full w-full",
|
|
||||||
// 커스텀 테두리 설정 시, 내부 select trigger의 기본 테두리 제거
|
|
||||||
hasCustomBorder && "[&_button]:border-0! **:data-[slot=select-trigger]:border-0! [&_.border]:border-0!",
|
|
||||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거
|
|
||||||
(hasCustomBorder || hasCustomRadius) && "[&_button]:rounded-none! **:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none!",
|
|
||||||
// 커스텀 배경 설정 시, 내부 요소를 투명하게
|
|
||||||
hasCustomBackground && "[&_button]:bg-transparent! **:data-[slot=select-trigger]:bg-transparent!",
|
|
||||||
)}
|
|
||||||
style={hasCustomText ? customTextStyle : undefined}
|
|
||||||
>
|
|
||||||
{renderSelect()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -568,6 +568,12 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
if (hasParentMapping) {
|
if (hasParentMapping) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// 수정 모드 감지 (parentKeys 구성 전에 필요)
|
||||||
|
const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
|
||||||
|
const urlEditMode = urlParams?.get("mode") === "edit";
|
||||||
|
const dataHasDbId = items.some(item => !!item.originalData?.id);
|
||||||
|
const isEditMode = urlEditMode || dataHasDbId;
|
||||||
|
|
||||||
// 부모 키 추출 (parentDataMapping에서)
|
// 부모 키 추출 (parentDataMapping에서)
|
||||||
const parentKeys: Record<string, any> = {};
|
const parentKeys: Record<string, any> = {};
|
||||||
|
|
||||||
|
|
@ -581,16 +587,25 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
}
|
}
|
||||||
|
|
||||||
componentConfig.parentDataMapping.forEach((mapping) => {
|
componentConfig.parentDataMapping.forEach((mapping) => {
|
||||||
// 1차: formData(sourceData)에서 찾기
|
let value: any;
|
||||||
let value = getFieldValue(sourceData, mapping.sourceField);
|
|
||||||
|
|
||||||
// 2차: formData에 없으면 dataRegistry[sourceTable]에서 찾기
|
// 수정 모드: originalData의 targetField 값 우선 사용
|
||||||
// v2-split-panel-layout에서 좌측 항목 선택 시 dataRegistry에 저장한 데이터 활용
|
// 로드(editFilters)와 동일한 방식으로 FK 값을 가져와야
|
||||||
if ((value === undefined || value === null) && mapping.sourceTable) {
|
// 백엔드에서 기존 레코드를 정확히 매칭하여 UPDATE 수행 가능
|
||||||
const registryData = dataRegistry[mapping.sourceTable];
|
if (isEditMode && items.length > 0 && items[0].originalData) {
|
||||||
if (registryData && registryData.length > 0) {
|
value = items[0].originalData[mapping.targetField];
|
||||||
const registryItem = registryData[0].originalData || registryData[0];
|
}
|
||||||
value = registryItem[mapping.sourceField];
|
|
||||||
|
// 신규 모드 또는 originalData에 값 없으면 기존 로직
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = getFieldValue(sourceData, mapping.sourceField);
|
||||||
|
|
||||||
|
if ((value === undefined || value === null) && mapping.sourceTable) {
|
||||||
|
const registryData = dataRegistry[mapping.sourceTable];
|
||||||
|
if (registryData && registryData.length > 0) {
|
||||||
|
const registryItem = registryData[0].originalData || registryData[0];
|
||||||
|
value = registryItem[mapping.sourceField];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -646,15 +661,6 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
const additionalFields = componentConfig.additionalFields || [];
|
const additionalFields = componentConfig.additionalFields || [];
|
||||||
const mainTable = componentConfig.targetTable!;
|
const mainTable = componentConfig.targetTable!;
|
||||||
|
|
||||||
// 수정 모드 감지 (2가지 방법으로 확인)
|
|
||||||
// 1. URL에 mode=edit 파라미터 확인
|
|
||||||
// 2. 로드된 데이터에 DB id(PK)가 존재하는지 확인
|
|
||||||
// 수정 모드에서는 항상 deleteOrphans=true (기존 레코드 교체, 복제 방지)
|
|
||||||
const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
|
|
||||||
const urlEditMode = urlParams?.get("mode") === "edit";
|
|
||||||
const dataHasDbId = items.some(item => !!item.originalData?.id);
|
|
||||||
const isEditMode = urlEditMode || dataHasDbId;
|
|
||||||
|
|
||||||
console.log("[SelectedItemsDetailInput] 수정 모드 감지:", {
|
console.log("[SelectedItemsDetailInput] 수정 모드 감지:", {
|
||||||
urlEditMode,
|
urlEditMode,
|
||||||
dataHasDbId,
|
dataHasDbId,
|
||||||
|
|
|
||||||
|
|
@ -812,7 +812,7 @@ export function BomItemEditorComponent({
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (node._isNew) {
|
if (node._isNew) {
|
||||||
const payload: Record<string, any> = {
|
const raw: Record<string, any> = {
|
||||||
...node.data,
|
...node.data,
|
||||||
[fkColumn]: bomId,
|
[fkColumn]: bomId,
|
||||||
[parentKeyColumn]: realParentId,
|
[parentKeyColumn]: realParentId,
|
||||||
|
|
@ -821,10 +821,16 @@ export function BomItemEditorComponent({
|
||||||
company_code: companyCode || undefined,
|
company_code: companyCode || undefined,
|
||||||
version_id: saveVersionId || undefined,
|
version_id: saveVersionId || undefined,
|
||||||
};
|
};
|
||||||
delete payload.id;
|
// bom_detail에 유효한 필드만 남기기 (item_info 조인 필드 제거)
|
||||||
delete payload.tempId;
|
const payload: Record<string, any> = {};
|
||||||
delete payload._isNew;
|
const validKeys = new Set([
|
||||||
delete payload._isDeleted;
|
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(
|
const resp = await apiClient.post(
|
||||||
`/table-management/tables/${mainTableName}/add`,
|
`/table-management/tables/${mainTableName}/add`,
|
||||||
|
|
@ -835,17 +841,14 @@ export function BomItemEditorComponent({
|
||||||
savedCount++;
|
savedCount++;
|
||||||
} else if (node.id) {
|
} else if (node.id) {
|
||||||
const updatedData: Record<string, any> = {
|
const updatedData: Record<string, any> = {
|
||||||
...node.data,
|
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
[fkColumn]: bomId,
|
||||||
[parentKeyColumn]: realParentId,
|
[parentKeyColumn]: realParentId,
|
||||||
seq_no: String(seqNo),
|
seq_no: String(seqNo),
|
||||||
level: String(level),
|
level: String(level),
|
||||||
};
|
};
|
||||||
delete updatedData.tempId;
|
["quantity", "unit", "loss_rate", "remark", "process_type", "base_qty", "revision", "child_item_id", "version_id", "company_code"].forEach((k) => {
|
||||||
delete updatedData._isNew;
|
if (node.data[k] !== undefined) updatedData[k] = node.data[k];
|
||||||
delete updatedData._isDeleted;
|
|
||||||
Object.keys(updatedData).forEach((k) => {
|
|
||||||
if (k.startsWith(`${sourceFk}_`)) delete updatedData[k];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await apiClient.put(
|
await apiClient.put(
|
||||||
|
|
@ -934,6 +937,20 @@ export function BomItemEditorComponent({
|
||||||
setItemSearchOpen(true);
|
setItemSearchOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 이미 추가된 품목 ID 목록 (중복 방지용)
|
||||||
|
const existingItemIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
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(() => {
|
const handleAddRoot = useCallback(() => {
|
||||||
setAddTargetParentId(null);
|
setAddTargetParentId(null);
|
||||||
|
|
@ -1353,18 +1370,7 @@ export function BomItemEditorComponent({
|
||||||
onClose={() => setItemSearchOpen(false)}
|
onClose={() => setItemSearchOpen(false)}
|
||||||
onSelect={handleItemSelect}
|
onSelect={handleItemSelect}
|
||||||
companyCode={companyCode}
|
companyCode={companyCode}
|
||||||
existingItemIds={useMemo(() => {
|
existingItemIds={existingItemIds}
|
||||||
const ids = new Set<string>();
|
|
||||||
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])}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ export function BomDetailEditModal({
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
quantity: node.quantity || "",
|
quantity: node.quantity || "",
|
||||||
unit: node.unit || node.detail_unit || "",
|
|
||||||
process_type: node.process_type || "",
|
process_type: node.process_type || "",
|
||||||
base_qty: node.base_qty || "",
|
|
||||||
loss_rate: node.loss_rate || "",
|
loss_rate: node.loss_rate || "",
|
||||||
remark: node.remark || "",
|
remark: node.remark || "",
|
||||||
});
|
});
|
||||||
|
|
@ -151,11 +149,19 @@ export function BomDetailEditModal({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">단위</Label>
|
<Label className="text-xs sm:text-sm">단위</Label>
|
||||||
<Input
|
{isRootNode ? (
|
||||||
value={formData.unit}
|
<Input
|
||||||
onChange={(e) => handleChange("unit", e.target.value)}
|
value={formData.unit}
|
||||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
onChange={(e) => handleChange("unit", e.target.value)}
|
||||||
/>
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={node?.child_unit || node?.unit || "-"}
|
||||||
|
disabled
|
||||||
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ export function BomTreeComponent({
|
||||||
item_name: raw.item_name || "",
|
item_name: raw.item_name || "",
|
||||||
item_code: raw.item_number || raw.item_code || "",
|
item_code: raw.item_number || raw.item_code || "",
|
||||||
item_type: raw.item_type || raw.division || "",
|
item_type: raw.item_type || raw.division || "",
|
||||||
|
unit: raw.unit || raw.item_unit || "",
|
||||||
} as BomHeaderInfo;
|
} as BomHeaderInfo;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -379,6 +380,18 @@ export function BomTreeComponent({
|
||||||
detail.editData[key] = (headerInfo as any)[key];
|
detail.editData[key] = (headerInfo as any)[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// entity join된 필드를 dot notation으로도 매핑 (item_info.xxx 형식)
|
||||||
|
const h = headerInfo as Record<string, any>;
|
||||||
|
if (h.item_name) detail.editData["item_info.item_name"] = h.item_name;
|
||||||
|
if (h.item_type) detail.editData["item_info.division"] = h.item_type;
|
||||||
|
if (h.item_code || h.item_number) detail.editData["item_info.item_number"] = h.item_code || h.item_number;
|
||||||
|
if (h.unit) detail.editData["item_info.unit"] = h.unit;
|
||||||
|
// entity join alias 형식도 매핑
|
||||||
|
if (h.item_name) detail.editData["item_id_item_name"] = h.item_name;
|
||||||
|
if (h.item_type) detail.editData["item_id_division"] = h.item_type;
|
||||||
|
if (h.item_code || h.item_number) detail.editData["item_id_item_number"] = h.item_code || h.item_number;
|
||||||
|
if (h.unit) detail.editData["item_id_unit"] = h.unit;
|
||||||
};
|
};
|
||||||
// capture: true → EditModal 리스너(bubble)보다 반드시 먼저 실행
|
// capture: true → EditModal 리스너(bubble)보다 반드시 먼저 실행
|
||||||
window.addEventListener("openEditModal", handler, true);
|
window.addEventListener("openEditModal", handler, true);
|
||||||
|
|
|
||||||
|
|
@ -558,31 +558,7 @@ export class ButtonActionExecutor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
// beforeFormSave 이벤트 발송 (BomItemEditor 등 서브 컴포넌트의 저장 처리)
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const beforeSaveEventDetail = {
|
const beforeSaveEventDetail = {
|
||||||
formData: context.formData,
|
formData: context.formData,
|
||||||
skipDefaultSave: false,
|
skipDefaultSave: false,
|
||||||
|
|
@ -596,22 +572,28 @@ export class ButtonActionExecutor {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 비동기 핸들러가 등록한 Promise들 대기 + 동기 핸들러를 위한 최소 대기
|
|
||||||
if (beforeSaveEventDetail.pendingPromises.length > 0) {
|
if (beforeSaveEventDetail.pendingPromises.length > 0) {
|
||||||
console.log(
|
|
||||||
`[handleSave] 비동기 beforeFormSave 핸들러 ${beforeSaveEventDetail.pendingPromises.length}건 대기 중...`,
|
|
||||||
);
|
|
||||||
await Promise.all(beforeSaveEventDetail.pendingPromises);
|
await Promise.all(beforeSaveEventDetail.pendingPromises);
|
||||||
} else {
|
} else {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검증 실패 시 저장 중단
|
|
||||||
if (beforeSaveEventDetail.validationFailed) {
|
if (beforeSaveEventDetail.validationFailed) {
|
||||||
console.log("❌ [handleSave] 검증 실패로 저장 중단:", beforeSaveEventDetail.validationErrors);
|
console.log("❌ [handleSave] 검증 실패로 저장 중단:", beforeSaveEventDetail.validationErrors);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||||
|
if (onSave) {
|
||||||
|
try {
|
||||||
|
await onSave();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ [handleSave] onSave 콜백 실행 오류:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||||
// 단, _tableSection_ 데이터가 있으면 건너뛰기 (handleUniversalFormModalTableSectionSave가 처리)
|
// 단, _tableSection_ 데이터가 있으면 건너뛰기 (handleUniversalFormModalTableSectionSave가 처리)
|
||||||
// EditModal이 내부에서 직접 repeaterSave 이벤트를 발행하고 완료를 기다림
|
// EditModal이 내부에서 직접 repeaterSave 이벤트를 발행하고 완료를 기다림
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,12 @@ export interface CommonStyle {
|
||||||
// 라벨 스타일
|
// 라벨 스타일
|
||||||
labelDisplay?: boolean; // 라벨 표시 여부
|
labelDisplay?: boolean; // 라벨 표시 여부
|
||||||
labelText?: string; // 라벨 텍스트
|
labelText?: string; // 라벨 텍스트
|
||||||
|
labelPosition?: "top" | "left" | "right" | "bottom"; // 라벨 위치 (기본: top)
|
||||||
labelFontSize?: string;
|
labelFontSize?: string;
|
||||||
labelColor?: string;
|
labelColor?: string;
|
||||||
labelFontWeight?: string;
|
labelFontWeight?: string;
|
||||||
labelMarginBottom?: string;
|
labelMarginBottom?: string;
|
||||||
|
labelGap?: string; // 라벨-위젯 간격 (좌/우 배치 시 사용)
|
||||||
|
|
||||||
// 레이아웃
|
// 레이아웃
|
||||||
display?: string;
|
display?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue