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

This commit is contained in:
kjs 2026-03-12 14:31:31 +09:00
commit dc37ad4471
6 changed files with 219 additions and 33 deletions

View File

@ -204,6 +204,66 @@ function FullWidthOverlayRow({
);
}
function ProportionalRenderer({
components,
canvasWidth,
canvasHeight,
renderComponent,
}: ResponsiveGridRendererProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerW, setContainerW] = useState(0);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect.width;
if (w && w > 0) setContainerW(w);
});
ro.observe(el);
return () => ro.disconnect();
}, []);
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" }}
>
{containerW > 0 &&
topLevel.map((component) => {
const typeId = getComponentTypeId(component);
return (
<div
key={component.id}
data-component-id={component.id}
data-component-type={typeId}
style={{
position: "absolute",
left: `${(component.position.x / canvasWidth) * 100}%`,
top: `${component.position.y * ratio}px`,
width: `${((component.size?.width || 100) / canvasWidth) * 100}%`,
height: `${(component.size?.height || 40) * ratio}px`,
zIndex: component.position.z || 1,
}}
>
{renderComponent(component)}
</div>
);
})}
</div>
);
}
export function ResponsiveGridRenderer({
components,
canvasWidth,
@ -213,6 +273,18 @@ export function ResponsiveGridRenderer({
const { isMobile } = useResponsive();
const topLevel = components.filter((c) => !c.parentId);
const hasFullWidthComponent = topLevel.some((c) => isFullWidthComponent(c));
if (!isMobile && !hasFullWidthComponent) {
return (
<ProportionalRenderer
components={components}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}
renderComponent={renderComponent}
/>
);
}
const rows = groupComponentsIntoRows(topLevel);
const processedRows: ProcessedRow[] = [];
@ -357,7 +429,7 @@ export function ResponsiveGridRenderer({
style={{
width: isFullWidth ? "100%" : undefined,
flexBasis: useFlexHeight ? undefined : flexBasis,
flexGrow: 1,
flexGrow: percentWidth,
flexShrink: 1,
minWidth: isMobile ? "100%" : undefined,
minHeight: useFlexHeight ? "300px" : (component.size?.height

View File

@ -194,7 +194,7 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
operator: "contains", // 기본 연산자
value: "",
filterType: cf.filterType,
width: cf.width || 200, // 너비 포함 (기본 200px)
width: cf.width && cf.width >= 10 && cf.width <= 100 ? cf.width : 25,
}));
// localStorage에 저장 (화면별로 독립적)
@ -334,20 +334,20 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
{/* 너비 입력 */}
<Input
type="number"
value={filter.width || 200}
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
onChange={(e) => {
const newWidth = parseInt(e.target.value) || 200;
const newWidth = Math.min(100, Math.max(10, parseInt(e.target.value) || 25));
setColumnFilters((prev) =>
prev.map((f) => (f.columnName === filter.columnName ? { ...f, width: newWidth } : f)),
);
}}
disabled={!filter.enabled}
placeholder="너비"
placeholder="25"
className="h-8 w-[80px] text-xs sm:h-9 sm:text-sm"
min={50}
max={500}
min={10}
max={100}
/>
<span className="text-muted-foreground text-xs">px</span>
<span className="text-muted-foreground text-xs">%</span>
</div>
))}
</div>

View File

@ -136,7 +136,7 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
inputType,
enabled: false,
filterType,
width: 200,
width: 25,
};
});
@ -271,7 +271,7 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
operator: "contains",
value: "",
filterType: f.filterType,
width: f.width || 200,
width: f.width && f.width >= 10 && f.width <= 100 ? f.width : 25,
}));
onFiltersApplied?.(activeFilters);
@ -498,15 +498,15 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
</Select>
<Input
type="number"
min={100}
max={400}
value={filter.width || 200}
min={10}
max={100}
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
onChange={(e) =>
handleFilterWidthChange(filter.columnName, parseInt(e.target.value) || 200)
handleFilterWidthChange(filter.columnName, Math.min(100, Math.max(10, parseInt(e.target.value) || 25)))
}
className="h-7 w-16 text-center text-xs"
/>
<span className="text-muted-foreground text-xs">px</span>
<span className="text-muted-foreground text-xs">%</span>
</div>
))}
</div>

View File

@ -648,12 +648,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
const renderFilterInput = (filter: TableFilter) => {
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
const value = filterValues[filter.columnName] || "";
const width = filter.width || 200; // 기본 너비 200px
switch (filter.filterType) {
case "date":
return (
<div className="w-full sm:w-auto" style={{ maxWidth: `${width}px` }}>
<div className="w-full">
<ModernDatePicker
label={column?.columnLabel || filter.columnName}
value={value ? (typeof value === "string" ? { from: new Date(value), to: new Date(value) } : value) : {}}
@ -676,8 +675,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
type="number"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-9 w-full text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:w-auto sm:text-sm"
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
className="h-9 w-full text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
style={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
placeholder={column?.columnLabel}
/>
);
@ -726,10 +725,10 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
variant="outline"
role="combobox"
className={cn(
"h-9 min-h-9 w-full justify-between text-xs font-normal focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 sm:w-auto sm:text-sm",
"h-9 min-h-9 w-full justify-between text-xs font-normal focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 sm:text-sm",
selectedValues.length === 0 && "text-muted-foreground",
)}
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
style={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
>
<span className="truncate">{getDisplayText()}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
@ -781,8 +780,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
type="text"
value={value}
onChange={(e) => handleFilterChange(filter.columnName, e.target.value)}
className="h-9 w-full text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:w-auto sm:text-sm"
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
className="h-9 w-full text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none sm:text-sm"
style={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
placeholder={column?.columnLabel}
/>
);
@ -802,9 +801,18 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
{/* 필터 입력 필드들 */}
{activeFilters.length > 0 && (
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
{activeFilters.map((filter) => (
<div key={filter.columnName}>{renderFilterInput(filter)}</div>
))}
{activeFilters.map((filter) => {
const widthPercent = filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25;
return (
<div
key={filter.columnName}
className="w-full sm:w-auto"
style={{ flex: `0 1 ${widthPercent}%`, minWidth: "120px" }}
>
{renderFilterInput(filter)}
</div>
);
})}
{/* 초기화 버튼 */}
<Button variant="outline" size="sm" onClick={handleResetFilters} className="h-9 shrink-0 text-xs sm:text-sm">

View File

@ -106,7 +106,7 @@ export function TableSearchWidgetConfigPanel({
columnName: "",
columnLabel: "",
filterType: "text",
width: 200,
width: 25,
};
const updatedFilters = [...localPresetFilters, newFilter];
setLocalPresetFilters(updatedFilters);
@ -346,15 +346,15 @@ export function TableSearchWidgetConfigPanel({
{/* 너비 */}
<div>
<Label className="text-[10px] sm:text-xs mb-1"> (px)</Label>
<Label className="text-[10px] sm:text-xs mb-1"> (%)</Label>
<Input
type="number"
value={filter.width || 200}
onChange={(e) => updateFilter(filter.id, "width", parseInt(e.target.value))}
placeholder="200"
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
onChange={(e) => updateFilter(filter.id, "width", Math.min(100, Math.max(10, parseInt(e.target.value) || 25)))}
placeholder="25"
className="h-7 text-xs"
min={100}
max={500}
min={10}
max={100}
/>
</div>
</div>

View File

@ -0,0 +1,106 @@
/**
* 회사 기본정보 화면 - 컴포넌트 렌더링 비율 정밀 분석
*
* 사용법: 브라우저에서 회사 기본정보 화면을 상태에서
* F12 Console 스크립트 전체를 붙여넣고 Enter
*/
(function analyzeLayout() {
const results = { part1: null, part2: null };
// ========== Part 1: DesktopCanvasRenderer 구조 확인 ==========
const runtime = document.querySelector('[data-screen-runtime="true"]');
if (!runtime) {
console.warn('⚠️ [data-screen-runtime="true"] 요소를 찾을 수 없습니다.');
console.log('대안: ScreenModal 기반 렌더링이거나 다른 구조일 수 있습니다.');
results.part1 = { error: 'Runtime not found' };
} else {
const rect = runtime.getBoundingClientRect();
const inner = runtime.firstElementChild;
results.part1 = {
runtimeContainer: { width: rect.width, height: rect.height },
innerDiv: null,
components: [],
};
if (inner) {
const style = inner.style;
results.part1.innerDiv = {
width: style.width,
height: style.height,
transform: style.transform,
transformOrigin: style.transformOrigin,
position: style.position,
};
const comps = inner.querySelectorAll('[data-component-id]');
comps.forEach((comp) => {
const s = comp.style;
const r = comp.getBoundingClientRect();
results.part1.components.push({
type: comp.getAttribute('data-component-type'),
id: comp.getAttribute('data-component-id'),
stylePos: `(${s.left}, ${s.top})`,
styleSize: `${s.width} x ${s.height}`,
renderedSize: `${Math.round(r.width)} x ${Math.round(r.height)}`,
});
});
} else {
// ResponsiveGridRenderer (flex 기반) 구조일 수 있음 - 행 단위로 확인
const rows = runtime.querySelectorAll(':scope > div');
results.part1.rows = [];
rows.forEach((row, i) => {
const children = row.children;
const rowData = { rowIndex: i, childCount: children.length, children: [] };
Array.from(children).forEach((child, j) => {
const cs = window.getComputedStyle(child);
const r = child.getBoundingClientRect();
rowData.children.push({
type: child.getAttribute('data-component-type') || 'unknown',
width: Math.round(r.width),
height: Math.round(r.height),
flexGrow: cs.flexGrow,
flexBasis: cs.flexBasis,
});
});
results.part1.rows.push(rowData);
});
}
}
// ========== Part 2: wrapper vs child 크기 확인 ==========
const comps = document.querySelectorAll('[data-component-id]');
results.part2 = [];
comps.forEach((comp) => {
const type = comp.getAttribute('data-component-type');
const child = comp.firstElementChild;
if (child) {
const childRect = child.getBoundingClientRect();
const compRect = comp.getBoundingClientRect();
results.part2.push({
type,
wrapper: `${Math.round(compRect.width)}x${Math.round(compRect.height)}`,
child: `${Math.round(childRect.width)}x${Math.round(childRect.height)}`,
overflow: childRect.width > compRect.width ? 'YES' : 'no',
});
}
});
// ========== 결과 출력 ==========
console.log('========== Part 1: Runtime 구조 ==========');
console.log(JSON.stringify(results.part1, null, 2));
console.log('\n========== Part 2: Wrapper vs Child ==========');
results.part2.forEach((r) => {
console.log(`${r.type}: wrapper=${r.wrapper}, child=${r.child}, overflow=${r.overflow}`);
});
// scale 값 추출 (transform에서)
if (results.part1?.innerDiv?.transform) {
const m = results.part1.innerDiv.transform.match(/scale\(([^)]+)\)/);
if (m) console.log('\n📐 Scale 값:', m[1]);
}
return results;
})();