Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-18 10:31:04 +09:00
28 changed files with 3262 additions and 2664 deletions
@@ -362,15 +362,15 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
// 런타임 모드에서 컴포넌트 타입별 높이 처리
if (!isDesignMode) {
const compType = (component as any).componentType || component.componentConfig?.type || "";
// 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1)
const flexGrowTypes = [
// 레이아웃 계열: 부모 래퍼를 꽉 채움 (ResponsiveGridRenderer가 % 높이 관리)
const fillParentTypes = [
"table-list", "v2-table-list",
"split-panel-layout", "split-panel-layout2",
"v2-split-panel-layout", "screen-split-panel",
"v2-tab-container", "tab-container",
"tabs-widget", "v2-tabs-widget",
];
if (flexGrowTypes.some(t => compType === t)) {
if (fillParentTypes.some(t => compType === t)) {
return "100%";
}
const autoHeightTypes = [
@@ -23,8 +23,9 @@ function getComponentTypeId(component: ComponentData): string {
}
/**
* 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링.
* 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대.
* 디자이너 절대좌표를 캔버스 대비 비율(%)로 변환하여 렌더링.
* 가로: 컨테이너 너비 대비 % → 반응형 스케일
* 세로: 컨테이너 높이 대비 % → 뷰포트에 맞게 자동 조절
*/
function ProportionalRenderer({
components,
@@ -47,19 +48,12 @@ function ProportionalRenderer({
}, []);
const topLevel = components.filter((c) => !c.parentId);
const ratio = containerW > 0 ? containerW / canvasWidth : 1;
const maxBottom = topLevel.reduce((max, c) => {
const bottom = c.position.y + (c.size?.height || 40);
return Math.max(max, bottom);
}, 0);
return (
<div
ref={containerRef}
data-screen-runtime="true"
className="bg-background relative w-full overflow-x-hidden"
style={{ minHeight: containerW > 0 ? `${maxBottom * ratio}px` : "200px" }}
className="bg-background relative h-full w-full overflow-hidden"
>
{containerW > 0 &&
topLevel.map((component) => {
@@ -72,9 +66,9 @@ function ProportionalRenderer({
style={{
position: "absolute",
left: `${(component.position.x / canvasWidth) * 100}%`,
top: `${component.position.y * ratio}px`,
top: `${(component.position.y / canvasHeight) * 100}%`,
width: `${((component.size?.width || 100) / canvasWidth) * 100}%`,
height: `${(component.size?.height || 40) * ratio}px`,
height: `${((component.size?.height || 40) / canvasHeight) * 100}%`,
zIndex: component.position.z || 1,
}}
>
@@ -17,7 +17,6 @@ import {
GroupComponent,
DataTableComponent,
TableInfo,
LayoutComponent,
FileComponent,
AreaComponent,
} from "@/types/screen";
@@ -47,7 +46,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent
// ComponentRegistry import (동적 ConfigPanel 가져오기용)
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer";
import { columnMetaCache, loadColumnMeta } from "@/lib/registry/DynamicComponentRenderer";
import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
import StyleEditor from "../StyleEditor";
import { Slider } from "@/components/ui/slider";
@@ -98,6 +97,24 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
// 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용)
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName?: string }>>([]);
// 🆕 선택된 컴포넌트의 테이블에 대한 columnMeta 캐시가 비어 있으면 로드 후 재렌더
const [columnMetaVersion, setColumnMetaVersion] = useState(0);
useEffect(() => {
if (!selectedComponent) return;
const tblName =
(selectedComponent as any).tableName ||
currentTable?.tableName ||
tables?.[0]?.tableName;
if (!tblName) return;
if (columnMetaCache[tblName]) return;
loadColumnMeta(tblName).then(() => setColumnMetaVersion((v) => v + 1));
}, [
selectedComponent?.id,
(selectedComponent as any)?.tableName,
currentTable?.tableName,
tables?.[0]?.tableName,
]);
// 🆕 전체 테이블 목록 로드
useEffect(() => {
const loadAllTables = async () => {
@@ -211,20 +228,20 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
// 현재 화면의 테이블명 가져오기
const currentTableName = tables?.[0]?.tableName;
// DB input_type 가져오기 (columnMetaCache에서 최신값 조회)
const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
// DB input_type만 조회 (saved config와 분리하여 전달)
const colName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName;
const tblName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName;
const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined;
const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined;
const inputType = dbInputType || currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType;
// 컴포넌트별 추가 props
const extraProps: Record<string, any> = {};
const resolvedTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName;
const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName;
const resolvedTableName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName;
const resolvedColumnName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName;
if (componentId === "v2-input" || componentId === "v2-select") {
extraProps.inputType = inputType;
extraProps.componentType = componentId;
extraProps.inputType = dbInputType;
extraProps.tableName = resolvedTableName;
extraProps.columnName = resolvedColumnName;
extraProps.screenTableName = resolvedTableName;
@@ -256,7 +273,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
const currentConfig = selectedComponent.componentConfig || {};
// 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지)
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const config = currentConfig || (definition as any).defaultProps?.componentConfig || {};
const handlePanelConfigChange = (newConfig: any) => {
// 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합
@@ -282,14 +299,14 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
onConfigChange={handlePanelConfigChange}
tables={tables}
allTables={allTables}
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
tableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
columnName={
(selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName
}
inputType={(selectedComponent as any).inputType || currentConfig?.inputType}
componentType={componentType}
tableColumns={currentTable?.columns || []}
tableColumns={(currentTable as any)?.columns || []}
allComponents={allComponents}
currentComponent={selectedComponent}
menuObjid={menuObjid}
@@ -323,8 +340,8 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
componentType={componentType}
config={selectedComponent.componentConfig || {}}
onChange={handleDynamicConfigChange}
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableColumns={currentTable?.columns || []}
screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
tableColumns={(currentTable as any)?.columns || []}
tables={tables}
menuObjid={menuObjid}
allComponents={allComponents}
@@ -491,7 +508,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<span className="text-muted-foreground text-xs"></span>
<div className="w-[160px]">
<Input
value={group.title || area.title || ""}
value={(group as any).title || (area as any).title || ""}
onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목"
className="h-7 text-xs"
@@ -503,7 +520,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<span className="text-muted-foreground text-xs"></span>
<div className="w-[160px]">
<Input
value={area.description || ""}
value={(area as any).description || ""}
onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명"
className="h-7 text-xs"
@@ -519,9 +536,9 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<h4 className="text-muted-foreground py-2 text-[10px] font-semibold tracking-wider uppercase">OPTIONS</h4>
{(isInputField || widget.required !== undefined) &&
(() => {
const colName = widget.columnName || selectedComponent?.columnName;
const colName = widget.columnName || (selectedComponent as any)?.columnName;
const colMeta = colName
? currentTable?.columns?.find(
? (currentTable as any)?.columns?.find(
(c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase(),
)
: null;
@@ -568,7 +585,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<div className="flex items-center justify-between py-1.5">
<span className="text-muted-foreground text-xs"></span>
<Checkbox
checked={selectedComponent.hidden === true || selectedComponent.componentConfig?.hidden === true}
checked={(selectedComponent as any).hidden === true || selectedComponent.componentConfig?.hidden === true}
onCheckedChange={(checked) => {
handleUpdate("hidden", checked);
handleUpdate("componentConfig.hidden", checked);
@@ -689,7 +706,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<div className="flex items-center justify-between py-1.5">
<span className="text-muted-foreground text-xs"></span>
<Checkbox
checked={selectedComponent.style?.labelDisplay === true || selectedComponent.labelDisplay === true}
checked={selectedComponent.style?.labelDisplay === true || (selectedComponent as any).labelDisplay === true}
onCheckedChange={(checked) => {
const boolValue = checked === true;
handleUpdate("style.labelDisplay", boolValue);
@@ -785,7 +802,7 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
const webType = selectedComponent.componentConfig?.webType;
// 테이블 패널에서 드래그한 컴포넌트인지 확인
const isFromTablePanel = !!(selectedComponent.tableName && selectedComponent.columnName);
const isFromTablePanel = !!((selectedComponent as any).tableName && (selectedComponent as any).columnName);
if (!componentId) {
return (
@@ -845,8 +862,8 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<DynamicComponentConfigPanel
componentId={componentId}
config={selectedComponent.componentConfig || {}}
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
tableColumns={currentTable?.columns || []}
screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
tableColumns={(currentTable as any)?.columns || []}
tables={tables}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용
@@ -1006,8 +1023,8 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
<DynamicComponentConfigPanel
componentId={widget.widgetType}
config={widget.componentConfig || {}}
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
tableColumns={currentTable?.columns || []}
screenTableName={(widget as any).tableName || currentTable?.tableName || currentTableName}
tableColumns={(currentTable as any)?.columns || []}
tables={tables}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
allComponents={allComponents} // 🆕 연쇄 드롭다운 부모 감지용
@@ -1023,17 +1040,17 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
return (
<div className="space-y-4">
{/* WebType 선택 (있는 경우만) */}
{widget.webType && (
{(widget as any).webType && (
<div>
<Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<Select value={(widget as any).webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypes.map((wt) => (
<SelectItem key={wt.web_type} value={wt.web_type}>
{wt.web_type_name_kor || wt.web_type}
{(wt as any).web_type_name_kor || wt.web_type}
</SelectItem>
))}
</SelectContent>