From f01fdfc57c773077a0f131b91ec2981fec0b7a8c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 29 Mar 2026 14:49:52 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260329052843-hdtq round-1 --- .../dashboard/data-sources/MultiApiConfig.tsx | 204 +++--- .../data-sources/MultiDatabaseConfig.tsx | 98 +-- .../components/screen/CopyScreenModal.tsx | 20 +- .../screen/FileAttachmentDetailModal.tsx | 4 +- .../screen/InteractiveDataTable.tsx | 6 +- .../screen/InteractiveScreenViewer.tsx | 117 ++-- .../screen/InteractiveScreenViewerDynamic.tsx | 52 +- frontend/components/screen/ScreenDesigner.tsx | 22 +- .../components/screen/TableSettingModal.tsx | 190 +++--- .../config-panels/EntityConfigPanel.tsx | 36 +- .../screen/config-panels/FileConfigPanel.tsx | 18 +- .../screen/panels/DetailSettingsPanel.tsx | 62 +- frontend/lib/api/data.ts | 2 +- frontend/lib/api/dataflowSave.ts | 2 +- frontend/lib/api/dynamicForm.ts | 4 +- .../AccordionBasicComponent.tsx | 10 +- .../button-primary/ButtonPrimaryComponent.tsx | 2 +- .../RepeatScreenModalComponent.tsx | 74 +-- .../components/repeat-screen-modal/types.ts | 2 +- .../SelectedItemsDetailInputComponent.tsx | 607 +----------------- .../ButtonPrimaryComponent.tsx | 9 +- .../SplitPanelLayoutConfigPanel.tsx | 95 ++- .../PopCardListV2Component.tsx | 6 +- .../pop-search/PopSearchConfig.tsx | 76 +-- .../buttonDataflowPerformance.test.ts | 28 +- .../optimizedButtonDataflowService.ts | 2 +- frontend/lib/stores/flowEditorStore.ts | 75 +-- frontend/lib/utils/buttonActions.ts | 67 +- 28 files changed, 659 insertions(+), 1231 deletions(-) diff --git a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx index fcabdbbf..d7164c89 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx @@ -22,7 +22,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [apiConnections, setApiConnections] = useState([]); - const [selectedConnectionId, setSelectedConnectionId] = useState(dataSource.external_connection_id || ""); + const [selectedConnectionId, setSelectedConnectionId] = useState(dataSource.externalConnectionId || ""); const [availableColumns, setAvailableColumns] = useState([]); // API 테스트 후 발견된 컬럼 목록 const [columnTypes, setColumnTypes] = useState>({}); // 컬럼 타입 정보 const [sampleData, setSampleData] = useState([]); // 샘플 데이터 (최대 3개) @@ -37,12 +37,12 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M loadApiConnections(); }, []); - // dataSource.external_connection_id가 변경되면 selectedConnectionId 업데이트 + // dataSource.externalConnectionId가 변경되면 selectedConnectionId 업데이트 useEffect(() => { - if (dataSource.external_connection_id) { - setSelectedConnectionId(dataSource.external_connection_id); + if (dataSource.externalConnectionId) { + setSelectedConnectionId(dataSource.externalConnectionId); } - }, [dataSource.external_connection_id]); + }, [dataSource.externalConnectionId]); // 외부 커넥션 선택 핸들러 const handleConnectionSelect = async (connectionId: string) => { @@ -67,7 +67,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M const updates: Partial = { endpoint: fullEndpoint, - external_connection_id: connectionId, // 외부 연결 ID 저장 + externalConnectionId: connectionId, // 외부 연결 ID 저장 }; const headers: KeyValuePair[] = []; @@ -153,7 +153,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M updates.headers = headers; } if (queryParams.length > 0) { - updates.query_params = queryParams; + updates.queryParams = queryParams; } onChange(updates); @@ -183,24 +183,24 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M // 쿼리 파라미터 추가 const handleAddQueryParam = () => { - const queryParams = dataSource.query_params || []; + const queryParams = dataSource.queryParams || []; onChange({ - query_params: [...queryParams, { id: Date.now().toString(), key: "", value: "" }], + queryParams: [...queryParams, { id: Date.now().toString(), key: "", value: "" }], }); }; // 쿼리 파라미터 삭제 const handleDeleteQueryParam = (id: string) => { - const queryParams = (dataSource.query_params || []).filter((q) => q.id !== id); - onChange({ query_params: queryParams }); + const queryParams = (dataSource.queryParams || []).filter((q) => q.id !== id); + onChange({ queryParams }); }; // 쿼리 파라미터 업데이트 const handleUpdateQueryParam = (id: string, field: "key" | "value", value: string) => { - const queryParams = (dataSource.query_params || []).map((q) => + const queryParams = (dataSource.queryParams || []).map((q) => q.id === id ? { ...q, [field]: value } : q ); - onChange({ query_params: queryParams }); + onChange({ queryParams }); }; // API 테스트 @@ -215,7 +215,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M try { const queryParams: Record = {}; - (dataSource.query_params || []).forEach((param) => { + (dataSource.queryParams || []).forEach((param) => { if (param.key && param.value) { queryParams[param.key] = param.value; } @@ -243,7 +243,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M headers, query_params: queryParams, body: bodyPayload, - external_connection_id: dataSource.external_connection_id, // 외부 연결 ID 전달 + external_connection_id: dataSource.externalConnectionId, // 외부 연결 ID 전달 }), }); @@ -257,20 +257,20 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M try { const lines = text.split('\n').filter(line => { const trimmed = line.trim(); - return trimmed && - !trimmed.startsWith('#') && + return trimmed && + !trimmed.startsWith('#') && !trimmed.startsWith('=') && !trimmed.startsWith('---'); }); - + if (lines.length === 0) return []; - + const result: any[] = []; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const values = line.split(',').map(v => v.trim().replace(/,=$/g, '')); - + // 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명 if (values.length >= 4) { const obj: any = { @@ -285,11 +285,11 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M description: values.slice(8).join(', ').trim() || '', name: values[3] || values[1] || values[0], // 하위 지역명 우선 }; - + result.push(obj); } } - + return result; } catch (error) { console.error("❌ 텍스트 파싱 오류:", error); @@ -299,29 +299,29 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M // JSON Path로 데이터 추출 let data = result.data; - + // 텍스트 데이터 체크 (기상청 API 등) if (data && typeof data === 'object' && data.text && typeof data.text === 'string') { const parsedData = parseTextData(data.text); if (parsedData.length > 0) { data = parsedData; } - } else if (dataSource.json_path) { - const pathParts = dataSource.json_path.split("."); + } else if (dataSource.jsonPath) { + const pathParts = dataSource.jsonPath.split("."); for (const part of pathParts) { data = data?.[part]; } } const rows = Array.isArray(data) ? data : [data]; - + console.log("📊 [최종 파싱된 데이터]", rows); // 컬럼 목록 및 타입 추출 if (rows.length > 0) { const columns = Object.keys(rows[0]); setAvailableColumns(columns); - + // 컬럼 타입 분석 (첫 번째 행 기준) const types: Record = {}; columns.forEach(col => { @@ -344,11 +344,11 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M } }); setColumnTypes(types); - + // 샘플 데이터 저장 (최대 3개) setSampleData(rows.slice(0, 3)); } - + // 위도/경도 또는 coordinates 필드 또는 지역 코드 체크 const hasLocationData = rows.some((row) => { const hasLatLng = (row.lat || row.latitude) && (row.lng || row.longitude); @@ -358,25 +358,25 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M }); if (hasLocationData) { - const markerCount = rows.filter(r => - ((r.lat || r.latitude) && (r.lng || r.longitude)) || + const markerCount = rows.filter(r => + ((r.lat || r.latitude) && (r.lng || r.longitude)) || r.code || r.areaCode || r.regionCode ).length; const polygonCount = rows.filter(r => r.coordinates && Array.isArray(r.coordinates)).length; - - setTestResult({ - success: true, - message: `API 연결 성공 - 마커 ${markerCount}개, 영역 ${polygonCount}개 발견` + + setTestResult({ + success: true, + message: `API 연결 성공 - 마커 ${markerCount}개, 영역 ${polygonCount}개 발견` }); - + // 부모에게 테스트 결과 전달 (지도 미리보기용) if (onTestResult) { onTestResult(rows); } } else { - setTestResult({ - success: true, - message: `API 연결 성공 - ${rows.length}개 데이터 (위치 정보 없음)` + setTestResult({ + success: true, + message: `API 연결 성공 - ${rows.length}개 데이터 (위치 정보 없음)` }); } } else { @@ -494,13 +494,13 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M {/* JSON Path */}
-
- {(dataSource.query_params || []).map((param) => ( + {(dataSource.queryParams || []).map((param) => (
{ - const newMapping = { ...dataSource.column_mapping }; + const newMapping = { ...dataSource.columnMapping }; newMapping[original] = e.target.value; - onChange({ column_mapping: newMapping }); + onChange({ columnMapping: newMapping }); }} placeholder="표시 이름" className="h-8 flex-1 text-xs" @@ -922,9 +922,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M variant="ghost" size="sm" onClick={() => { - const newMapping = { ...dataSource.column_mapping }; + const newMapping = { ...dataSource.columnMapping }; delete newMapping[original]; - onChange({ column_mapping: newMapping }); + onChange({ columnMapping: newMapping }); }} className="h-8 w-8 p-0" > @@ -939,9 +939,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M { - const newFields = [...(dataSource.popup_fields || [])]; + const newFields = [...(dataSource.popupFields || [])]; newFields[index].fieldName = value; - onChange({ popup_fields: newFields }); + onChange({ popupFields: newFields }); }} > @@ -1023,9 +1023,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M { - const newFields = [...(dataSource.popup_fields || [])]; + const newFields = [...(dataSource.popupFields || [])]; newFields[index].label = e.target.value; - onChange({ popup_fields: newFields }); + onChange({ popupFields: newFields }); }} placeholder="예: 차량 번호" className="h-8 w-full text-xs" @@ -1039,9 +1039,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M onChange({ external_connection_id: value })} + value={dataSource.externalConnectionId || ""} + onValueChange={(value) => onChange({ externalConnectionId: value })} > @@ -327,8 +327,8 @@ ORDER BY 하위부서수 DESC`, 마커 종류 @@ -610,9 +610,9 @@ ORDER BY 하위부서수 DESC`, { - const newMapping = { ...dataSource.column_mapping }; + const newMapping = { ...dataSource.columnMapping }; newMapping[original] = e.target.value; - onChange({ column_mapping: newMapping }); + onChange({ columnMapping: newMapping }); }} placeholder="표시 이름" className="h-8 flex-1 text-xs" @@ -623,9 +623,9 @@ ORDER BY 하위부서수 DESC`, variant="ghost" size="sm" onClick={() => { - const newMapping = { ...dataSource.column_mapping }; + const newMapping = { ...dataSource.columnMapping }; delete newMapping[original]; - onChange({ column_mapping: newMapping }); + onChange({ columnMapping: newMapping }); }} className="h-8 w-8 p-0" > @@ -640,9 +640,9 @@ ORDER BY 하위부서수 DESC`, { - const newFields = [...(dataSource.popup_fields || [])]; + const newFields = [...(dataSource.popupFields || [])]; newFields[index].fieldName = value; - onChange({ popup_fields: newFields }); + onChange({ popupFields: newFields }); }} > @@ -721,9 +721,9 @@ ORDER BY 하위부서수 DESC`, { - const newFields = [...(dataSource.popup_fields || [])]; + const newFields = [...(dataSource.popupFields || [])]; newFields[index].label = e.target.value; - onChange({ popup_fields: newFields }); + onChange({ popupFields: newFields }); }} placeholder="예: 차량 번호" className="h-8 w-full text-xs" @@ -737,9 +737,9 @@ ORDER BY 하위부서수 DESC`, = ( accept: config?.accept, multiple: config?.multiple, maxSize: config?.maxSize, - preview: config?.preview, + preview: config?.showPreview, }, }); @@ -1306,7 +1304,7 @@ export const InteractiveScreenViewer: React.FC = ( }; const renderFilePreview = () => { - if (!currentValue || !config?.preview) return null; + if (!currentValue || !config?.showPreview) return null; // 새로운 JSON 구조에서 파일 정보 추출 const fileData = currentValue.files || []; @@ -1366,7 +1364,7 @@ export const InteractiveScreenViewer: React.FC = ( disabled={isReadonly} required={required} multiple={config?.multiple} - accept={config?.accept} + accept={config?.accept?.join(",")} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" style={{ zIndex: 1 }} /> @@ -1402,7 +1400,7 @@ export const InteractiveScreenViewer: React.FC = ( <>

- {config?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'} + {(config as any)?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'}

{(config?.accept || config?.maxSize) && (
@@ -1425,7 +1423,7 @@ export const InteractiveScreenViewer: React.FC = ( case "code": { const widget = comp as WidgetComponent; - const config = widget.webTypeConfig as CodeTypeConfig | undefined; + const config = widget.webTypeConfig as any; console.log(`🔍 [InteractiveScreenViewer] Code 위젯 렌더링:`, { componentId: widget.id, @@ -1487,7 +1485,7 @@ export const InteractiveScreenViewer: React.FC = ( case "entity": { const widget = comp as WidgetComponent; - const config = widget.webTypeConfig as EntityTypeConfig | undefined; + const config = widget.webTypeConfig as any; console.log("🏢 InteractiveScreenViewer - Entity 위젯:", { componentId: widget.id, @@ -1542,16 +1540,16 @@ export const InteractiveScreenViewer: React.FC = ( case "button": { const widget = comp as WidgetComponent; - const config = widget.webTypeConfig as ButtonTypeConfig | undefined; + const config = widget.webTypeConfig as ExtendedButtonTypeConfig | undefined; const handleButtonClick = async () => { // 프리뷰 모드에서는 버튼 동작 차단 if (isPreviewMode) { return; } - - const actionType = config?.actionType || "save"; - + + const actionType: string = config?.actionType || "save"; + try { switch (actionType) { case "save": @@ -1592,7 +1590,7 @@ export const InteractiveScreenViewer: React.FC = ( } } catch (error) { // console.error(`버튼 액션 실행 오류 (${actionType}):`, error); - alert(`작업 중 오류가 발생했습니다: ${error.message}`); + alert(`작업 중 오류가 발생했습니다: ${(error as any).message}`); } }; @@ -1629,17 +1627,17 @@ export const InteractiveScreenViewer: React.FC = ( // 필수 항목 검증 (테이블 타입관리 NOT NULL 기반 + 기존 required 속성 폴백) const requiredFields = allComponents.filter(c => { - const colName = c.columnName || c.id; + const colName = (c as any).columnName || c.id; return (c.required || isColumnRequired(colName)) && colName; }); const missingFields = requiredFields.filter(field => { - const fieldName = field.columnName || field.id; + const fieldName = (field as any).columnName || field.id; const value = currentFormData[fieldName]; return !value || value.toString().trim() === ""; }); if (missingFields.length > 0) { - const fieldNames = missingFields.map(f => f.label || f.columnName || f.id).join(", "); + const fieldNames = missingFields.map(f => f.label || (f as any).columnName || f.id).join(", "); alert(`다음 필수 항목을 입력해주세요: ${fieldNames}`); return; } @@ -1741,8 +1739,8 @@ export const InteractiveScreenViewer: React.FC = ( } // 테이블명 결정 (화면 정보에서 가져오거나 첫 번째 컴포넌트의 테이블명 사용) - const tableName = screenInfo.tableName || - allComponents.find(c => c.columnName)?.tableName || + const tableName = screenInfo.tableName || + (allComponents.find(c => (c as any).columnName) as any)?.tableName || "dynamic_form_data"; // 기본값 // 🆕 자동으로 작성자 정보 추가 (user.userId가 확실히 있음) @@ -1808,11 +1806,9 @@ export const InteractiveScreenViewer: React.FC = ( // 저장 후 데이터 초기화 (선택사항) if (onFormDataChange) { - const resetData: Record = {}; Object.keys(formData).forEach(key => { - resetData[key] = ""; + onFormDataChange!(key, ""); }); - onFormDataChange(resetData); } } else { throw new Error(result.message || "저장에 실패했습니다."); @@ -1841,8 +1837,8 @@ export const InteractiveScreenViewer: React.FC = ( } // 테이블명 결정 - const tableName = screenInfo?.tableName || - allComponents.find(c => c.columnName)?.tableName || + const tableName = screenInfo?.tableName || + (allComponents.find(c => (c as any).columnName) as any)?.tableName || "unknown_table"; if (!tableName || tableName === "unknown_table") { @@ -1861,11 +1857,9 @@ export const InteractiveScreenViewer: React.FC = ( // 삭제 후 폼 초기화 if (onFormDataChange) { - const resetData: Record = {}; Object.keys(formData).forEach(key => { - resetData[key] = ""; + onFormDataChange!(key, ""); }); - onFormDataChange(resetData); } } else { throw new Error(result.message || "삭제에 실패했습니다."); @@ -1881,8 +1875,8 @@ export const InteractiveScreenViewer: React.FC = ( console.log("✏️ 수정 액션 실행"); // 버튼 컴포넌트의 수정 모달 설정 가져오기 - const editModalTitle = config?.editModalTitle || ""; - const editModalDescription = config?.editModalDescription || ""; + const editModalTitle = (config as any)?.editModalTitle || ""; + const editModalDescription = (config as any)?.editModalDescription || ""; console.log("📝 버튼 수정 모달 설정:", { editModalTitle, editModalDescription }); @@ -1926,11 +1920,9 @@ export const InteractiveScreenViewer: React.FC = ( const handleResetAction = () => { if (confirm("모든 입력을 초기화하시겠습니까?")) { if (onFormDataChange) { - const resetData: Record = {}; Object.keys(formData).forEach(key => { - resetData[key] = ""; + onFormDataChange!(key, ""); }); - onFormDataChange(resetData); } // console.log("🔄 폼 초기화 완료"); alert("입력이 초기화되었습니다."); @@ -2046,7 +2038,7 @@ export const InteractiveScreenViewer: React.FC = ( } // console.log("⚡ 커스텀 액션 실행 완료"); } catch (error) { - throw new Error(`커스텀 액션 실행 실패: ${error.message}`); + throw new Error(`커스텀 액션 실행 실패: ${(error as any).message}`); } } else { // console.log("⚡ 커스텀 액션이 설정되지 않았습니다."); @@ -2165,8 +2157,8 @@ export const InteractiveScreenViewer: React.FC = ( return (
@@ -2212,7 +2204,6 @@ export const InteractiveScreenViewer: React.FC = ( const shouldShowLabel = !hideLabel && (component.style?.labelDisplay ?? true) !== false && - component.style?.labelDisplay !== "false" && (component.label || component.style?.labelText) && !templateTypes.includes(component.type); @@ -2237,9 +2228,9 @@ export const InteractiveScreenViewer: React.FC = ( fontSize: component.style?.labelFontSize || "14px", color: component.style?.labelColor || "hsl(var(--foreground))", fontWeight: component.style?.labelFontWeight || "500", - backgroundColor: component.style?.labelBackgroundColor || "transparent", - padding: component.style?.labelPadding || "0", - borderRadius: component.style?.labelBorderRadius || "0", + backgroundColor: (component.style as any)?.labelBackgroundColor || "transparent", + padding: (component.style as any)?.labelPadding || "0", + borderRadius: (component.style as any)?.labelBorderRadius || "0", ...(isHorizontalLabel ? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" } : { marginBottom: component.style?.labelMarginBottom || "4px" }), @@ -2504,7 +2495,7 @@ export const InteractiveScreenViewer: React.FC = ( style={labelStyle} > {labelText} - {(component.required || component.componentConfig?.required || isColumnRequired(component.columnName || component.style?.columnName || "")) && *} + {(component.required || component.componentConfig?.required || isColumnRequired((component as any).columnName || (component.style as any)?.columnName || "")) && *} )} @@ -2523,7 +2514,7 @@ export const InteractiveScreenViewer: React.FC = ( }} > {labelText} - {(component.required || component.componentConfig?.required || isColumnRequired(component.columnName || component.style?.columnName || "")) && *} + {(component.required || component.componentConfig?.required || isColumnRequired((component as any).columnName || (component.style as any)?.columnName || "")) && *} )}
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index da3b1646..58f8f85b 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -15,7 +15,7 @@ import { DynamicComponentRenderer, isColumnRequiredByMeta } from "@/lib/registry import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils"; import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; -import { FlowVisibilityConfig } from "@/types/control-management"; +import { FlowVisibilityConfig } from "@/types/screen-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; @@ -283,7 +283,7 @@ export const InteractiveScreenViewerDynamic: React.FC { try { setPopupLoading(true); - const response = await screenApi.getScreenLayout(screenId); + const layoutData = await screenApi.getLayout(screenId) as any; - if (response.success && response.data) { - const screenData = response.data; - setPopupLayout(screenData.components || []); + if (layoutData) { + setPopupLayout(layoutData.components || []); setPopupScreenResolution({ - width: screenData.screen_resolution?.width || 1200, - height: screenData.screen_resolution?.height || 800, + width: layoutData.screen_resolution?.width || 1200, + height: layoutData.screen_resolution?.height || 800, }); setPopupScreenInfo({ - id: screenData.id, - tableName: screenData.table_name, + id: layoutData.id || screenId, + tableName: layoutData.table_name, }); } else { toast.error("팝업 화면을 불러올 수 없습니다."); @@ -353,7 +352,7 @@ export const InteractiveScreenViewerDynamic: React.FC { // 조건부 표시 평가 (기존 conditional 시스템) - const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents); + const conditionalResult = evaluateConditional(comp.conditional as any, formData, allComponents); // 조건에 따라 숨김 처리 if (!conditionalResult.visible) { @@ -393,7 +392,7 @@ export const InteractiveScreenViewerDynamic: React.FC { - const config = (comp as any).webTypeConfig as ButtonTypeConfig | undefined; + const config = (comp as any).webTypeConfig as any; const { label } = comp; // 버튼 액션 핸들러들 @@ -726,7 +725,7 @@ export const InteractiveScreenViewerDynamic: React.FC 0) { try { - const { default: apiClient } = await import("@/lib/api/client"); + const { apiClient } = await import("@/lib/api/client"); // 중복 체크를 위한 검색 조건 구성 const searchConditions: Record = {}; @@ -880,7 +879,7 @@ export const InteractiveScreenViewerDynamic: React.FC { setPopupFormData((prev) => ({ ...prev, [fieldName]: value })); }} - screenInfo={popupScreenInfo} + screenInfo={popupScreenInfo ?? undefined} /> ))}
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 553a2b6f..e5048a61 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -10,13 +10,12 @@ import { ComponentData, LayoutData, GroupState, - TableInfo, Position, - ColumnInfo, GridSettings, ScreenResolution, - SCREEN_RESOLUTIONS, } from "@/types/screen"; +import type { TableInfo, ColumnInfo } from "@/types/screen-legacy-backup"; +import { SCREEN_RESOLUTIONS } from "@/types/screen-management"; import { generateComponentId } from "@/lib/utils/generateId"; import { getComponentIdFromWebType, @@ -108,7 +107,7 @@ import DetailSettingsPanel from "./panels/DetailSettingsPanel"; import ResolutionPanel from "./panels/ResolutionPanel"; import { usePanelState, PanelConfig } from "@/hooks/usePanelState"; import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; -import { FlowVisibilityConfig } from "@/types/control-management"; +import { FlowVisibilityConfig } from "@/types/screen-management"; import { areAllButtons, generateGroupId, @@ -172,6 +171,7 @@ export default function ScreenDesigner({ // POP 모드 여부에 따른 API 분기 const USE_POP_API = isPop; const [layout, setLayout] = useState({ + screenId: 0, components: [], gridSettings: { columns: 12, @@ -181,6 +181,10 @@ export default function ScreenDesigner({ showGrid: false, // 기본값 false로 변경 gridColor: "#d1d5db", gridOpacity: 0.5, + enabled: false, + size: 16, + color: "#d1d5db", + opacity: 0.5, }, }); const [isSaving, setIsSaving] = useState(false); @@ -567,7 +571,7 @@ export default function ScreenDesigner({ // 레이어의 condition_config에서 zone_id를 가져와서 zones에서 찾기 const findZone = async () => { try { - const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id, activeLayerId); + const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id!, activeLayerId); const zoneId = layerData?.conditionConfig?.zone_id; if (zoneId) { const zone = zones.find(z => z.zone_id === zoneId); @@ -587,14 +591,14 @@ export default function ScreenDesigner({ if (!selectedScreen?.screen_id) return; const loadOtherLayerComponents = async () => { try { - const allLayers = await screenApi.getScreenLayers(selectedScreen.screen_id); + const allLayers = await screenApi.getScreenLayers(selectedScreen.screen_id!); const currentLayerId = activeLayerIdRef.current || 1; const otherLayers = allLayers.filter((l: any) => l.layer_id !== currentLayerId && l.layer_id > 0); const components: ComponentData[] = []; for (const layerInfo of otherLayers) { try { - const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id, layerInfo.layer_id); + const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id!, layerInfo.layer_id); const rawComps = layerData?.components; if (rawComps && Array.isArray(rawComps)) { for (const comp of rawComps) { @@ -1549,7 +1553,7 @@ export default function ScreenDesigner({ const loadLayout = async () => { try { // 🆕 화면에 할당된 메뉴 조회 - const menuInfo = await screenApi.getScreenMenu(selectedScreen.screen_id); + const menuInfo = await screenApi.getScreenMenu(selectedScreen.screen_id!); if (menuInfo) { setMenuObjid(menuInfo.menuObjid); console.log("🔗 화면에 할당된 메뉴:", menuInfo); @@ -1561,7 +1565,7 @@ export default function ScreenDesigner({ let response: any; if (USE_POP_API) { // POP 모드: screen_layouts_pop 테이블 사용 - const popResponse = await screenApi.getLayoutPop(selectedScreen.screen_id); + const popResponse = await screenApi.getLayoutPop(selectedScreen.screen_id!); response = popResponse ? convertV2ToLegacy(popResponse) : null; console.log("📱 POP 레이아웃 로드:", popResponse?.components?.length || 0, "개 컴포넌트"); } else if (USE_V2_API) { diff --git a/frontend/components/screen/TableSettingModal.tsx b/frontend/components/screen/TableSettingModal.tsx index 3ac40c13..8701a54c 100644 --- a/frontend/components/screen/TableSettingModal.tsx +++ b/frontend/components/screen/TableSettingModal.tsx @@ -306,22 +306,22 @@ export function TableSettingModal({ // 초기 편집 상태 설정 const initialEdits: Record> = {}; columnsData.forEach((col) => { - // referenceTable이 설정되어 있으면 inputType은 entity여야 함 - let effectiveInputType = col.inputType || "direct"; - if (col.referenceTable && effectiveInputType !== "entity") { + // reference_table이 설정되어 있으면 input_type은 entity여야 함 + let effectiveInputType = col.input_type || "direct"; + if (col.reference_table && effectiveInputType !== "entity") { effectiveInputType = "entity"; } - // codeCategory/codeValue가 설정되어 있으면 inputType은 code여야 함 - if (col.codeCategory && effectiveInputType !== "code") { + // code_category/code_value가 설정되어 있으면 input_type은 code여야 함 + if (col.code_category && effectiveInputType !== "code") { effectiveInputType = "code"; } - - initialEdits[col.columnName] = { - displayName: col.displayName, - inputType: effectiveInputType, - referenceTable: col.referenceTable, - referenceColumn: col.referenceColumn, - displayColumn: col.displayColumn, + + initialEdits[col.column_name] = { + display_name: col.display_name, + input_type: effectiveInputType, + reference_table: col.reference_table, + reference_column: col.reference_column, + display_column: col.display_column, }; }); setEditedColumns(initialEdits); @@ -408,8 +408,8 @@ export function TableSettingModal({ // 참조 테이블 변경 시 컬럼 로드 useEffect(() => { Object.values(editedColumns).forEach((col) => { - if (col.referenceTable && col.referenceTable !== "none") { - loadRefTableColumns(col.referenceTable); + if (col.reference_table && col.reference_table !== "none") { + loadRefTableColumns(col.reference_table); } }); }, [editedColumns, loadRefTableColumns]); @@ -431,17 +431,17 @@ export function TableSettingModal({ })); // 입력 타입 변경 시 관련 필드 초기화 - if (field === "inputType") { + if (field === "input_type") { // 엔티티가 아닌 다른 타입으로 변경하면 참조 설정 초기화 if (value !== "entity") { setEditedColumns((prev) => ({ ...prev, [columnName]: { ...prev[columnName], - inputType: value, - referenceTable: "", - referenceColumn: "", - displayColumn: "", + input_type: value, + reference_table: "", + reference_column: "", + display_column: "", }, })); } @@ -451,22 +451,22 @@ export function TableSettingModal({ ...prev, [columnName]: { ...prev[columnName], - inputType: value, - codeCategory: "", - codeValue: "", + input_type: value, + code_category: "", + code_value: "", }, })); } } - + // 참조 테이블 변경 시 참조 컬럼 초기화 - if (field === "referenceTable") { + if (field === "reference_table") { setEditedColumns((prev) => ({ ...prev, [columnName]: { ...prev[columnName], - referenceColumn: "", - displayColumn: "", + reference_column: "", + display_column: "", }, })); if (value && value !== "none") { @@ -482,7 +482,7 @@ export function TableSettingModal({ // 변경된 컬럼들만 저장 for (const [columnName, editedSettings] of Object.entries(editedColumns)) { // 기존 컬럼 정보 찾기 - const originalColumn = tableColumns.find((c) => c.columnName === columnName); + const originalColumn = tableColumns.find((c) => c.column_name === columnName); if (!originalColumn) continue; // 기존 값과 편집된 값 병합 @@ -491,25 +491,25 @@ export function TableSettingModal({ ...editedSettings, }; - // detailSettings 처리 (Entity 타입인 경우) - let finalDetailSettings = mergedColumn.detailSettings || ""; - - // referenceTable이 설정되어 있으면 inputType을 entity로 자동 설정 - let currentInputType = (mergedColumn.inputType || "") as string; - if (mergedColumn.referenceTable && currentInputType !== "entity") { + // detail_settings 처리 (Entity 타입인 경우) + let finalDetailSettings = mergedColumn.detail_settings || ""; + + // reference_table이 설정되어 있으면 input_type을 entity로 자동 설정 + let currentInputType = (mergedColumn.input_type || "") as string; + if (mergedColumn.reference_table && currentInputType !== "entity") { currentInputType = "entity"; } - // codeCategory가 설정되어 있으면 inputType을 code로 자동 설정 - if (mergedColumn.codeCategory && currentInputType !== "code") { + // code_category가 설정되어 있으면 input_type을 code로 자동 설정 + if (mergedColumn.code_category && currentInputType !== "code") { currentInputType = "code"; } - if (currentInputType === "entity" && mergedColumn.referenceTable) { - // 기존 detailSettings를 파싱하거나 새로 생성 + if (currentInputType === "entity" && mergedColumn.reference_table) { + // 기존 detail_settings를 파싱하거나 새로 생성 let existingSettings: Record = {}; - if (typeof mergedColumn.detailSettings === "string" && mergedColumn.detailSettings.trim().startsWith("{")) { + if (typeof mergedColumn.detail_settings === "string" && mergedColumn.detail_settings.trim().startsWith("{")) { try { - existingSettings = JSON.parse(mergedColumn.detailSettings); + existingSettings = JSON.parse(mergedColumn.detail_settings); } catch { existingSettings = {}; } @@ -518,9 +518,9 @@ export function TableSettingModal({ // 엔티티 설정 추가 const entitySettings = { ...existingSettings, - entityTable: mergedColumn.referenceTable, - entityCodeColumn: mergedColumn.referenceColumn || "id", - entityLabelColumn: mergedColumn.displayColumn || "name", + entityTable: mergedColumn.reference_table, + entityCodeColumn: mergedColumn.reference_column || "id", + entityLabelColumn: mergedColumn.display_column || "name", placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요", searchable: existingSettings.searchable ?? true, }; @@ -552,14 +552,14 @@ export function TableSettingModal({ // ColumnSettings 인터페이스에 맞게 데이터 구성 const columnSetting: ColumnSettings = { columnName: columnName, - columnLabel: mergedColumn.displayName || originalColumn.displayName || "", - inputType: currentInputType || "text", // referenceTable/codeCategory가 설정된 경우 자동 보정된 값 사용 + columnLabel: mergedColumn.display_name || originalColumn.display_name || "", + inputType: currentInputType || "text", // reference_table/code_category가 설정된 경우 자동 보정된 값 사용 detailSettings: finalDetailSettings, - codeCategory: mergedColumn.codeCategory || originalColumn.codeCategory || "", - codeValue: mergedColumn.codeValue || originalColumn.codeValue || "", - referenceTable: mergedColumn.referenceTable || "", - referenceColumn: mergedColumn.referenceColumn || "", - displayColumn: mergedColumn.displayColumn || "", + codeCategory: mergedColumn.code_category || originalColumn.code_category || "", + codeValue: mergedColumn.code_value || originalColumn.code_value || "", + referenceTable: mergedColumn.reference_table || "", + referenceColumn: mergedColumn.reference_column || "", + displayColumn: mergedColumn.display_column || "", }; console.log("저장할 컬럼 설정:", columnSetting); @@ -590,11 +590,11 @@ export function TableSettingModal({ const mergedColumns = useMemo(() => { const columnsMap = new Map(); - // API에서 가져온 컬럼 정보 (camelCase) + // API에서 가져온 컬럼 정보 (snake_case) tableColumns.forEach((tcol) => { - columnsMap.set(tcol.columnName, { + columnsMap.set(tcol.column_name, { ...tcol, - isPK: tcol.isPrimaryKey, + isPK: tcol.is_primary_key, isFK: false, // 백엔드에서 isForeignKey를 제공하지 않으므로 false 기본값 }); }); @@ -605,7 +605,7 @@ export function TableSettingModal({ // 선택된 컬럼 정보 const selectedColumnInfo = useMemo(() => { if (!selectedColumn) return null; - return mergedColumns.find((c) => c.columnName === selectedColumn); + return mergedColumns.find((c) => c.column_name === selectedColumn); }, [selectedColumn, mergedColumns]); // 테이블 옵션 @@ -613,9 +613,9 @@ export function TableSettingModal({ () => [ { value: "none", label: "-- 선택 안함 --" }, ...tables.map((t) => ({ - value: t.tableName, - label: t.displayName || t.tableName, - description: t.tableName, + value: t.table_name, + label: t.display_name || t.table_name, + description: t.table_name, })), ], [tables] @@ -637,9 +637,9 @@ export function TableSettingModal({ return [ { value: "", label: "-- 선택 안함 --" }, ...cols.map((c) => ({ - value: c.columnName, - label: c.displayName || c.columnName, - description: c.dataType, + value: c.column_name, + label: c.display_name || c.column_name, + description: c.data_type, })), ]; }; @@ -719,8 +719,8 @@ export function TableSettingModal({ ({ ...col, - isPK: col.columnName === "id" || col.columnName.endsWith("_id"), - isFK: (col.inputType as string) === "entity", + isPK: col.column_name === "id" || col.column_name.endsWith("_id"), + isFK: (col.input_type as string) === "entity", }))} editedColumns={editedColumns} selectedColumn={selectedColumn} @@ -747,9 +747,9 @@ export function TableSettingModal({ {/* 우측: 상세 설정 (60%) */}
- {selectedColumn && mergedColumns.find((c) => c.columnName === selectedColumn) ? ( + {selectedColumn && mergedColumns.find((c) => c.column_name === selectedColumn) ? ( c.columnName === selectedColumn)!} + columnInfo={mergedColumns.find((c) => c.column_name === selectedColumn)!} editedColumn={editedColumns[selectedColumn] || {}} tableOptions={tableOptions} inputTypeOptions={inputTypeOptions} @@ -997,8 +997,8 @@ function ColumnListTab({ const term = searchTerm.toLowerCase(); return columns.filter( (col) => - col.columnName.toLowerCase().includes(term) || - (col.displayName || "").toLowerCase().includes(term) + col.column_name.toLowerCase().includes(term) || + (col.display_name || "").toLowerCase().includes(term) ); }, [columns, searchTerm]); @@ -1045,15 +1045,15 @@ function ColumnListTab({ ) : (
{filteredColumns.map((col) => { - const edited = editedColumns[col.columnName] || {}; - // editedColumns에서 inputType을 가져옴 (초기화 시 이미 보정됨) - const inputType = (edited.inputType || col.inputType || "text") as string; - const isSelected = selectedColumn === col.columnName; + const edited = editedColumns[col.column_name] || {}; + // editedColumns에서 input_type을 가져옴 (초기화 시 이미 보정됨) + const inputType = (edited.input_type || col.input_type || "text") as string; + const isSelected = selectedColumn === col.column_name; return (
onSelectColumn(col.columnName)} + key={col.column_name} + onClick={() => onSelectColumn(col.column_name)} className={cn( "cursor-pointer rounded-lg border p-3 transition-colors", isSelected @@ -1067,7 +1067,7 @@ function ColumnListTab({ {getInputTypeIcon(inputType)} - {edited.displayName || col.displayName || col.columnName} + {edited.display_name || col.display_name || col.column_name}
@@ -1077,8 +1077,8 @@ function ColumnListTab({ PK )} - {/* 엔티티 타입이거나 referenceTable이 설정되어 있으면 조인 배지 표시 (FK와 동일 의미) */} - {(inputType === "entity" || edited.referenceTable || col.referenceTable) && ( + {/* 엔티티 타입이거나 reference_table이 설정되어 있으면 조인 배지 표시 (FK와 동일 의미) */} + {(inputType === "entity" || edited.reference_table || col.reference_table) && ( 조인 @@ -1087,7 +1087,7 @@ function ColumnListTab({
- {col.columnName} + {col.column_name}
); @@ -1122,12 +1122,12 @@ function ColumnDetailPanel({ loadingRefColumns, onColumnChange, }: ColumnDetailPanelProps) { - const currentLabel = editedColumn.displayName ?? columnInfo.displayName ?? ""; - const currentRefTable = editedColumn.referenceTable ?? columnInfo.referenceTable ?? ""; - const currentRefColumn = editedColumn.referenceColumn ?? columnInfo.referenceColumn ?? ""; - const currentDisplayColumn = editedColumn.displayColumn ?? columnInfo.displayColumn ?? ""; - // editedColumn에서 inputType을 가져옴 (초기화 시 이미 보정됨) - const currentInputType = (editedColumn.inputType ?? columnInfo.inputType ?? "text") as string; + const currentLabel = editedColumn.display_name ?? columnInfo.display_name ?? ""; + const currentRefTable = editedColumn.reference_table ?? columnInfo.reference_table ?? ""; + const currentRefColumn = editedColumn.reference_column ?? columnInfo.reference_column ?? ""; + const currentDisplayColumn = editedColumn.display_column ?? columnInfo.display_column ?? ""; + // editedColumn에서 input_type을 가져옴 (초기화 시 이미 보정됨) + const currentInputType = (editedColumn.input_type ?? columnInfo.input_type ?? "text") as string; return (
@@ -1139,9 +1139,9 @@ function ColumnDetailPanel({
- {columnInfo.columnName} + {columnInfo.column_name} - {columnInfo.dataType} + {columnInfo.data_type} {columnInfo.isPK && ( Primary Key @@ -1166,8 +1166,8 @@ function ColumnDetailPanel({ onColumnChange("displayName", e.target.value)} - placeholder={columnInfo.columnName} + onChange={(e) => onColumnChange("display_name", e.target.value)} + placeholder={columnInfo.column_name} className="h-9 text-sm" />

@@ -1179,7 +1179,7 @@ function ColumnDetailPanel({