diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index 8f671c21..c3c13a94 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -202,6 +202,66 @@ function FullWidthOverlayRow({ ); } +function ProportionalRenderer({ + components, + canvasWidth, + canvasHeight, + renderComponent, +}: ResponsiveGridRendererProps) { + const containerRef = useRef(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 ( +
0 ? `${maxBottom * ratio}px` : "200px" }} + > + {containerW > 0 && + topLevel.map((component) => { + const typeId = getComponentTypeId(component); + return ( +
+ {renderComponent(component)} +
+ ); + })} +
+ ); +} + export function ResponsiveGridRenderer({ components, canvasWidth, @@ -211,6 +271,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 ( + + ); + } const rows = groupComponentsIntoRows(topLevel); const processedRows: ProcessedRow[] = []; @@ -334,7 +406,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" : undefined, diff --git a/frontend/components/screen/table-options/FilterPanel.tsx b/frontend/components/screen/table-options/FilterPanel.tsx index 1b6104b6..02be2e9f 100644 --- a/frontend/components/screen/table-options/FilterPanel.tsx +++ b/frontend/components/screen/table-options/FilterPanel.tsx @@ -194,7 +194,7 @@ export const FilterPanel: React.FC = ({ 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 = ({ isOpen, onClose, onFiltersApplied {/* 너비 입력 */} = 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} /> - px + % ))} diff --git a/frontend/components/screen/table-options/TableSettingsModal.tsx b/frontend/components/screen/table-options/TableSettingsModal.tsx index 3d37c153..215ff2ef 100644 --- a/frontend/components/screen/table-options/TableSettingsModal.tsx +++ b/frontend/components/screen/table-options/TableSettingsModal.tsx @@ -136,7 +136,7 @@ export const TableSettingsModal: React.FC = ({ isOpen, onClose, onFilters inputType, enabled: false, filterType, - width: 200, + width: 25, }; }); @@ -271,7 +271,7 @@ export const TableSettingsModal: React.FC = ({ 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 = ({ isOpen, onClose, onFilters = 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" /> - px + % ))} diff --git a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx index c9d60136..0d917414 100644 --- a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx @@ -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 ( -
+
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" }} > {getDisplayText()} @@ -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 && (
- {activeFilters.map((filter) => ( -
{renderFilterInput(filter)}
- ))} + {activeFilters.map((filter) => { + const widthPercent = filter.width && filter.width >= 10 && filter.width <= 100 ? filter.width : 25; + return ( +
+ {renderFilterInput(filter)} +
+ ); + })} {/* 초기화 버튼 */}
diff --git a/scripts/analyze-company-info-layout.js b/scripts/analyze-company-info-layout.js new file mode 100644 index 00000000..8312df97 --- /dev/null +++ b/scripts/analyze-company-info-layout.js @@ -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; +})();