Compare commits
No commits in common. "dc37ad4471f61cbae8ed6ea7014dd66cda8808bd" and "cada8cd4b00a2bca1892ec4c05db8a04f9474b64" have entirely different histories.
dc37ad4471
...
cada8cd4b0
|
|
@ -204,66 +204,6 @@ 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({
|
export function ResponsiveGridRenderer({
|
||||||
components,
|
components,
|
||||||
canvasWidth,
|
canvasWidth,
|
||||||
|
|
@ -273,18 +213,6 @@ export function ResponsiveGridRenderer({
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
const topLevel = components.filter((c) => !c.parentId);
|
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 rows = groupComponentsIntoRows(topLevel);
|
||||||
const processedRows: ProcessedRow[] = [];
|
const processedRows: ProcessedRow[] = [];
|
||||||
|
|
@ -429,7 +357,7 @@ export function ResponsiveGridRenderer({
|
||||||
style={{
|
style={{
|
||||||
width: isFullWidth ? "100%" : undefined,
|
width: isFullWidth ? "100%" : undefined,
|
||||||
flexBasis: useFlexHeight ? undefined : flexBasis,
|
flexBasis: useFlexHeight ? undefined : flexBasis,
|
||||||
flexGrow: percentWidth,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
minWidth: isMobile ? "100%" : undefined,
|
minWidth: isMobile ? "100%" : undefined,
|
||||||
minHeight: useFlexHeight ? "300px" : (component.size?.height
|
minHeight: useFlexHeight ? "300px" : (component.size?.height
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
operator: "contains", // 기본 연산자
|
operator: "contains", // 기본 연산자
|
||||||
value: "",
|
value: "",
|
||||||
filterType: cf.filterType,
|
filterType: cf.filterType,
|
||||||
width: cf.width && cf.width >= 10 && cf.width <= 100 ? cf.width : 25,
|
width: cf.width || 200, // 너비 포함 (기본 200px)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// localStorage에 저장 (화면별로 독립적)
|
// localStorage에 저장 (화면별로 독립적)
|
||||||
|
|
@ -334,20 +334,20 @@ export const FilterPanel: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied
|
||||||
{/* 너비 입력 */}
|
{/* 너비 입력 */}
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
|
value={filter.width || 200}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newWidth = Math.min(100, Math.max(10, parseInt(e.target.value) || 25));
|
const newWidth = parseInt(e.target.value) || 200;
|
||||||
setColumnFilters((prev) =>
|
setColumnFilters((prev) =>
|
||||||
prev.map((f) => (f.columnName === filter.columnName ? { ...f, width: newWidth } : f)),
|
prev.map((f) => (f.columnName === filter.columnName ? { ...f, width: newWidth } : f)),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
disabled={!filter.enabled}
|
disabled={!filter.enabled}
|
||||||
placeholder="25"
|
placeholder="너비"
|
||||||
className="h-8 w-[80px] text-xs sm:h-9 sm:text-sm"
|
className="h-8 w-[80px] text-xs sm:h-9 sm:text-sm"
|
||||||
min={10}
|
min={50}
|
||||||
max={100}
|
max={500}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground text-xs">%</span>
|
<span className="text-muted-foreground text-xs">px</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
|
||||||
inputType,
|
inputType,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
filterType,
|
filterType,
|
||||||
width: 25,
|
width: 200,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -271,7 +271,7 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
|
||||||
operator: "contains",
|
operator: "contains",
|
||||||
value: "",
|
value: "",
|
||||||
filterType: f.filterType,
|
filterType: f.filterType,
|
||||||
width: f.width && f.width >= 10 && f.width <= 100 ? f.width : 25,
|
width: f.width || 200,
|
||||||
}));
|
}));
|
||||||
onFiltersApplied?.(activeFilters);
|
onFiltersApplied?.(activeFilters);
|
||||||
|
|
||||||
|
|
@ -498,15 +498,15 @@ export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFilters
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={10}
|
min={100}
|
||||||
max={100}
|
max={400}
|
||||||
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
|
value={filter.width || 200}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleFilterWidthChange(filter.columnName, Math.min(100, Math.max(10, parseInt(e.target.value) || 25)))
|
handleFilterWidthChange(filter.columnName, parseInt(e.target.value) || 200)
|
||||||
}
|
}
|
||||||
className="h-7 w-16 text-center text-xs"
|
className="h-7 w-16 text-center text-xs"
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground text-xs">%</span>
|
<span className="text-muted-foreground text-xs">px</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -648,11 +648,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
const renderFilterInput = (filter: TableFilter) => {
|
const renderFilterInput = (filter: TableFilter) => {
|
||||||
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
|
const column = currentTable?.columns.find((c) => c.columnName === filter.columnName);
|
||||||
const value = filterValues[filter.columnName] || "";
|
const value = filterValues[filter.columnName] || "";
|
||||||
|
const width = filter.width || 200; // 기본 너비 200px
|
||||||
|
|
||||||
switch (filter.filterType) {
|
switch (filter.filterType) {
|
||||||
case "date":
|
case "date":
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full sm:w-auto" style={{ maxWidth: `${width}px` }}>
|
||||||
<ModernDatePicker
|
<ModernDatePicker
|
||||||
label={column?.columnLabel || filter.columnName}
|
label={column?.columnLabel || filter.columnName}
|
||||||
value={value ? (typeof value === "string" ? { from: new Date(value), to: new Date(value) } : value) : {}}
|
value={value ? (typeof value === "string" ? { from: new Date(value), to: new Date(value) } : value) : {}}
|
||||||
|
|
@ -675,8 +676,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
type="number"
|
type="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.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:text-sm"
|
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={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||||
placeholder={column?.columnLabel}
|
placeholder={column?.columnLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -725,10 +726,10 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
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: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:w-auto sm:text-sm",
|
||||||
selectedValues.length === 0 && "text-muted-foreground",
|
selectedValues.length === 0 && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
style={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||||
>
|
>
|
||||||
<span className="truncate">{getDisplayText()}</span>
|
<span className="truncate">{getDisplayText()}</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
|
@ -780,8 +781,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => handleFilterChange(filter.columnName, e.target.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:text-sm"
|
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={{ height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
style={{ maxWidth: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
|
||||||
placeholder={column?.columnLabel}
|
placeholder={column?.columnLabel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -801,18 +802,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
{/* 필터 입력 필드들 */}
|
{/* 필터 입력 필드들 */}
|
||||||
{activeFilters.length > 0 && (
|
{activeFilters.length > 0 && (
|
||||||
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
||||||
{activeFilters.map((filter) => {
|
{activeFilters.map((filter) => (
|
||||||
const widthPercent = filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25;
|
<div key={filter.columnName}>{renderFilterInput(filter)}</div>
|
||||||
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">
|
<Button variant="outline" size="sm" onClick={handleResetFilters} className="h-9 shrink-0 text-xs sm:text-sm">
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export function TableSearchWidgetConfigPanel({
|
||||||
columnName: "",
|
columnName: "",
|
||||||
columnLabel: "",
|
columnLabel: "",
|
||||||
filterType: "text",
|
filterType: "text",
|
||||||
width: 25,
|
width: 200,
|
||||||
};
|
};
|
||||||
const updatedFilters = [...localPresetFilters, newFilter];
|
const updatedFilters = [...localPresetFilters, newFilter];
|
||||||
setLocalPresetFilters(updatedFilters);
|
setLocalPresetFilters(updatedFilters);
|
||||||
|
|
@ -346,15 +346,15 @@ export function TableSearchWidgetConfigPanel({
|
||||||
|
|
||||||
{/* 너비 */}
|
{/* 너비 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-[10px] sm:text-xs mb-1">너비 (%)</Label>
|
<Label className="text-[10px] sm:text-xs mb-1">너비 (px)</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25}
|
value={filter.width || 200}
|
||||||
onChange={(e) => updateFilter(filter.id, "width", Math.min(100, Math.max(10, parseInt(e.target.value) || 25)))}
|
onChange={(e) => updateFilter(filter.id, "width", parseInt(e.target.value))}
|
||||||
placeholder="25"
|
placeholder="200"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
min={10}
|
min={100}
|
||||||
max={100}
|
max={500}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
/**
|
|
||||||
* 회사 기본정보 화면 - 컴포넌트 렌더링 비율 정밀 분석
|
|
||||||
*
|
|
||||||
* 사용법: 브라우저에서 회사 기본정보 화면을 연 상태에서
|
|
||||||
* 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;
|
|
||||||
})();
|
|
||||||
Loading…
Reference in New Issue