# 반응형 그리드 시스템 아키텍처 > 최종 업데이트: 2026-01-30 --- ## 1. 개요 ### 1.1 현재 문제 **컴포넌트 위치/크기가 픽셀 단위로 고정되어 반응형 미지원** ```json // 현재 DB 저장 방식 (screen_layouts_v2.layout_data) { "position": { "x": 1753, "y": 88 }, "size": { "width": 158, "height": 40 } } ``` | 화면 크기 | 결과 | |-----------|------| | 1920px (디자인 기준) | 정상 | | 1280px (노트북) | 오른쪽 버튼 잘림 | | 768px (태블릿) | 레이아웃 완전히 깨짐 | | 375px (모바일) | 사용 불가 | ### 1.2 목표 | 목표 | 설명 | |------|------| | PC 대응 | 1280px ~ 1920px | | 태블릿 대응 | 768px ~ 1024px | | 모바일 대응 | 320px ~ 767px | ### 1.3 해결 방향 ``` 현재: 픽셀 좌표 → position: absolute → 고정 레이아웃 변경: 그리드 셀 번호 → CSS Grid + ResizeObserver → 반응형 레이아웃 ``` --- ## 2. 현재 시스템 분석 ### 2.1 데이터 현황 ``` 총 레이아웃: 1,250개 총 컴포넌트: 5,236개 회사 수: 14개 테이블 크기: 약 3MB ``` ### 2.2 컴포넌트 타입별 분포 | 컴포넌트 | 수량 | shadcn 사용 | |----------|------|-------------| | v2-input | 1,914 | ✅ `@/components/ui/input` | | v2-button-primary | 1,549 | ✅ `@/components/ui/button` | | v2-table-search-widget | 355 | ✅ shadcn 기반 | | v2-select | 327 | ✅ `@/components/ui/select` | | v2-table-list | 285 | ✅ `@/components/ui/table` | | v2-media | 181 | ✅ shadcn 기반 | | v2-date | 132 | ✅ `@/components/ui/calendar` | | **v2-split-panel-layout** | **131** | ✅ shadcn 기반 (**반응형 필요**) | | v2-tabs-widget | 75 | ✅ shadcn 기반 | | 기타 | 287 | ✅ shadcn 기반 | | **합계** | **5,236** | **전부 shadcn** | ### 2.3 현재 렌더링 방식 ```tsx // frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx (라인 234-248) {components.map((child) => (
{renderer.renderChild(child)}
))} ``` ### 2.4 핵심 발견 ``` ✅ 이미 있는 것: - 12컬럼 그리드 설정 (gridSettings.columns: 12) - 그리드 스냅 기능 (snapToGrid: true) - shadcn/ui 기반 컴포넌트 (전체) ❌ 없는 것: - 그리드 셀 번호 저장 (현재 픽셀 저장) - 반응형 브레이크포인트 설정 - CSS Grid 기반 렌더링 - 분할 패널 반응형 처리 ``` ### 2.5 레이아웃 시스템 구조 현재 시스템에는 두 가지 레벨의 레이아웃이 존재합니다: #### 2.5.1 화면 레이아웃 (screen_layouts_v2) 화면 전체의 컴포넌트 배치를 담당합니다. ```json // DB 구조 { "version": "2.0", "components": [ { "id": "comp_1", "position": { "x": 100, "y": 50 }, ... }, { "id": "comp_2", "position": { "x": 500, "y": 50 }, ... }, { "id": "GridLayout_1", "position": { "x": 100, "y": 200 }, ... } ] } ``` **현재**: absolute 포지션으로 컴포넌트 배치 → **반응형 불가** #### 2.5.2 컴포넌트 레이아웃 (GridLayout, FlexboxLayout 등) 개별 레이아웃 컴포넌트 내부의 zone 배치를 담당합니다. | 컴포넌트 | 위치 | 내부 구조 | CSS Grid 사용 | |----------|------|-----------|---------------| | `GridLayout` | `layouts/grid/` | zones 배열 | ✅ 이미 사용 | | `FlexboxLayout` | `layouts/flexbox/` | zones 배열 | ❌ absolute | | `SplitLayout` | `layouts/split/` | left/right | ❌ flex | | `TabsLayout` | `layouts/` | tabs 배열 | ❌ 탭 구조 | | `CardLayout` | `layouts/card-layout/` | zones 배열 | ❌ flex | | `AccordionLayout` | `layouts/accordion/` | items 배열 | ❌ 아코디언 | #### 2.5.3 구조 다이어그램 ``` ┌─────────────────────────────────────────────────────────────────┐ │ screen_layouts_v2 (화면 전체) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 현재: absolute 포지션 → 반응형 불가 │ │ │ │ 변경: ResponsiveGridLayout (CSS Grid) → 반응형 가능 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────┐ │ │ │ v2-button │ │ v2-input │ │ GridLayout (컴포넌트) │ │ │ │ (shadcn) │ │ (shadcn) │ │ ┌─────────┬─────────────┐ │ │ │ └──────────┘ └──────────┘ │ │ zone1 │ zone2 │ │ │ │ │ │ (이미 │ (이미 │ │ │ │ │ │ CSS Grid│ CSS Grid) │ │ │ │ │ └─────────┴─────────────┘ │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.6 기존 레이아웃 컴포넌트 호환성 #### 2.6.1 GridLayout (기존 커스텀 그리드) ```tsx // frontend/lib/registry/layouts/grid/GridLayout.tsx // 이미 CSS Grid를 사용하고 있음! const gridStyle: React.CSSProperties = { display: "grid", gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`, gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`, gap: `${gridConfig.gap || 16}px`, }; ``` **호환성**: ✅ **완전 호환** - GridLayout은 화면 내 하나의 컴포넌트로 취급됨 - ResponsiveGridLayout이 GridLayout의 **위치만** 관리 - GridLayout 내부는 기존 방식 그대로 동작 #### 2.6.2 FlexboxLayout ```tsx // frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx // zone 내부에서 컴포넌트를 absolute로 배치 {zoneChildren.map((child) => (
{renderer.renderChild(child)}
))} ``` **호환성**: ✅ **호환** (내부는 기존 방식 유지) - FlexboxLayout 컴포넌트 자체의 위치는 ResponsiveGridLayout이 관리 - 내부 zone의 컴포넌트 배치는 기존 absolute 방식 유지 #### 2.6.3 SplitPanelLayout (분할 패널) **호환성**: ⚠️ **별도 수정 필요** - 외부 위치: ResponsiveGridLayout이 관리 ✅ - 내부 반응형: 별도 수정 필요 (모바일에서 상하 분할) #### 2.6.4 호환성 요약 | 컴포넌트 | 외부 배치 | 내부 동작 | 추가 수정 | |----------|----------|----------|-----------| | **v2-button, v2-input 등** | ✅ 반응형 | ✅ shadcn 그대로 | ❌ 불필요 | | **GridLayout** | ✅ 반응형 | ✅ CSS Grid 그대로 | ❌ 불필요 | | **FlexboxLayout** | ✅ 반응형 | ⚠️ absolute 유지 | ❌ 불필요 | | **SplitPanelLayout** | ✅ 반응형 | ❌ 좌우 고정 | ⚠️ 내부 반응형 추가 | | **TabsLayout** | ✅ 반응형 | ✅ 탭 그대로 | ❌ 불필요 | ### 2.7 동작 방식 비교 #### 변경 전 ``` 화면 로드 ↓ screen_layouts_v2에서 components 조회 ↓ 각 컴포넌트를 position.x, position.y로 absolute 배치 ↓ GridLayout 컴포넌트도 absolute로 배치됨 ↓ GridLayout 내부는 CSS Grid로 zone 배치 ↓ 결과: 화면 크기 변해도 모든 컴포넌트 위치 고정 ``` #### 변경 후 ``` 화면 로드 ↓ screen_layouts_v2에서 components 조회 ↓ layoutMode === "grid" 확인 ↓ ResponsiveGridLayout으로 렌더링 (CSS Grid) ↓ 각 컴포넌트를 grid.col, grid.colSpan으로 배치 ↓ 화면 크기 감지 (ResizeObserver) ↓ breakpoint에 따라 responsive.sm/md/lg 적용 ↓ GridLayout 컴포넌트도 반응형으로 배치됨 ↓ GridLayout 내부는 기존 CSS Grid로 zone 배치 (변경 없음) ↓ 결과: 화면 크기에 따라 컴포넌트 재배치 ``` --- ## 3. 기술 결정 ### 3.1 왜 Tailwind 동적 클래스가 아닌 CSS Grid + Inline Style인가? **Tailwind 동적 클래스의 한계**: ```tsx // ❌ 이건 안 됨 - Tailwind가 빌드 타임에 인식 못함 className={`col-start-${col} md:col-start-${mdCol}`} // ✅ 이것만 됨 - 정적 클래스 className="col-start-1 md:col-start-3" ``` Tailwind는 **빌드 타임**에 클래스를 스캔하므로, 런타임에 동적으로 생성되는 클래스는 인식하지 못합니다. **해결책: CSS Grid + Inline Style + ResizeObserver**: ```tsx // ✅ 올바른 방법
{component}
``` ### 3.2 역할 분담 | 영역 | 기술 | 설명 | |------|------|------| | **UI 컴포넌트** | shadcn/ui | 버튼, 인풋, 테이블 등 (이미 적용됨) | | **레이아웃 배치** | CSS Grid + Inline Style | 컴포넌트 위치, 크기, 반응형 | | **반응형 감지** | ResizeObserver | 화면 크기 감지 및 브레이크포인트 변경 | ``` ┌─────────────────────────────────────────────────────────┐ │ ResponsiveGridLayout (CSS Grid) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ shadcn │ │ shadcn │ │ shadcn │ │ │ │ Button │ │ Input │ │ Select │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────────────────────────────────────┐ │ │ │ shadcn Table │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## 4. 데이터 구조 변경 ### 4.1 현재 구조 (V2) ```json { "version": "2.0", "components": [{ "id": "comp_xxx", "url": "@/lib/registry/components/v2-button-primary", "position": { "x": 1753, "y": 88, "z": 1 }, "size": { "width": 158, "height": 40 }, "overrides": { ... } }] } ``` ### 4.2 변경 후 구조 (V2 + 그리드) ```json { "version": "2.0", "layoutMode": "grid", "components": [{ "id": "comp_xxx", "url": "@/lib/registry/components/v2-button-primary", "position": { "x": 1753, "y": 88, "z": 1 }, "size": { "width": 158, "height": 40 }, "grid": { "col": 11, "row": 2, "colSpan": 1, "rowSpan": 1 }, "responsive": { "sm": { "col": 1, "colSpan": 12 }, "md": { "col": 7, "colSpan": 6 }, "lg": { "col": 11, "colSpan": 1 } }, "overrides": { ... } }], "gridSettings": { "columns": 12, "rowHeight": 80, "gap": 16 } } ``` ### 4.3 필드 설명 | 필드 | 타입 | 설명 | |------|------|------| | `layoutMode` | string | "grid" (반응형 그리드 사용) | | `grid.col` | number | 시작 컬럼 (1-12) | | `grid.row` | number | 시작 행 (1부터) | | `grid.colSpan` | number | 차지하는 컬럼 수 | | `grid.rowSpan` | number | 차지하는 행 수 | | `responsive.sm` | object | 모바일 (< 768px) 설정 | | `responsive.md` | object | 태블릿 (768px ~ 1024px) 설정 | | `responsive.lg` | object | 데스크톱 (> 1024px) 설정 | ### 4.4 호환성 - `position`, `size` 필드는 유지 (디자인 모드 + 폴백용) - `layoutMode`가 없으면 기존 방식(absolute) 사용 - 마이그레이션 후에도 기존 화면 정상 동작 --- ## 5. 구현 상세 ### 5.1 그리드 변환 유틸리티 ```typescript // frontend/lib/utils/gridConverter.ts const DESIGN_WIDTH = 1920; const COLUMNS = 12; const COLUMN_WIDTH = DESIGN_WIDTH / COLUMNS; // 160px const ROW_HEIGHT = 80; /** * 픽셀 좌표를 그리드 셀 번호로 변환 */ export function pixelToGrid( position: { x: number; y: number }, size: { width: number; height: number } ): GridPosition { return { col: Math.max(1, Math.min(12, Math.round(position.x / COLUMN_WIDTH) + 1)), row: Math.max(1, Math.round(position.y / ROW_HEIGHT) + 1), colSpan: Math.max(1, Math.round(size.width / COLUMN_WIDTH)), rowSpan: Math.max(1, Math.round(size.height / ROW_HEIGHT)), }; } /** * 기본 반응형 설정 생성 */ export function getDefaultResponsive(grid: GridPosition): ResponsiveConfig { return { sm: { col: 1, colSpan: 12 }, // 모바일: 전체 너비 md: { col: Math.max(1, Math.round(grid.col / 2)), colSpan: Math.min(grid.colSpan * 2, 12) }, // 태블릿: 2배 확장 lg: { col: grid.col, colSpan: grid.colSpan }, // 데스크톱: 원본 }; } ``` ### 5.2 반응형 그리드 레이아웃 컴포넌트 ```tsx // frontend/lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx import React, { useRef, useState, useEffect } from "react"; type Breakpoint = "sm" | "md" | "lg"; interface ResponsiveGridLayoutProps { layout: LayoutData; isDesignMode: boolean; renderer: ComponentRenderer; } export function ResponsiveGridLayout({ layout, isDesignMode, renderer, }: ResponsiveGridLayoutProps) { const containerRef = useRef(null); const [breakpoint, setBreakpoint] = useState("lg"); // 화면 크기 감지 useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { const width = entries[0].contentRect.width; if (width < 768) setBreakpoint("sm"); else if (width < 1024) setBreakpoint("md"); else setBreakpoint("lg"); }); observer.observe(containerRef.current); return () => observer.disconnect(); }, []); const gridSettings = layout.gridSettings || { columns: 12, rowHeight: 80, gap: 16 }; return (
{layout.components .sort((a, b) => (a.grid?.row || 0) - (b.grid?.row || 0)) .map((component) => { // 반응형 설정 가져오기 const gridConfig = component.responsive?.[breakpoint] || component.grid; const { col, colSpan } = gridConfig; const rowSpan = component.grid?.rowSpan || 1; return (
{renderer.renderChild(component)}
); })}
); } ``` ### 5.3 브레이크포인트 훅 ```typescript // frontend/lib/registry/layouts/responsive-grid/useBreakpoint.ts import { useState, useEffect, RefObject } from "react"; type Breakpoint = "sm" | "md" | "lg"; export function useBreakpoint(containerRef: RefObject): Breakpoint { const [breakpoint, setBreakpoint] = useState("lg"); useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { const width = entries[0].contentRect.width; if (width < 768) setBreakpoint("sm"); else if (width < 1024) setBreakpoint("md"); else setBreakpoint("lg"); }); observer.observe(containerRef.current); return () => observer.disconnect(); }, [containerRef]); return breakpoint; } ``` ### 5.4 분할 패널 반응형 수정 ```tsx // frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx // 추가할 코드 const containerRef = useRef(null); const [isMobile, setIsMobile] = useState(false); useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { const width = entries[0].contentRect.width; setIsMobile(width < 768); }); observer.observe(containerRef.current); return () => observer.disconnect(); }, []); // 렌더링 부분 수정 return (
{/* 좌측/상단 패널 */}
{/* 우측/하단 패널 */}
); ``` --- ## 6. 렌더링 분기 처리 ```typescript // frontend/lib/registry/DynamicComponentRenderer.tsx function renderLayout(layout: LayoutData) { // layoutMode에 따라 분기 if (layout.layoutMode === "grid") { return ; } // 기존 방식 (폴백) return ; } ``` --- ## 7. 마이그레이션 ### 7.1 백업 ```sql -- 마이그레이션 전 백업 CREATE TABLE screen_layouts_v2_backup_20260130 AS SELECT * FROM screen_layouts_v2; ``` ### 7.2 마이그레이션 스크립트 ```sql -- grid, responsive 필드 추가 UPDATE screen_layouts_v2 SET layout_data = ( SELECT jsonb_set( jsonb_set( layout_data, '{layoutMode}', '"grid"' ), '{components}', ( SELECT jsonb_agg( comp || jsonb_build_object( 'grid', jsonb_build_object( 'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)), 'row', GREATEST(1, ROUND((comp->'position'->>'y')::NUMERIC / 80) + 1), 'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)), 'rowSpan', GREATEST(1, ROUND((comp->'size'->>'height')::NUMERIC / 80)) ), 'responsive', jsonb_build_object( 'sm', jsonb_build_object('col', 1, 'colSpan', 12), 'md', jsonb_build_object( 'col', GREATEST(1, ROUND((ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1) / 2.0)), 'colSpan', LEAST(ROUND((comp->'size'->>'width')::NUMERIC / 160) * 2, 12) ), 'lg', jsonb_build_object( 'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)), 'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)) ) ) ) ) FROM jsonb_array_elements(layout_data->'components') as comp ) ) ); ``` ### 7.3 롤백 ```sql -- 문제 발생 시 롤백 DROP TABLE screen_layouts_v2; ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; ``` --- ## 8. 동작 흐름 ### 8.1 데스크톱 (> 1024px) ``` ┌────────────────────────────────────────────────────────────┐ │ 1 2 3 4 5 6 7 8 9 10 │ 11 12 │ │ │ │ [버튼] │ │ ├────────────────────────────────────────────────────────────┤ │ │ │ 테이블 (12컬럼) │ │ │ └────────────────────────────────────────────────────────────┘ ``` ### 8.2 태블릿 (768px ~ 1024px) ``` ┌─────────────────────────────────────┐ │ 1 2 3 4 5 6 │ 7 8 9 10 11 12 │ │ │ [버튼] │ ├─────────────────────────────────────┤ │ │ │ 테이블 (12컬럼) │ │ │ └─────────────────────────────────────┘ ``` ### 8.3 모바일 (< 768px) ``` ┌──────────────────┐ │ [버튼] │ ← 12컬럼 (전체 너비) ├──────────────────┤ │ │ │ 테이블 (스크롤) │ ← 12컬럼 (전체 너비) │ │ └──────────────────┘ ``` ### 8.4 분할 패널 (반응형) **데스크톱**: ``` ┌─────────────────────────┬─────────────────────────┐ │ 좌측 패널 (60%) │ 우측 패널 (40%) │ └─────────────────────────┴─────────────────────────┘ ``` **모바일**: ``` ┌─────────────────────────┐ │ 상단 패널 (이전 좌측) │ ├─────────────────────────┤ │ 하단 패널 (이전 우측) │ └─────────────────────────┘ ``` --- ## 9. 수정 파일 목록 ### 9.1 새로 생성 | 파일 | 설명 | |------|------| | `lib/utils/gridConverter.ts` | 픽셀 → 그리드 변환 유틸리티 | | `lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx` | CSS Grid 레이아웃 | | `lib/registry/layouts/responsive-grid/useBreakpoint.ts` | ResizeObserver 훅 | | `lib/registry/layouts/responsive-grid/index.ts` | 모듈 export | ### 9.2 수정 | 파일 | 수정 내용 | |------|-----------| | `lib/registry/DynamicComponentRenderer.tsx` | layoutMode 분기 추가 | | `components/screen/ScreenDesigner.tsx` | 저장 시 grid/responsive 생성 | | `v2-split-panel-layout/SplitPanelLayoutComponent.tsx` | 반응형 처리 추가 | ### 9.3 수정 없음 | 파일 | 이유 | |------|------| | `v2-input/*` | 레이아웃과 무관 (shadcn 그대로) | | `v2-button-primary/*` | 레이아웃과 무관 (shadcn 그대로) | | `v2-table-list/*` | 레이아웃과 무관 (shadcn 그대로) | | `v2-select/*` | 레이아웃과 무관 (shadcn 그대로) | | **...모든 v2 컴포넌트** | **수정 불필요** | --- ## 10. 작업 일정 | Phase | 작업 | 파일 | 시간 | |-------|------|------|------| | **1** | 그리드 변환 유틸리티 | `gridConverter.ts` | 2시간 | | **1** | 브레이크포인트 훅 | `useBreakpoint.ts` | 1시간 | | **2** | ResponsiveGridLayout | `ResponsiveGridLayout.tsx` | 4시간 | | **2** | 렌더링 분기 처리 | `DynamicComponentRenderer.tsx` | 1시간 | | **3** | 저장 로직 수정 | `ScreenDesigner.tsx` | 2시간 | | **3** | 분할 패널 반응형 | `SplitPanelLayoutComponent.tsx` | 3시간 | | **4** | 마이그레이션 스크립트 | SQL | 2시간 | | **4** | 마이그레이션 실행 | - | 1시간 | | **5** | 테스트 및 버그 수정 | - | 4시간 | | | **합계** | | **약 2.5일** | --- ## 11. 체크리스트 ### 개발 전 - [ ] screen_layouts_v2 백업 완료 - [ ] 개발 환경에서 테스트 데이터 준비 ### Phase 1: 유틸리티 - [ ] `gridConverter.ts` 생성 - [ ] `useBreakpoint.ts` 생성 - [ ] 단위 테스트 작성 ### Phase 2: 레이아웃 - [ ] `ResponsiveGridLayout.tsx` 생성 - [ ] `DynamicComponentRenderer.tsx` 분기 추가 - [ ] 기존 화면 정상 동작 확인 ### Phase 3: 저장/수정 - [ ] `ScreenDesigner.tsx` 저장 로직 수정 - [ ] `SplitPanelLayoutComponent.tsx` 반응형 추가 - [ ] 디자인 모드 테스트 ### Phase 4: 마이그레이션 - [ ] 마이그레이션 스크립트 테스트 (개발 DB) - [ ] 운영 DB 백업 - [ ] 마이그레이션 실행 - [ ] 검증 ### Phase 5: 테스트 - [ ] PC (1920px, 1280px) 테스트 - [ ] 태블릿 (768px, 1024px) 테스트 - [ ] 모바일 (375px, 414px) 테스트 - [ ] 분할 패널 화면 테스트 - [ ] GridLayout 컴포넌트 포함 화면 테스트 - [ ] FlexboxLayout 컴포넌트 포함 화면 테스트 - [ ] TabsLayout 컴포넌트 포함 화면 테스트 - [ ] 중첩 레이아웃 (GridLayout 안에 컴포넌트) 테스트 --- ## 12. 리스크 및 대응 | 리스크 | 영향 | 대응 | |--------|------|------| | 마이그레이션 실패 | 높음 | 백업 테이블에서 즉시 롤백 | | 기존 화면 깨짐 | 중간 | `layoutMode` 없으면 기존 방식 사용 (폴백) | | 디자인 모드 혼란 | 낮음 | position/size 필드 유지 | | GridLayout 내부 깨짐 | 낮음 | 내부는 기존 방식 유지, 외부 배치만 변경 | | 중첩 레이아웃 문제 | 낮음 | 각 레이아웃 컴포넌트는 독립적으로 동작 | --- ## 13. 참고 - [COMPONENT_LAYOUT_V2_ARCHITECTURE.md](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) - V2 아키텍처 - [CSS Grid Layout - MDN](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Grid_Layout) - [ResizeObserver - MDN](https://developer.mozilla.org/ko/docs/Web/API/ResizeObserver) - [shadcn/ui](https://ui.shadcn.com/) - 컴포넌트 라이브러리