From a1819e749c04719d8b69ab5a35f9328c5d51a5d9 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 25 Nov 2025 15:55:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=83=AD=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20menuObjid=20=EC=A0=84=EB=8B=AC,=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=ED=95=84=ED=84=B0=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90,=20=EC=84=A4=EC=A0=95=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 수정사항: 1. 탭 컴포넌트 내 자식 화면에 menuObjid와 tableName 전달 - TabsWidget에 menuObjid prop 추가 - InteractiveScreenViewerDynamic를 통해 자식 화면에 전달 - 채번 규칙 생성 시 올바른 메뉴 스코프 및 테이블명 적용 2. 백엔드: 화면 레이아웃 API에 tableName 추가 - screenManagementService.getLayout()에서 테이블명 반환 - LayoutData 타입에 tableName 필드 추가 - 채번 규칙 생성 시 tableName 검증 강화 3. 카테고리 필터링 기능 복원 - DataFilterConfigPanel에 menuObjid 전달 - getCategoryValues API 사용으로 메뉴 스코프 적용 - 새로고침 후 카테고리 값 자동 재로드 - SplitPanelLayoutConfigPanel에 menuObjid 전달 4. 선택항목 상세입력 설정 패널 포커스 문제 해결 - 로컬 입력 상태 추가로 실시간 속성 편집 패턴 적용 - 텍스트 및 라벨 입력 시 포커스 유지 5. 테이블 리스트 설정 초기화 문제 해결 - handleChange 함수에서 기존 config와 병합하여 전달 - 다른 속성 손실 방지 (columns, dataFilter 등) 버그 수정: - 채번 규칙 생성 시 빈 문자열 대신 null 전달 - 필터 설정 변경 시 컬럼 설정 초기화 방지 - 카테고리 컬럼 선택 시 셀렉트박스 표시 --- .../controllers/numberingRuleController.ts | 10 ++++ .../src/services/screenManagementService.ts | 8 ++- backend-node/src/types/screen.ts | 1 + .../numbering-rule/NumberingRuleDesigner.tsx | 2 +- .../screen/InteractiveScreenViewer.tsx | 5 +- .../screen/InteractiveScreenViewerDynamic.tsx | 3 + .../config-panels/DataFilterConfigPanel.tsx | 43 +++++++++++++-- .../components/screen/widgets/TabsWidget.tsx | 8 ++- .../SelectedItemsDetailInputConfigPanel.tsx | 55 +++++++++++++++++-- .../SplitPanelLayoutConfigPanel.tsx | 43 +++++++++++++-- .../table-list/TableListConfigPanel.tsx | 3 +- 11 files changed, 160 insertions(+), 21 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 55c19353..031a1506 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -132,6 +132,16 @@ router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Respo return res.status(400).json({ success: false, error: "최소 1개 이상의 규칙 파트가 필요합니다" }); } + // 🆕 scopeType이 'table'인 경우 tableName 필수 체크 + if (ruleConfig.scopeType === "table") { + if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") { + return res.status(400).json({ + success: false, + error: "테이블 범위 규칙은 테이블명(tableName)이 필수입니다", + }); + } + } + const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId); logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", { diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 9dbe0270..a7445637 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -1418,9 +1418,9 @@ export class ScreenManagementService { console.log(`=== 레이아웃 로드 시작 ===`); console.log(`화면 ID: ${screenId}`); - // 권한 확인 - const screens = await query<{ company_code: string | null }>( - `SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`, + // 권한 확인 및 테이블명 조회 + const screens = await query<{ company_code: string | null; table_name: string | null }>( + `SELECT company_code, table_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`, [screenId] ); @@ -1512,11 +1512,13 @@ export class ScreenManagementService { console.log(`반환할 컴포넌트 수: ${components.length}`); console.log(`최종 격자 설정:`, gridSettings); console.log(`최종 해상도 설정:`, screenResolution); + console.log(`테이블명:`, existingScreen.table_name); return { components, gridSettings, screenResolution, + tableName: existingScreen.table_name, // 🆕 테이블명 추가 }; } diff --git a/backend-node/src/types/screen.ts b/backend-node/src/types/screen.ts index 304c589c..ca5a466f 100644 --- a/backend-node/src/types/screen.ts +++ b/backend-node/src/types/screen.ts @@ -101,6 +101,7 @@ export interface LayoutData { components: ComponentData[]; gridSettings?: GridSettings; screenResolution?: ScreenResolution; + tableName?: string; // 🆕 화면에 연결된 테이블명 } // 그리드 설정 diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 0bd49982..bfdb69c2 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -152,7 +152,7 @@ export const NumberingRuleDesigner: React.FC = ({ const ruleToSave = { ...currentRule, scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지 - tableName: currentTableName || currentRule.tableName || "", // 현재 테이블명 자동 설정 + tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 자동 설정 (빈 값은 null) menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용) }; diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 4c3e6506..8e1f1ce3 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -433,7 +433,10 @@ export const InteractiveScreenViewer: React.FC = ( return (
- +
); } diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index d1cd2a5f..32591d95 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -39,6 +39,7 @@ interface InteractiveScreenViewerProps { id: number; tableName?: string; }; + menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용) onSave?: () => Promise; onRefresh?: () => void; onFlowRefresh?: () => void; @@ -57,6 +58,7 @@ export const InteractiveScreenViewerDynamic: React.FC void; + menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요) } /** @@ -27,7 +28,15 @@ export function DataFilterConfigPanel({ columns = [], config, onConfigChange, + menuObjid, // 🆕 메뉴 OBJID }: DataFilterConfigPanelProps) { + console.log("🔍 [DataFilterConfigPanel] 초기화:", { + tableName, + columnsCount: columns.length, + menuObjid, + sampleColumns: columns.slice(0, 3), + }); + const [localConfig, setLocalConfig] = useState( config || { enabled: false, @@ -43,6 +52,14 @@ export function DataFilterConfigPanel({ useEffect(() => { if (config) { setLocalConfig(config); + + // 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드 + config.filters?.forEach((filter) => { + if (filter.valueType === "category" && filter.columnName) { + console.log("🔄 기존 카테고리 필터 감지, 값 로딩:", filter.columnName); + loadCategoryValues(filter.columnName); + } + }); } }, [config]); @@ -55,20 +72,34 @@ export function DataFilterConfigPanel({ setLoadingCategories(prev => ({ ...prev, [columnName]: true })); try { - const response = await apiClient.get( - `/table-categories/${tableName}/${columnName}/values` + console.log("🔍 카테고리 값 로드 시작:", { + tableName, + columnName, + menuObjid, + }); + + const response = await getCategoryValues( + tableName, + columnName, + false, // includeInactive + menuObjid // 🆕 메뉴 OBJID 전달 ); - if (response.data.success && response.data.data) { - const values = response.data.data.map((item: any) => ({ + console.log("📦 카테고리 값 로드 응답:", response); + + if (response.success && response.data) { + const values = response.data.map((item: any) => ({ value: item.valueCode, label: item.valueLabel, })); + console.log("✅ 카테고리 값 설정:", { columnName, valuesCount: values.length }); setCategoryValues(prev => ({ ...prev, [columnName]: values })); + } else { + console.warn("⚠️ 카테고리 값 로드 실패 또는 데이터 없음:", response); } } catch (error) { - console.error(`카테고리 값 로드 실패 (${columnName}):`, error); + console.error(`❌ 카테고리 값 로드 실패 (${columnName}):`, error); } finally { setLoadingCategories(prev => ({ ...prev, [columnName]: false })); } diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 683017cf..73b53783 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -11,9 +11,10 @@ interface TabsWidgetProps { component: TabsComponent; className?: string; style?: React.CSSProperties; + menuObjid?: number; // 🆕 부모 화면의 메뉴 OBJID } -export function TabsWidget({ component, className, style }: TabsWidgetProps) { +export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) { const { tabs = [], defaultTab, @@ -233,6 +234,11 @@ export function TabsWidget({ component, className, style }: TabsWidgetProps) { key={component.id} component={component} allComponents={components} + screenInfo={{ + id: tab.screenId, + tableName: layoutData.tableName, + }} + menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달 /> ))} diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx index 80fb210a..8aba0a1b 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx @@ -50,6 +50,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC(config.fieldGroups || []); + // 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용) + const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState>>({}); + // 🆕 그룹별 펼침/접힘 상태 const [expandedGroups, setExpandedGroups] = useState>({}); @@ -140,6 +143,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC { + setLocalFieldGroups(config.fieldGroups || []); + // 로컬 입력 상태는 기존 값 보존 (사용자가 입력 중인 값 유지) + }, [config.fieldGroups]); + // 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드 useEffect(() => { if (!localFields || localFields.length === 0) return; @@ -1177,8 +1186,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC updateDisplayItemInGroup(group.id, itemIndex, { value: e.target.value })} + value={ + localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined + ? localDisplayItemInputs[group.id][itemIndex].value + : item.value || "" + } + onChange={(e) => { + const newValue = e.target.value; + // 로컬 상태 즉시 업데이트 (포커스 유지) + setLocalDisplayItemInputs(prev => ({ + ...prev, + [group.id]: { + ...prev[group.id], + [itemIndex]: { + ...prev[group.id]?.[itemIndex], + value: newValue + } + } + })); + // 실제 상태 업데이트 + updateDisplayItemInGroup(group.id, itemIndex, { value: newValue }); + }} placeholder="| , / , -" className="h-6 text-[9px] sm:text-[10px]" /> @@ -1206,8 +1234,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC updateDisplayItemInGroup(group.id, itemIndex, { label: e.target.value })} + value={ + localDisplayItemInputs[group.id]?.[itemIndex]?.label !== undefined + ? localDisplayItemInputs[group.id][itemIndex].label + : item.label || "" + } + onChange={(e) => { + const newValue = e.target.value; + // 로컬 상태 즉시 업데이트 (포커스 유지) + setLocalDisplayItemInputs(prev => ({ + ...prev, + [group.id]: { + ...prev[group.id], + [itemIndex]: { + ...prev[group.id]?.[itemIndex], + label: newValue + } + } + })); + // 실제 상태 업데이트 + updateDisplayItemInGroup(group.id, itemIndex, { label: newValue }); + }} placeholder="라벨 (예: 거래처:)" className="h-6 w-full text-[9px] sm:text-[10px]" /> diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 9f88e290..387ef85f 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -23,6 +23,7 @@ interface SplitPanelLayoutConfigPanelProps { onChange: (config: SplitPanelLayoutConfig) => void; tables?: TableInfo[]; // 전체 테이블 목록 (선택적) screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용) + menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요) } /** @@ -201,6 +202,7 @@ export const SplitPanelLayoutConfigPanel: React.FC { const [rightTableOpen, setRightTableOpen] = useState(false); const [leftColumnOpen, setLeftColumnOpen] = useState(false); @@ -211,9 +213,26 @@ export const SplitPanelLayoutConfigPanel: React.FC>({}); + + // 🆕 입력 필드용 로컬 상태 + const [isUserEditing, setIsUserEditing] = useState(false); + const [localTitles, setLocalTitles] = useState({ + left: config.leftPanel?.title || "", + right: config.rightPanel?.title || "", + }); // 관계 타입 const relationshipType = config.rightPanel?.relation?.type || "detail"; + + // config 변경 시 로컬 타이틀 동기화 (사용자가 입력 중이 아닐 때만) + useEffect(() => { + if (!isUserEditing) { + setLocalTitles({ + left: config.leftPanel?.title || "", + right: config.rightPanel?.title || "", + }); + } + }, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]); // 조인 모드일 때만 전체 테이블 목록 로드 useEffect(() => { @@ -568,8 +587,15 @@ export const SplitPanelLayoutConfigPanel: React.FC updateLeftPanel({ title: e.target.value })} + value={localTitles.left} + onChange={(e) => { + setIsUserEditing(true); + setLocalTitles(prev => ({ ...prev, left: e.target.value })); + }} + onBlur={() => { + setIsUserEditing(false); + updateLeftPanel({ title: localTitles.left }); + }} placeholder="좌측 패널 제목" /> @@ -1345,6 +1371,7 @@ export const SplitPanelLayoutConfigPanel: React.FC updateLeftPanel({ dataFilter })} + menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 /> @@ -1355,8 +1382,15 @@ export const SplitPanelLayoutConfigPanel: React.FC updateRightPanel({ title: e.target.value })} + value={localTitles.right} + onChange={(e) => { + setIsUserEditing(true); + setLocalTitles(prev => ({ ...prev, right: e.target.value })); + }} + onBlur={() => { + setIsUserEditing(false); + updateRightPanel({ title: localTitles.right }); + }} placeholder="우측 패널 제목" /> @@ -2270,6 +2304,7 @@ export const SplitPanelLayoutConfigPanel: React.FC updateRightPanel({ dataFilter })} + menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 /> diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index c5ed9aaa..0f13abf8 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -255,7 +255,8 @@ export const TableListConfigPanel: React.FC = ({ }, [config.columns]); const handleChange = (key: keyof TableListConfig, value: any) => { - onChange({ [key]: value }); + // 기존 config와 병합하여 전달 (다른 속성 손실 방지) + onChange({ ...config, [key]: value }); }; const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {