+```
+
+### 4.5 ๊ณตํต ์์ ํ์ผ ์์ฑ
+
+```typescript
+// frontend/lib/constants/responsive.ts
+
+export const RESPONSIVE_CONFIG = {
+ DESIGN_WIDTH: 1920,
+ DESIGN_HEIGHT: 1080,
+ MIN_WIDTH: 1280,
+ MAX_WIDTH: 1920,
+} as const;
+
+export function toPercentX(pixelX: number): string {
+ return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
+}
+
+export function toPercentWidth(pixelWidth: number): string {
+ return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
+}
+```
+
+---
+
+## 5. ๊ฐ์ ์๋ฎฌ๋ ์ด์
+
+### 5.1 ์๋ฎฌ๋ ์ด์
์๋๋ฆฌ์ค
+
+**ํ
์คํธ ํ๋ฉด**: screen_id = 68 (์์ฃผ ๋ชฉ๋ก)
+```json
+{
+ "components": [
+ {
+ "id": "comp_1895",
+ "url": "v2-table-list",
+ "position": { "x": 8, "y": 128 },
+ "size": { "width": 1904, "height": 600 }
+ },
+ {
+ "id": "comp_1896",
+ "url": "v2-button-primary",
+ "position": { "x": 1753, "y": 88 },
+ "size": { "width": 158, "height": 40 }
+ },
+ {
+ "id": "comp_1897",
+ "url": "v2-button-primary",
+ "position": { "x": 1594, "y": 88 },
+ "size": { "width": 158, "height": 40 }
+ },
+ {
+ "id": "comp_1898",
+ "url": "v2-button-primary",
+ "position": { "x": 1436, "y": 88 },
+ "size": { "width": 158, "height": 40 }
+ }
+ ]
+}
+```
+
+### 5.2 ํ์ฌ ๋ฐฉ์ ์๋ฎฌ๋ ์ด์
+
+**1920px ํ๋ฉด**:
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ [๋ถ๋ฆฌ] [์ ์ฅ] [์์ ] [์ญ์ ] โ
+โ 1277 1436 1594 1753 โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ x=8 x=1904 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ํ
์ด๋ธ (width: 1904px) โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ
์ ์ ํ์
+```
+
+**1280px ํ๋ฉด (ํ์ฌ scale ๋ฐฉ์)**:
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ scale(0.67) ์ ์ฉ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ [๋ถ๋ฆฌ][์ ][์][์ญ] โ โ โ ์ ์ฒด ์ถ์, ํฐํธ ์์์ง
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
+โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
+โ โ โ ํ
์ด๋ธ (์ถ์๋จ) โ โ โ
+โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ
+โ (์ฌ๋ฐฑ ๋ฐ์) โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๏ธ ์๋ํ์ง๋ง ํฐํธ/์ฌ๋ฐฑ ๋ฌธ์
+```
+
+### 5.3 ํผ์ผํธ ๋ฐฉ์ ์๋ฎฌ๋ ์ด์
+
+**๋ณํ ๊ณ์ฐ**:
+```
+ํ
์ด๋ธ:
+ x: 8px โ 8/1920 = 0.42%
+ width: 1904px โ 1904/1920 = 99.17%
+
+์ญ์ ๋ฒํผ:
+ x: 1753px โ 1753/1920 = 91.30%
+ width: 158px โ 158/1920 = 8.23%
+
+์์ ๋ฒํผ:
+ x: 1594px โ 1594/1920 = 83.02%
+ width: 158px โ 158/1920 = 8.23%
+
+์ ์ฅ ๋ฒํผ:
+ x: 1436px โ 1436/1920 = 74.79%
+ width: 158px โ 158/1920 = 8.23%
+
+๋ถ๋ฆฌ ๋ฒํผ:
+ x: 1277px โ 1277/1920 = 66.51%
+ width: 158px โ 158/1920 = 8.23%
+```
+
+**1920px ํ๋ฉด**:
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ [๋ถ๋ฆฌ] [์ ์ฅ] [์์ ] [์ญ์ ] โ
+โ 66.5% 74.8% 83.0% 91.3% โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ 0.42% 99.6% โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ํ
์ด๋ธ (width: 99.17%) โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ
์ ์ ํ์ (1920px์ ๋์ผ)
+```
+
+**1280px ํ๋ฉด (ํผ์ผํธ ๋ฐฉ์)**:
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ [๋ถ๋ฆฌ][์ ์ฅ][์์ ][์ญ์ ] โ
+โ 66.5% 74.8% 83.0% 91.3% โ
+โ = 851 957 1063 1169 โ โ ํ๋ฉด ์์ ํ์!
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ 0.42% 99.6% โ
+โ = 5px = 1275 โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ ํ
์ด๋ธ (width: 99.17%) โ โ โ ํ๋ฉด ๋๋น์ ๋ง๊ฒ ์กฐ์
+โ โ = 1280 * 0.9917 = 1269px โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ
๋น์จ ์ ์ง, ํ๋ฉด ์์ ํ์, ํฐํธ ํฌ๊ธฐ ์ ์ง
+```
+
+### 5.4 ๋ฒํผ ๊ฐ๊ฒฉ ๊ฒ์ฆ
+
+**1920px**:
+```
+๋ถ๋ฆฌ: 1277px, ๋๋น 158px โ ๋: 1435px
+์ ์ฅ: 1436px (๊ฐ๊ฒฉ: 1px)
+์์ : 1594px (๊ฐ๊ฒฉ: 1px)
+์ญ์ : 1753px (๊ฐ๊ฒฉ: 1px)
+```
+
+**1280px (ํผ์ผํธ ๋ณํ ํ)**:
+```
+๋ถ๋ฆฌ: 1280 * 0.665 = 851px, ๋๋น 1280 * 0.082 = 105px โ ๋: 956px
+์ ์ฅ: 1280 * 0.748 = 957px (๊ฐ๊ฒฉ: 1px) โ
+์์ : 1280 * 0.830 = 1063px (๊ฐ๊ฒฉ: 1px) โ
+์ญ์ : 1280 * 0.913 = 1169px (๊ฐ๊ฒฉ: 1px) โ
+```
+
+**๊ฒฐ๋ก **: ๋ฒํผ ๊ฐ๊ฒฉ ๋น์จ๋ ์ ์ง๋จ
+
+---
+
+## 6. ์ฃ์ง ์ผ์ด์ค ๊ฒ์ฆ
+
+### 6.1 ๋ถํ ํจ๋ (SplitPanelLayout)
+
+**ํ์ฌ ๋์**:
+- ์ข์ธก ํจ๋: 60% ๋๋น
+- ์ฐ์ธก ํจ๋: 40% ๋๋น
+- **์ด๋ฏธ ํผ์ผํธ ๊ธฐ๋ฐ!**
+
+**์๋ฎฌ๋ ์ด์
**:
+```
+1920px: ์ข์ธก 1152px, ์ฐ์ธก 768px
+1280px: ์ข์ธก 768px, ์ฐ์ธก 512px
+โ
์๋์ผ๋ก ๋น์จ ์ ์ง๋จ
+```
+
+**๋ถํ ํจ๋ ๋ด๋ถ ์ปดํฌ๋ํธ**:
+- ๋ฌธ์ : ๋ด๋ถ ์ปดํฌ๋ํธ๊ฐ ํฝ์
๊ณ ์ ์ด๋ฉด ๊นจ์ง
+- ํด๊ฒฐ: ๋ถํ ํจ๋ ๋ด๋ถ๋ ํผ์ผํธ ์ ์ฉ ํ์
+
+### 6.2 ํ
์ด๋ธ ์ปดํฌ๋ํธ (TableList)
+
+**ํ์ฌ**:
+- ํ
์ด๋ธ ์์ฒด๋ ์ปจํ
์ด๋ ๋๋น 100% ์ฌ์ฉ
+- ์ปฌ๋ผ ๋๋น๋ ๋ด๋ถ์ ์ผ๋ก ์กฐ์
+
+**์๋ฎฌ๋ ์ด์
**:
+```
+1920px: ํ
์ด๋ธ ์ปจํ
์ด๋ width: 99.17% = 1904px
+1280px: ํ
์ด๋ธ ์ปจํ
์ด๋ width: 99.17% = 1269px
+โ
ํ
์ด๋ธ์ด ์๋์ผ๋ก ์กฐ์ ๋จ
+```
+
+### 6.3 ์์ ์ปดํฌ๋ํธ ์๋ ์์น
+
+**ํ์ฌ ์ฝ๋ (page.tsx ๋ผ์ธ 744-745)**:
+```typescript
+const relativeChildComponent = {
+ position: {
+ x: child.position.x - component.position.x,
+ y: child.position.y - component.position.y,
+ },
+};
+```
+
+**๋ฌธ์ **: ์๋ ์ขํ๋ ํฝ์
๊ธฐ๋ฐ
+
+**ํด๊ฒฐ**: ๋ถ๋ชจ ๊ธฐ์ค ํผ์ผํธ๋ก ๋ณํ
+```typescript
+const relativeChildComponent = {
+ position: {
+ // ๋ถ๋ชจ ๋๋น ๊ธฐ์ค ํผ์ผํธ
+ xPercent: ((child.position.x - component.position.x) / component.size.width) * 100,
+ y: child.position.y - component.position.y,
+ },
+};
+```
+
+### 6.4 ๋๋๊ทธ ์ค ๋๋กญ (๋์์ธ ๋ชจ๋)
+
+**ScreenDesigner.tsx**:
+- ๋๋กญ ์์น๋ ์ฌ์ ํ ํฝ์
๋ก ์ ์ฅ
+- ๋ ๋๋ง ์์๋ง ํผ์ผํธ๋ก ๋ณํ
+- **์ ์ฅ ๋ฐฉ์ ๋ณ๊ฒฝ ์์!**
+
+**์๋ฎฌ๋ ์ด์
**:
+```
+1. ๋์์ด๋๊ฐ 1920px ํ๋ฉด์์ ๋ฒํผ ๋๋กญ
+2. position: { x: 1753, y: 88 } ์ ์ฅ (ํฝ์
)
+3. ๋ ๋๋ง ์ 91.3%๋ก ๋ณํ
+4. 1280px ํ๋ฉด์์๋ ์ ์ ํ์
+โ
๋์์ธ ๋ชจ๋ ํธํ
+```
+
+### 6.5 ๋ชจ๋ฌ ๋ด ํ๋ฉด
+
+**ScreenModal.tsx (๋ผ์ธ 620-621)**:
+```typescript
+x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
+y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
+```
+
+**๋ฌธ์ **: ์คํ์
๊ณ์ฐ์ด ํฝ์
๊ธฐ๋ฐ
+
+**ํด๊ฒฐ**: ๋ชจ๋ฌ ์ปจํ
์ด๋๋ ํผ์ผํธ ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ
+```typescript
+// ๋ชจ๋ฌ ์ปจํ
์ด๋ ๋๋น ๊ธฐ์ค์ผ๋ก ํผ์ผํธ ๊ณ์ฐ
+const modalWidth = containerRef.current?.clientWidth || DESIGN_WIDTH;
+const xPercent = ((position.x - offsetX) / DESIGN_WIDTH) * 100;
+```
+
+---
+
+## 7. ์ ์ฌ์ ๋ฌธ์ ๋ฐ ํด๊ฒฐ์ฑ
+
+### 7.1 ์ต์ ๋๋น ๋ฌธ์
+
+**๋ฌธ์ **: ๋ฒํผ์ด ๋๋ฌด ์์์ง ์ ์์
+```
+158px ๋ฒํผ โ 1280px ํ๋ฉด์์ 105px
+โ ํ
์คํธ๊ฐ ์๋ฆด ์ ์์
+```
+
+**ํด๊ฒฐ**: min-width ์ค์
+```css
+min-width: 80px;
+```
+
+### 7.2 ๊ฒน์นจ ๋ฌธ์
+
+**๋ฌธ์ **: ํ๋ฉด์ด ์์์ง๋ฉด ์ปดํฌ๋ํธ๊ฐ ๊ฒน์น ์ ์์
+
+**์๋ฎฌ๋ ์ด์
**:
+```
+1920px: ๋ฒํผ 4๊ฐ๊ฐ ๊ฐ๊ฒฉ 1px๋ก ๋ฐฐ์น
+1280px: ๋ฒํผ 4๊ฐ๊ฐ ๊ฐ๊ฒฉ 1px๋ก ๋ฐฐ์น (๋น์จ ์ ์ง)
+โ
๊ฒน์น์ง ์์ (๊ฐ๊ฒฉ๋ ๋น์จ๋ก ์ถ์)
+```
+
+### 7.3 ํฐํธ ํฌ๊ธฐ
+
+**ํ์ฌ**: ํฐํธ๋ px ๊ณ ์
+**๋ณ๊ฒฝ ํ**: ํฐํธ ํฌ๊ธฐ ์ ์ง (scale์ด ์๋๋ฏ๋ก)
+
+**๊ฒฐ๊ณผ**: ํฐํธ ํฌ๊ธฐ๋ ๊ทธ๋๋ก, ๋ ์ด์์๋ง ๋น์จ ์กฐ์
+โ
๊ฐ๋
์ฑ ์ ์ง
+
+### 7.4 height ์ฒ๋ฆฌ
+
+**๊ฒฐ์ **: height๋ ํฝ์
์ ์ง
+- ์ด์ : ์ธ๋ก ์คํฌ๋กค์ ์์ฐ์ค๋ฌ์
+- ์ธ๋ก ๋ฐ์ํ์ ๋ถํ์ (PC ํ๊ฒฝ)
+
+---
+
+## 8. ํธํ์ฑ ๊ฒ์ฆ
+
+### 8.1 ๊ธฐ์กด ํ๋ฉด ํธํ
+
+| ํญ๋ชฉ | ํธํ ์ฌ๋ถ | ์ด์ |
+|------|----------|------|
+| ์ผ๋ฐ ๋ฒํผ | โ
| ํผ์ผํธ๋ก ๋ณํ, ์์น ์ ์ง |
+| ํ
์ด๋ธ | โ
| ์ปจํ
์ด๋ ๋น์จ ์ ์ง |
+| ๋ถํ ํจ๋ | โ
| ์ด๋ฏธ ํผ์ผํธ ๊ธฐ๋ฐ |
+| ํญ ๋ ์ด์์ | โ
| ์ปจํ
์ด๋ ๋น์จ ์ ์ง |
+| ๊ทธ๋ฆฌ๋ ๋ ์ด์์ | โ
| ๋ด๋ถ๋ ๊ธฐ์กด ๋ฐฉ์ |
+| ์ธํ ํ๋ | โ
| ์ปจํ
์ด๋ ๋น์จ ์ ์ง |
+
+### 8.2 ๋์์ธ ๋ชจ๋ ํธํ
+
+| ํญ๋ชฉ | ํธํ ์ฌ๋ถ | ์ด์ |
+|------|----------|------|
+| ๋๋๊ทธ ์ค ๋๋กญ | โ
| ์ ์ฅ์ ํฝ์
, ๋ ๋๋ง๋ง ํผ์ผํธ |
+| ๋ฆฌ์ฌ์ด์ฆ | โ
| ์ ์ฅ์ ํฝ์
, ๋ ๋๋ง๋ง ํผ์ผํธ |
+| ๊ทธ๋ฆฌ๋ ์ค๋
| โ
| ์ค๋
์ ํฝ์
๊ธฐ์ค ์ ์ง |
+| ๋ฏธ๋ฆฌ๋ณด๊ธฐ | โ
| ๋ ๋๋ง ๋์ผ ๋ฐฉ์ |
+
+### 8.3 API ํธํ
+
+| ํญ๋ชฉ | ํธํ ์ฌ๋ถ | ์ด์ |
+|------|----------|------|
+| DB ์ ์ฅ | โ
| ๊ตฌ์กฐ ๋ณ๊ฒฝ ์์ (ํฝ์
์ ์ฅ) |
+| API ์๋ต | โ
| ๊ตฌ์กฐ ๋ณ๊ฒฝ ์์ |
+| V2 ๋ณํ | โ
| ๋ณํ ๋ก์ง ๋ณ๊ฒฝ ์์ |
+
+---
+
+## 9. ๊ตฌํ ์์
+
+### Phase 1: ๊ณตํต ์ ํธ๋ฆฌํฐ ์์ฑ (30๋ถ)
+
+```typescript
+// frontend/lib/constants/responsive.ts
+export const RESPONSIVE_CONFIG = {
+ DESIGN_WIDTH: 1920,
+} as const;
+
+export function toPercentX(pixelX: number): string {
+ return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
+}
+
+export function toPercentWidth(pixelWidth: number): string {
+ return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`;
+}
+```
+
+### Phase 2: RealtimePreviewDynamic.tsx ์์ (1์๊ฐ)
+
+1. import ์ถ๊ฐ
+2. baseStyle์ left, width๋ฅผ ํผ์ผํธ๋ก ๋ณ๊ฒฝ
+3. ๋ถํ ํจ๋ ์ ๋ฒํผ ์กฐ์ ๋ก์ง๋ ํผ์ผํธ ์ ์ฉ
+
+### Phase 3: AutoRegisteringComponentRenderer.ts ์์ (30๋ถ)
+
+1. import ์ถ๊ฐ
+2. getComponentStyle()์ left, width๋ฅผ ํผ์ผํธ๋ก ๋ณ๊ฒฝ
+
+### Phase 4: page.tsx ์์ (1์๊ฐ)
+
+1. scale ๋ก์ง ์ ๊ฑฐ ๋๋ ์์
+2. ์ปจํ
์ด๋ width: 100%๋ก ๋ณ๊ฒฝ
+3. ์์ ์ปดํฌ๋ํธ ์๋ ์์น ๊ณ์ฐ ์์
+
+### Phase 5: ํ
์คํธ (1์๊ฐ)
+
+1. 1920px ํ๋ฉด์์ ๊ธฐ์กด ํ๋ฉด ์ ์ ๋์ ํ์ธ
+2. 1280px ํ๋ฉด์ผ๋ก ์ถ์ ํ
์คํธ
+3. ๋ถํ ํจ๋ ํ๋ฉด ํ
์คํธ
+4. ๋์์ธ ๋ชจ๋ ํ
์คํธ
+
+---
+
+## 10. ์ต์ข
์ฒดํฌ๋ฆฌ์คํธ
+
+### ๊ตฌํ ์
+
+- [ ] ํ์ฌ ๋์ํ๋ ํ๋ฉด ์คํฌ๋ฆฐ์ท ์บก์ฒ (๋น๊ต์ฉ)
+- [ ] ํ
์คํธ ํ๋ฉด ๋ชฉ๋ก ์ ์
+
+### ๊ตฌํ ์ค
+
+- [ ] responsive.ts ์์ฑ
+- [ ] RealtimePreviewDynamic.tsx ์์
+- [ ] AutoRegisteringComponentRenderer.ts ์์
+- [ ] page.tsx ์์
+
+### ๊ตฌํ ํ
+
+- [ ] 1920px ํ๋ฉด ํ
์คํธ
+- [ ] 1440px ํ๋ฉด ํ
์คํธ
+- [ ] 1280px ํ๋ฉด ํ
์คํธ
+- [ ] ๋ถํ ํจ๋ ํ๋ฉด ํ
์คํธ
+- [ ] ๋์์ธ ๋ชจ๋ ํ
์คํธ
+- [ ] ๋ชจ๋ฌ ๋ด ํ๋ฉด ํ
์คํธ
+
+---
+
+## 11. ์์ ์์ ์๊ฐ
+
+| ์์
| ์๊ฐ |
+|------|------|
+| ์ ํธ๋ฆฌํฐ ์์ฑ | 30๋ถ |
+| RealtimePreviewDynamic.tsx | 1์๊ฐ |
+| AutoRegisteringComponentRenderer.ts | 30๋ถ |
+| page.tsx | 1์๊ฐ |
+| ํ
์คํธ | 1์๊ฐ |
+| **ํฉ๊ณ** | **4์๊ฐ** |
+
+---
+
+## 12. ๊ฒฐ๋ก
+
+**ํผ์ผํธ ๊ธฐ๋ฐ ๋ฐฐ์น**๊ฐ PC ๋ฐ์ํ์ ๊ฐ์ฅ ํ์คํ ํด๊ฒฐ์ฑ
์
๋๋ค.
+
+| ํญ๋ชฉ | scale ๋ฐฉ์ | ํผ์ผํธ ๋ฐฉ์ |
+|------|-----------|------------|
+| ํฐํธ ํฌ๊ธฐ | ์ถ์๋จ | **์ ์ง** |
+| ๋ ์ด์์ ๋น์จ | ์ ์ง | **์ ์ง** |
+| ํด๋ฆญ ์์ญ | ์ค์ฐจ ๊ฐ๋ฅ | **์ ํ** |
+| ๊ตฌํ ๋ณต์ก๋ | ๋ฎ์ | **์ค๊ฐ** |
+| ์ง์ ํ ๋ฐ์ํ | โ | **โ
** |
+
+**DB ๋ณ๊ฒฝ ์์ด, ๋ ๋๋ง ๋ก์ง๋ง ์์ **ํ์ฌ ์๋ฒฝํ PC ๋ฐ์ํ์ ๊ตฌํํ ์ ์์ต๋๋ค.
diff --git a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
index 42cd872b..411fdd1f 100644
--- a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
+++ b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md
@@ -103,6 +103,162 @@
- ๋ถํ ํจ๋ ๋ฐ์ํ ์ฒ๋ฆฌ
```
+### 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. ๊ธฐ์ ๊ฒฐ์
@@ -649,6 +805,10 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
- [ ] ํ๋ธ๋ฆฟ (768px, 1024px) ํ
์คํธ
- [ ] ๋ชจ๋ฐ์ผ (375px, 414px) ํ
์คํธ
- [ ] ๋ถํ ํจ๋ ํ๋ฉด ํ
์คํธ
+- [ ] GridLayout ์ปดํฌ๋ํธ ํฌํจ ํ๋ฉด ํ
์คํธ
+- [ ] FlexboxLayout ์ปดํฌ๋ํธ ํฌํจ ํ๋ฉด ํ
์คํธ
+- [ ] TabsLayout ์ปดํฌ๋ํธ ํฌํจ ํ๋ฉด ํ
์คํธ
+- [ ] ์ค์ฒฉ ๋ ์ด์์ (GridLayout ์์ ์ปดํฌ๋ํธ) ํ
์คํธ
---
@@ -659,6 +819,8 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
| ๋ง์ด๊ทธ๋ ์ด์
์คํจ | ๋์ | ๋ฐฑ์
ํ
์ด๋ธ์์ ์ฆ์ ๋กค๋ฐฑ |
| ๊ธฐ์กด ํ๋ฉด ๊นจ์ง | ์ค๊ฐ | `layoutMode` ์์ผ๋ฉด ๊ธฐ์กด ๋ฐฉ์ ์ฌ์ฉ (ํด๋ฐฑ) |
| ๋์์ธ ๋ชจ๋ ํผ๋ | ๋ฎ์ | position/size ํ๋ ์ ์ง |
+| GridLayout ๋ด๋ถ ๊นจ์ง | ๋ฎ์ | ๋ด๋ถ๋ ๊ธฐ์กด ๋ฐฉ์ ์ ์ง, ์ธ๋ถ ๋ฐฐ์น๋ง ๋ณ๊ฒฝ |
+| ์ค์ฒฉ ๋ ์ด์์ ๋ฌธ์ | ๋ฎ์ | ๊ฐ ๋ ์ด์์ ์ปดํฌ๋ํธ๋ ๋
๋ฆฝ์ ์ผ๋ก ๋์ |
---
diff --git a/docs/DDD1542/V2_๋ง์ด๊ทธ๋ ์ด์
_ํ์ต๋
ธํธ_DDD1542.md b/docs/DDD1542/V2_๋ง์ด๊ทธ๋ ์ด์
_ํ์ต๋
ธํธ_DDD1542.md
new file mode 100644
index 00000000..801fb213
--- /dev/null
+++ b/docs/DDD1542/V2_๋ง์ด๊ทธ๋ ์ด์
_ํ์ต๋
ธํธ_DDD1542.md
@@ -0,0 +1,399 @@
+# V2 ๋ง์ด๊ทธ๋ ์ด์
ํ์ต๋
ธํธ (DDD1542 ์ ์ฉ)
+
+> **๋ชฉ์ **: ๋ง์ด๊ทธ๋ ์ด์
์์
์ ์๋ฒฝํ ์ดํด๋ฅผ ์ํ ๊ฐ์ธ ํ์ต๋
ธํธ
+> **์์ฑ์ผ**: 2026-02-03
+> **์ ๋ ๊ท์น**: ๋ชจ๋ฅด๋ฉด ๋ฌผ์ด๋ณด๊ธฐ, ์ถ์ธก ๊ธ์ง
+
+---
+
+## 1. ๊ฐ์ฅ ์ค์ํ ํต์ฌ (์ด์ ์ ํ๊ฐ ์คํจํ ์ด์ )
+
+### 1.1 "component" vs "v2-input" ์ฐจ์ด
+
+```
+[์๋ชป๋ ์ํ] [์ฌ๋ฐ๋ฅธ ์ํ]
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
+โ component โ โ v2-input โ
+โ ์
์ฒด์ฝ๋ โ โ ์
์ฒด์ฝ๋ โ
+โ "์๋ ์์ฑ๋ฉ๋๋ค" โ โ "์๋ ์์ฑ๋ฉ๋๋ค" โ
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
+ โ โ
+ ํ
์ด๋ธ-์ปฌ๋ผ ์ฐ๊ฒฐ ์์ table_name + column_name ์ฐ๊ฒฐ๋จ
+```
+
+**ํต์ฌ**: ์ปฌ๋ผ์ ์ผ์ชฝ ํจ๋์์ **๋๋๊ทธ**ํด์ผ ์ฌ๋ฐ๋ฅธ ์ฐ๊ฒฐ์ด ์์ฑ๋จ
+
+### 1.2 ์ฌ๋ฐ๋ฅธ ์ปดํฌ๋ํธ ์์ฑ ๋ฐฉ๋ฒ
+
+```
+[์ผ์ชฝ ํจ๋: ํ
์ด๋ธ ์ปฌ๋ผ ๋ชฉ๋ก]
+์ด์ก์
์ฒด (8๊ฐ)
+โโโ ์
์ฒด์ฝ๋ [numbering] โ๋๋๊ทธโ ํ๋ฉด ์บ๋ฒ์ค โ v2-numbering-rule (๋๋ v2-input)
+โโโ ์
์ฒด๋ช
[text] โ๋๋๊ทธโ ํ๋ฉด ์บ๋ฒ์ค โ v2-input
+โโโ ์ ํ [category] โ๋๋๊ทธโ ํ๋ฉด ์บ๋ฒ์ค โ v2-select
+โโโ ์ฐ๋ฝ์ฒ [text] โ๋๋๊ทธโ ํ๋ฉด ์บ๋ฒ์ค โ v2-input
+โโโ ...
+```
+
+### 1.3 input_type โ V2 ์ปดํฌ๋ํธ ๋งคํ
+
+| table_type_columns.input_type | V2 ์ปดํฌ๋ํธ | ์ฐ๋ ํ
์ด๋ธ |
+|-------------------------------|-------------|-------------|
+| text | v2-input | - |
+| number | v2-input (type=number) | - |
+| date | v2-date | - |
+| category | v2-select | category_values |
+| numbering | v2-numbering-rule ๋๋ v2-input | numbering_rules |
+| entity | v2-entity-search | ์ํฐํฐ ์กฐ์ธ |
+
+---
+
+## 2. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด
+
+### 2.1 ํ
์ด๋ธ ๊ตฌ์กฐ
+
+```
+V1 (๋ณธ์๋ฒ: screen_layouts) V2 (๊ฐ๋ฐ์๋ฒ: screen_layouts_v2)
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+- ์ปดํฌ๋ํธ๋ณ 1๊ฐ ๋ ์ฝ๋ - ํ๋ฉด๋น 1๊ฐ ๋ ์ฝ๋
+- properties JSONB - layout_data JSONB
+- component_type VARCHAR - url (์ปดํฌ๋ํธ ๊ฒฝ๋ก)
+- menu_objid ๊ธฐ๋ฐ ์ฑ๋ฒ/์นดํ
๊ณ ๋ฆฌ - table_name + column_name ๊ธฐ๋ฐ
+```
+
+### 2.2 V2 layout_data ๊ตฌ์กฐ
+
+```json
+{
+ "version": "2.0",
+ "components": [
+ {
+ "id": "comp_xxx",
+ "url": "@/lib/registry/components/v2-table-list",
+ "position": { "x": 0, "y": 0 },
+ "size": { "width": 100, "height": 50 },
+ "displayOrder": 0,
+ "overrides": {
+ "tableName": "inspection_standard",
+ "columns": ["id", "name", "status"]
+ }
+ }
+ ],
+ "updatedAt": "2026-02-03T12:00:00Z"
+}
+```
+
+### 2.3 ์ปดํฌ๋ํธ URL ๋งคํ
+
+```typescript
+const V1_TO_V2_URL_MAPPING = {
+ 'table-list': '@/lib/registry/components/v2-table-list',
+ 'button-primary': '@/lib/registry/components/v2-button-primary',
+ 'text-input': '@/lib/registry/components/v2-input',
+ 'select-basic': '@/lib/registry/components/v2-select',
+ 'date-input': '@/lib/registry/components/v2-date',
+ 'entity-search-input': '@/lib/registry/components/v2-entity-search',
+ 'category-manager': '@/lib/registry/components/v2-category-manager',
+ 'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
+ 'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
+ 'split-panel-layout': '@/lib/registry/components/v2-split-panel-layout',
+};
+```
+
+---
+
+## 3. ๋ฐ์ดํฐ ํ์
๊ด๋ฆฌ (V2)
+
+### 3.1 ํต์ฌ ํ
์ด๋ธ ๊ด๊ณ
+
+```
+table_type_columns (์ปฌ๋ผ ํ์
์ ์)
+โโโ input_type = 'category' โ category_values (table_name + column_name)
+โโโ input_type = 'numbering' โ numbering_rules (detail_settings.numberingRuleId)
+โโโ input_type = 'entity' โ ์ํฐํฐ ์กฐ์ธ
+โโโ input_type = 'text', 'number', 'date', etc.
+```
+
+### 3.2 category_values ์กฐํ ์ฟผ๋ฆฌ
+
+```sql
+-- ํน์ ํ
์ด๋ธ.์ปฌ๋ผ์ ์นดํ
๊ณ ๋ฆฌ ๊ฐ ์กฐํ
+SELECT value_id, value_code, value_label, parent_value_id, depth
+FROM category_values
+WHERE table_name = 'ํ
์ด๋ธ๋ช
'
+ AND column_name = '์ปฌ๋ผ๋ช
'
+ AND company_code = 'COMPANY_7'
+ORDER BY value_order;
+```
+
+### 3.3 numbering_rules ์ฐ๊ฒฐ ๋ฐฉ์
+
+```json
+// table_type_columns.detail_settings
+{
+ "numberingRuleId": "rule-xxx"
+}
+
+// numbering_rules์์ ํด๋น rule ์กฐํ
+SELECT * FROM numbering_rules WHERE rule_id = 'rule-xxx';
+```
+
+---
+
+## 4. V2 ์ปดํฌ๋ํธ ๋ชฉ๋ก (23๊ฐ)
+
+### 4.1 ์
๋ ฅ ์ปดํฌ๋ํธ
+
+| ID | ์ด๋ฆ | ์ฉ๋ |
+|----|------|------|
+| v2-input | ์
๋ ฅ | ํ
์คํธ, ์ซ์, ๋น๋ฐ๋ฒํธ, ์ด๋ฉ์ผ |
+| v2-select | ์ ํ | ๋๋กญ๋ค์ด, ๋ผ๋์ค, ์ฒดํฌ๋ฐ์ค |
+| v2-date | ๋ ์ง | ๋ ์ง, ์๊ฐ, ๋ ์ง๋ฒ์ |
+
+### 4.2 ํ์ ์ปดํฌ๋ํธ
+
+| ID | ์ด๋ฆ | ์ฉ๋ |
+|----|------|------|
+| v2-text-display | ํ
์คํธ ํ์ | ๋ผ๋ฒจ, ์ ๋ชฉ |
+| v2-card-display | ์นด๋ ๋์คํ๋ ์ด | ์นด๋ ํํ ๋ฐ์ดํฐ |
+| v2-aggregation-widget | ์ง๊ณ ์์ ฏ | ํฉ๊ณ, ํ๊ท , ๊ฐ์ |
+
+### 4.3 ํ
์ด๋ธ/๋ฐ์ดํฐ ์ปดํฌ๋ํธ
+
+| ID | ์ด๋ฆ | ์ฉ๋ |
+|----|------|------|
+| v2-table-list | ํ
์ด๋ธ ๋ฆฌ์คํธ | ๋ฐ์ดํฐ ๊ทธ๋ฆฌ๋ |
+| v2-table-search-widget | ๊ฒ์ ํํฐ | ํ
์ด๋ธ ๊ฒ์ |
+| v2-pivot-grid | ํผ๋ฒ ๊ทธ๋ฆฌ๋ | ๋ค์ฐจ์ ๋ถ์ |
+| v2-table-grouped | ๊ทธ๋ฃนํ ํ
์ด๋ธ | ๊ทธ๋ฃน๋ณ ์ ๊ธฐ/ํผ์น๊ธฐ |
+
+### 4.4 ๋ ์ด์์ ์ปดํฌ๋ํธ
+
+| ID | ์ด๋ฆ | ์ฉ๋ |
+|----|------|------|
+| v2-split-panel-layout | ๋ถํ ํจ๋ | ๋ง์คํฐ-๋ํ
์ผ |
+| v2-tabs-widget | ํญ ์์ ฏ | ํญ ์ ํ |
+| v2-section-card | ์น์
์นด๋ | ์ ๋ชฉ+ํ
๋๋ฆฌ ๊ทธ๋ฃน |
+| v2-section-paper | ์น์
ํ์ดํผ | ๋ฐฐ๊ฒฝ์ ๊ทธ๋ฃน |
+| v2-divider-line | ๊ตฌ๋ถ์ | ์์ญ ๊ตฌ๋ถ |
+| v2-repeat-container | ๋ฆฌํผํฐ ์ปจํ
์ด๋ | ๋ฐ์ดํฐ ๋ฐ๋ณต |
+| v2-unified-repeater | ํตํฉ ๋ฆฌํผํฐ | ์ธ๋ผ์ธ/๋ชจ๋ฌ/๋ฒํผ |
+
+### 4.5 ์ก์
/ํน์ ์ปดํฌ๋ํธ
+
+| ID | ์ด๋ฆ | ์ฉ๋ |
+|----|------|------|
+| v2-button-primary | ๊ธฐ๋ณธ ๋ฒํผ | ์ ์ฅ, ์ญ์ ๋ฑ |
+| v2-numbering-rule | ์ฑ๋ฒ ๊ท์น | ์๋ ์ฝ๋ ์์ฑ |
+| v2-category-manager | ์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ์ | ์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ |
+| v2-location-swap-selector | ์์น ๊ตํ | ์์น ์ ํ |
+| v2-rack-structure | ๋ ๊ตฌ์กฐ | ์ฐฝ๊ณ ๋ ์๊ฐํ |
+
+---
+
+## 5. ํ๋ฉด ํจํด (5๊ฐ์ง)
+
+### 5.1 ํจํด A: ๊ธฐ๋ณธ ๋ง์คํฐ ํ๋ฉด
+
+```
+์ฌ์ฉ ์กฐ๊ฑด: ๋จ์ผ ํ
์ด๋ธ CRUD
+
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ v2-table-search-widget โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ v2-table-list โ
+โ [์ ๊ท] [์ญ์ ] v2-button-primary โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+### 5.2 ํจํด B: ๋ง์คํฐ-๋ํ
์ผ ํ๋ฉด
+
+```
+์ฌ์ฉ ์กฐ๊ฑด: ๋ง์คํฐ ์ ํ โ ๋ํ
์ผ ํ์
+
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๋ง์คํฐ ๋ฆฌ์คํธ โ ๋ํ
์ผ ๋ฆฌ์คํธ โ
+โ v2-table-list โ v2-table-list โ
+โ โ (relation: foreignKey) โ
+โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ v2-split-panel-layout
+```
+
+**ํ์ ์ค์ :**
+```json
+{
+ "leftPanel": { "tableName": "master_table" },
+ "rightPanel": {
+ "tableName": "detail_table",
+ "relation": { "type": "detail", "foreignKey": "master_id" }
+ },
+ "splitRatio": 30
+}
+```
+
+### 5.3 ํจํด C: ๋ง์คํฐ-๋ํ
์ผ + ํญ
+
+```
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๋ง์คํฐ ๋ฆฌ์คํธ โ v2-tabs-widget โ
+โ v2-table-list โ โโ ํญ1: v2-table-list โ
+โ โ โโ ํญ2: v2-table-list โ
+โ โ โโ ํญ3: ํผ ์ปดํฌ๋ํธ๋ค โ
+โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ v2-split-panel-layout
+```
+
+---
+
+## 6. ๋ชจ๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์ ๋ณ๊ฒฝ
+
+### 6.1 V1 (๋ณธ์๋ฒ)
+
+```
+ํ๋ฉด A (screen_id: 142) - ๊ฒ์ฌ์ฅ๋น๊ด๋ฆฌ
+ โโโ ๋ฒํผ ํด๋ฆญ โ ํ๋ฉด B (screen_id: 143) - ๊ฒ์ฌ์ฅ๋น ๋ฑ๋ก๋ชจ๋ฌ (๋ณ๋ screen_id)
+```
+
+### 6.2 V2 (๊ฐ๋ฐ์๋ฒ)
+
+```
+ํ๋ฉด A (screen_id: 142) - ๊ฒ์ฌ์ฅ๋น๊ด๋ฆฌ
+ โโโ layout_data.components[] ๋ด์ v2-dialog-form ๋๋ overlay ํฌํจ
+```
+
+**ํต์ฌ**: V2์์๋ ๋ชจ๋ฌ์ ๋ณ๋ ํ๋ฉด์ด ์๋, ๋ถ๋ชจ ํ๋ฉด์ ์ปดํฌ๋ํธ๋ก ํตํฉ
+
+---
+
+## 7. ๋ง์ด๊ทธ๋ ์ด์
์ ์ฐจ (Step by Step)
+
+### Step 1: ์ฌ์ ๋ถ์
+
+```sql
+-- ๋ณธ์๋ฒ ํ๋ฉด ๋ชฉ๋ก ํ์ธ
+SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
+ COUNT(sl.layout_id) as component_count
+FROM screen_definitions sd
+LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+WHERE sd.screen_code LIKE 'COMPANY_7_%'
+GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;
+
+-- ๊ฐ๋ฐ์๋ฒ V2 ํํฉ ํ์ธ
+SELECT sd.screen_id, sd.screen_code, sd.screen_name,
+ sv2.layout_data IS NOT NULL as has_v2_layout
+FROM screen_definitions sd
+LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
+WHERE sd.company_code = 'COMPANY_7';
+```
+
+### Step 2: table_type_columns ํ์ธ
+
+```sql
+-- ํด๋น ํ
์ด๋ธ์ ์ปฌ๋ผ ํ์
ํ์ธ
+SELECT column_name, column_label, input_type, detail_settings
+FROM table_type_columns
+WHERE table_name = '๋์ํ
์ด๋ธ๋ช
'
+ AND company_code = 'COMPANY_7';
+```
+
+### Step 3: V2 layout_data ์์ฑ
+
+```json
+{
+ "version": "2.0",
+ "components": [
+ {
+ "id": "์์ฑ๋ID",
+ "url": "@/lib/registry/components/v2-์ปดํฌ๋ํธํ์
",
+ "position": { "x": 0, "y": 0 },
+ "size": { "width": 100, "height": 50 },
+ "displayOrder": 0,
+ "overrides": {
+ "tableName": "ํ
์ด๋ธ๋ช
",
+ "fieldName": "์ปฌ๋ผ๋ช
"
+ }
+ }
+ ],
+ "migratedFrom": "V1",
+ "migratedAt": "2026-02-03T00:00:00Z"
+}
+```
+
+### Step 4: screen_layouts_v2 INSERT
+
+```sql
+INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data)
+VALUES ($1, $2, $3::jsonb)
+ON CONFLICT (screen_id, company_code)
+DO UPDATE SET layout_data = $3::jsonb, updated_at = NOW();
+```
+
+### Step 5: ๊ฒ์ฆ
+
+- [ ] ํ๋ฉด ๋ ๋๋ง ํ์ธ (component๊ฐ ์๋ v2-xxx๋ก ํ์๋๋์ง)
+- [ ] ์ปดํฌ๋ํธ๋ณ ํ
์ด๋ธ-์ปฌ๋ผ ์ฐ๊ฒฐ ํ์ธ
+- [ ] ์นดํ
๊ณ ๋ฆฌ ๋๋กญ๋ค์ด ๋์ ํ์ธ
+- [ ] ์ฑ๋ฒ ๊ท์น ๋์ ํ์ธ
+- [ ] ์ ์ฅ/์์ /์ญ์ ํ
์คํธ
+
+---
+
+## 8. ํ์ง๊ด๋ฆฌ ๋ฉ๋ด ๋ง์ด๊ทธ๋ ์ด์
ํํฉ
+
+| ๋ณธ์๋ฒ ์ฝ๋ | ํ๋ฉด๋ช
| ํ
์ด๋ธ | ์ํ | ๋น๊ณ |
+|-------------|--------|--------|------|------|
+| COMPANY_7_126 | ๊ฒ์ฌ์ ๋ณด ๊ด๋ฆฌ | inspection_standard | โ
V2 ์กด์ฌ | ์ปดํฌ๋ํธ ๊ฒ์ฆ ํ์ |
+| COMPANY_7_127 | ํ๋ชฉ์ต์
์ค์ | - | โ
V2 ์กด์ฌ | v2-category-manager |
+| COMPANY_7_138 | ์นดํ
๊ณ ๋ฆฌ ์ค์ | inspection_standard | โ ๋๋ฝ | table_name ๊ธฐ๋ฐ |
+| COMPANY_7_139 | ์ฝ๋ ์ค์ | inspection_standard | โ ๋๋ฝ | table_name ๊ธฐ๋ฐ |
+| COMPANY_7_142 | ๊ฒ์ฌ์ฅ๋น ๊ด๋ฆฌ | inspection_equipment_mng | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ |
+| COMPANY_7_143 | ๊ฒ์ฌ์ฅ๋น ๋ฑ๋ก๋ชจ๋ฌ | inspection_equipment_mng | โ ๋๋ฝ | โ 142 ํตํฉ |
+| COMPANY_7_144 | ๋ถ๋๊ธฐ์ค ์ ๋ณด | defect_standard_mng | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ |
+| COMPANY_7_145 | ๋ถ๋๊ธฐ์ค ๋ฑ๋ก๋ชจ๋ฌ | defect_standard_mng | โ ๋๋ฝ | โ 144 ํตํฉ |
+
+---
+
+## 9. ๊ด๋ จ ์ฝ๋ ํ์ผ ๊ฒฝ๋ก
+
+| ํญ๋ชฉ | ๊ฒฝ๋ก |
+|------|------|
+| V2 ์ปดํฌ๋ํธ ํด๋ | `frontend/lib/registry/components/v2-xxx/` |
+| ์ปดํฌ๋ํธ ๋ฑ๋ก | `frontend/lib/registry/components/index.ts` |
+| ์นดํ
๊ณ ๋ฆฌ ์๋น์ค | `backend-node/src/services/categoryTreeService.ts` |
+| ์ฑ๋ฒ ์๋น์ค | `backend-node/src/services/numberingRuleService.ts` |
+| ์ํฐํฐ ์กฐ์ธ API | `frontend/lib/api/entityJoin.ts` |
+| ํผ ํธํ์ฑ ํ
| `frontend/hooks/useFormCompatibility.ts` |
+
+---
+
+## 10. ์ ๋ ํ์ง ๋ง ๊ฒ
+
+1. โ **ํ
์ด๋ธ-์ปฌ๋ผ ์ฐ๊ฒฐ ์์ด ์ปดํฌ๋ํธ ๋ฐฐ์น** โ "component"๋ก ํ์๋จ
+2. โ **menu_objid ๊ธฐ๋ฐ ์นดํ
๊ณ ๋ฆฌ/์ฑ๋ฒ ์ฌ์ฉ** โ V2๋ table_name + column_name ๊ธฐ๋ฐ
+3. โ **๋ชจ๋ฌ์ ๋ณ๋ screen_id๋ก ์์ฑ** โ V2๋ ๋ถ๋ชจ ํ๋ฉด์ ํตํฉ
+4. โ **V1 ์ปดํฌ๋ํธ ํ์
์ฌ์ฉ** โ ๋ฐ๋์ v2- ์ ๋์ฌ ์ปดํฌ๋ํธ ์ฌ์ฉ
+5. โ **company_code ํํฐ๋ง ๋๋ฝ** โ ๋ฉํฐํ
๋์ ํ์
+
+---
+
+## 11. ๋ชจ๋ฅด๋ฉด ํ์ธํ ๊ณณ
+
+1. **์ปดํฌ๋ํธ ๊ตฌ์กฐ**: `docs/V2_์ปดํฌ๋ํธ_๋ถ์_๊ฐ์ด๋.md`
+2. **ํ๋ฉด ๊ฐ๋ฐ ํ์ค**: `docs/screen-implementation-guide/ํ๋ฉด๊ฐ๋ฐ_ํ์ค_๊ฐ์ด๋.md`
+3. **๋ง์ด๊ทธ๋ ์ด์
์ ์ฐจ**: `docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md`
+4. **ํ์ค ๋์์ธ ๋ช
์ธ**: `/Users/gbpark/Downloads/ํ๋ฉด๊ฐ๋ฐ 8/`
+5. **์ค์ ์ฝ๋**: ์ ๊ฒฝ๋ก์ ์์ค ํ์ผ๋ค
+
+---
+
+## 12. ์์ ํ๊ณ
+
+> **"ํญ์ ์ ๋งคํ ๊ฑฐ๋ mdํ์ผ ๋ณด๊ฑฐ๋ ๋ฌผ์ด๋ณผ ๊ฒ. ์ฝ๋์๋ ์ ๋ถ ์ ๋ต์ด ์์. ๋ง์ฝ ๋ชจ๋ฅธ๋ค๋ฉด ๋ ์๋ชป. ์ค์ํด๋ ๋ ์๋ชป."**
+
+---
+
+## ๋ณ๊ฒฝ ์ด๋ ฅ
+
+| ๋ ์ง | ์์ฑ์ | ๋ด์ฉ |
+|------|--------|------|
+| 2026-02-03 | DDD1542 | ์ด์ ์์ฑ (๋ฌธ์ 4๊ฐ ์ ๋
ํ) |
diff --git a/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_๊ฐ์ด๋.md b/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_๊ฐ์ด๋.md
new file mode 100644
index 00000000..b7a0e353
--- /dev/null
+++ b/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_๊ฐ์ด๋.md
@@ -0,0 +1,453 @@
+# ๋ณธ์๋ฒ โ ๊ฐ๋ฐ์๋ฒ ๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋ (๊ณต์ฉ)
+
+> **์ด ๋ฌธ์๋ ๋ค์ AI ์์ด์ ํธ๊ฐ ๋ง์ด๊ทธ๋ ์ด์
์์
์ ์ด์ด๋ฐ์ ๋ ์ฐธ๊ณ ํ๋ ํต์ฌ ๊ฐ์ด๋์
๋๋ค.**
+
+---
+
+## ๋น ๋ฅธ ์์
+
+### ๋ง์ด๊ทธ๋ ์ด์
๋ฐฉํฅ (์ ๋ ์์ง ๋ง ๊ฒ)
+
+```
+๋ณธ์๋ฒ (Production) โ ๊ฐ๋ฐ์๋ฒ (Development)
+211.115.91.141:11134 39.117.244.52:11132
+screen_layouts (V1) screen_layouts_v2 (V2)
+```
+
+**๋ฐ๋๋ก ํ๋ฉด ์ ๋จ!** ๊ฐ๋ฐ์๋ฒ ์์ฑ ํ โ ๋ณธ์๋ฒ๋ก ๋ฐฐํฌ ์์
+
+### DB ์ ์ ์ ๋ณด
+
+```bash
+# ๋ณธ์๋ฒ (Production)
+docker exec pms-backend-mac node -e '
+const { Pool } = require("pg");
+const pool = new Pool({
+ connectionString: "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/plm?sslmode=disable",
+ ssl: false
+});
+// ์ฟผ๋ฆฌ ์คํ
+'
+
+# ๊ฐ๋ฐ์๋ฒ (Development)
+docker exec pms-backend-mac node -e '
+const { Pool } = require("pg");
+const pool = new Pool({
+ connectionString: "postgresql://postgres:ph0909!!@39.117.244.52:11132/plm?sslmode=disable",
+ ssl: false
+});
+// ์ฟผ๋ฆฌ ์คํ
+'
+```
+
+---
+
+## ์ ๋ ์ฃผ์: ์ปดํฌ๋ํธ-์ปฌ๋ผ ์ฐ๊ฒฐ (์ด์ ์คํจ ์์ธ)
+
+### "component" vs "v2-input" ๊ตฌ๋ถ
+
+```
+โ ์๋ชป๋ ์ํ โ
์ฌ๋ฐ๋ฅธ ์ํ
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
+โ component โ โ v2-input โ
+โ ์
์ฒด์ฝ๋ โ โ ์
์ฒด์ฝ๋ โ
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
+ โ โ
+ overrides.type ์์ overrides.type = "v2-input"
+```
+
+**ํต์ฌ ์์ธ**: ์ปดํฌ๋ํธ๋ฅผ ๊ทธ๋ฅ ๋ฐฐ์นํ๋ฉด "component"๋ก ํ์๋จ. ๋ฐ๋์ ์ผ์ชฝ ํจ๋์์ ํ
์ด๋ธ ์ปฌ๋ผ์ **๋๋๊ทธ**ํด์ผ ์ฌ๋ฐ๋ฅธ v2-xxx ์ปดํฌ๋ํธ๊ฐ ์์ฑ๋จ.
+
+### ๐ฅ ํต์ฌ ๋ฐ๊ฒฌ: overrides.type ํ์ (2026-02-04 ๋ฐ๊ฒฌ)
+
+**"component"๋ก ํ์๋๋ ๊ทผ๋ณธ ์์ธ:**
+
+| ํญ๋ชฉ | ๋๋๊ทธ๋ก ๋ฐฐ์น | ๋ง์ด๊ทธ๋ ์ด์
(์๋ชป๋) |
+|------|---------------|----------------------|
+| `overrides.type` | **"v2-input"** โ
| **์์** โ |
+| `overrides.webType` | "text" ๋ฑ | ์์ |
+| `overrides.tableName` | "carrier_mng" ๋ฑ | ์์ |
+
+**ํ๋ก ํธ์๋๊ฐ ์ปดํฌ๋ํธ ํ์
์ ์ธ์ํ๋ ๋ฐฉ๋ฒ:**
+1. `overrides.type` ํ์ธ โ ์์ผ๋ฉด ํด๋น ๊ฐ ์ฌ์ฉ (์: "v2-input")
+2. ์์ผ๋ฉด โ ๊ธฐ๋ณธ๊ฐ "component"๋ก ํด๋ฐฑ
+
+**๊ฒฐ๋ก **: ๋ง์ด๊ทธ๋ ์ด์
์ `overrides.type` ํ๋๋ฅผ ๋ฐ๋์ ์ค์ ํด์ผ ํจ!
+
+### input_type โ V2 ์ปดํฌ๋ํธ ์๋ ๋งคํ
+
+| table_type_columns.input_type | ๋๋๊ทธ ์ ์์ฑ๋๋ V2 ์ปดํฌ๋ํธ |
+|-------------------------------|-------------------------------|
+| text | v2-input |
+| number | v2-input (type=number) |
+| date | v2-date |
+| category | v2-select (category_values ์ฐ๋) |
+| numbering | v2-numbering-rule ๋๋ v2-input |
+| entity | v2-entity-search |
+
+**์ ๋ ๊ท์น**: ์ปดํฌ๋ํธ๊ฐ "component"๋ก ํ์๋๋ฉด ์ฐ๊ฒฐ ์คํจ ์ํ. ๋ฐ๋์ "v2-xxx"๋ก ํ์๋์ด์ผ ํจ.
+
+---
+
+## ํต์ฌ ๊ฐ๋
+
+### V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด
+
+| ๊ตฌ๋ถ | V1 (๋ณธ์๋ฒ) | V2 (๊ฐ๋ฐ์๋ฒ) |
+|------|-------------|---------------|
+| ํ
์ด๋ธ | screen_layouts | screen_layouts_v2 |
+| ๋ ์ฝ๋ | ์ปดํฌ๋ํธ๋ณ 1๊ฐ | ํ๋ฉด๋น 1๊ฐ |
+| ์ค์ ์ ์ฅ | properties JSONB | layout_data.components[].overrides |
+| ์ฑ๋ฒ/์นดํ
๊ณ ๋ฆฌ | menu_objid ๊ธฐ๋ฐ | table_name + column_name ๊ธฐ๋ฐ |
+| ์ปดํฌ๋ํธ ์ฐธ์กฐ | component_type ๋ฌธ์์ด | url ๊ฒฝ๋ก (@/lib/registry/...) |
+
+### ๋ฐ์ดํฐ ํ์
๊ด๋ฆฌ (V2)
+
+```
+table_type_columns (input_type)
+โโโ 'category' โ category_values ํ
์ด๋ธ
+โโโ 'numbering' โ numbering_rules ํ
์ด๋ธ (detail_settings.numberingRuleId)
+โโโ 'entity' โ ์ํฐํฐ ๊ฒ์
+โโโ 'text', 'number', 'date', etc.
+```
+
+### ์ปดํฌ๋ํธ URL ๋งคํ
+
+```typescript
+const V1_TO_V2_MAPPING = {
+ 'table-list': '@/lib/registry/components/v2-table-list',
+ 'button-primary': '@/lib/registry/components/v2-button-primary',
+ 'text-input': '@/lib/registry/components/v2-text-input',
+ 'select-basic': '@/lib/registry/components/v2-select',
+ 'date-input': '@/lib/registry/components/v2-date-input',
+ 'entity-search-input': '@/lib/registry/components/v2-entity-search',
+ 'category-manager': '@/lib/registry/components/v2-category-manager',
+ 'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
+ 'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
+ 'textarea-basic': '@/lib/registry/components/v2-textarea',
+};
+```
+
+### ๋ชจ๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์ ๋ณ๊ฒฝ
+
+- **V1**: ๋ณ๋ ํ๋ฉด(screen_id)์ผ๋ก ๋ชจ๋ฌ ๊ด๋ฆฌ
+- **V2**: ๋ถ๋ชจ ํ๋ฉด์ overlay/dialog ์ปดํฌ๋ํธ๋ก ํตํฉ
+
+---
+
+## ๋ง์ด๊ทธ๋ ์ด์
๋์ ๋ฉ๋ด ํํฉ
+
+### ํ์ง๊ด๋ฆฌ (์ฐ์ ์์ 1)
+
+| ๋ณธ์๋ฒ ์ฝ๋ | ํ๋ฉด๋ช
| ์ํ | ๋น๊ณ |
+|-------------|--------|------|------|
+| COMPANY_7_126 | ๊ฒ์ฌ์ ๋ณด ๊ด๋ฆฌ | โ
V2 ์กด์ฌ | ์ปดํฌ๋ํธ ๊ฒ์ฆ ํ์ |
+| COMPANY_7_127 | ํ๋ชฉ์ต์
์ค์ | โ
V2 ์กด์ฌ | v2-category-manager ์ฌ์ฉ์ค |
+| COMPANY_7_138 | ์นดํ
๊ณ ๋ฆฌ ์ค์ | โ ๋๋ฝ | table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| COMPANY_7_139 | ์ฝ๋ ์ค์ | โ ๋๋ฝ | table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| COMPANY_7_142 | ๊ฒ์ฌ์ฅ๋น ๊ด๋ฆฌ | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ ํ์ |
+| COMPANY_7_143 | ๊ฒ์ฌ์ฅ๋น ๋ฑ๋ก๋ชจ๋ฌ | โ ๋๋ฝ | โ 142์ ํตํฉ |
+| COMPANY_7_144 | ๋ถ๋๊ธฐ์ค ์ ๋ณด | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ ํ์ |
+| COMPANY_7_145 | ๋ถ๋๊ธฐ์ค ๋ฑ๋ก๋ชจ๋ฌ | โ ๋๋ฝ | โ 144์ ํตํฉ |
+
+### ๋ค์ ๋ง์ด๊ทธ๋ ์ด์
๋์ (๋ฏธ์ )
+
+- [ ] ๋ฌผ๋ฅ๊ด๋ฆฌ
+- [ ] ์์ฐ๊ด๋ฆฌ
+- [ ] ์์
๊ด๋ฆฌ
+- [ ] ๊ธฐํ ๋ฉ๋ด๋ค
+
+---
+
+## ๋ง์ด๊ทธ๋ ์ด์
์์
์ ์ฐจ
+
+### Step 1: ๋ถ์
+
+```sql
+-- ๋ณธ์๋ฒ ํน์ ๋ฉ๋ด ํ๋ฉด ๋ชฉ๋ก ์กฐํ
+SELECT
+ sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
+ COUNT(sl.layout_id) as component_count
+FROM screen_definitions sd
+LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+WHERE sd.screen_name LIKE '%[๋ฉ๋ด๋ช
]%'
+ AND sd.company_code = 'COMPANY_7'
+GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;
+
+-- ๊ฐ๋ฐ์๋ฒ V2 ํํฉ ํ์ธ
+SELECT
+ sd.screen_id, sd.screen_code, sd.screen_name,
+ sv2.layout_id IS NOT NULL as has_v2
+FROM screen_definitions sd
+LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
+WHERE sd.company_code = 'COMPANY_7';
+```
+
+### Step 2: screen_definitions ๋๊ธฐํ
+
+๋ณธ์๋ฒ์๋ง ์๋ ํ๋ฉด์ ๊ฐ๋ฐ์๋ฒ์ ์ถ๊ฐ
+
+### Step 3: V1 โ V2 ๋ ์ด์์ ๋ณํ
+
+```typescript
+// layout_data ๊ตฌ์กฐ
+{
+ "version": "2.0",
+ "components": [
+ {
+ "id": "comp_xxx",
+ "url": "@/lib/registry/components/v2-table-list",
+ "position": { "x": 0, "y": 0 },
+ "size": { "width": 100, "height": 50 },
+ "displayOrder": 0,
+ "overrides": {
+ "tableName": "ํ
์ด๋ธ๋ช
",
+ "columns": ["์ปฌ๋ผ1", "์ปฌ๋ผ2"]
+ }
+ }
+ ]
+}
+```
+
+### Step 4: ์นดํ
๊ณ ๋ฆฌ ๋ฐ์ดํฐ ํ์ธ/์์ฑ
+
+```sql
+-- ํ
์ด๋ธ์ category ์ปฌ๋ผ ํ์ธ
+SELECT column_name, column_label
+FROM table_type_columns
+WHERE table_name = '[ํ
์ด๋ธ๋ช
]'
+ AND input_type = 'category';
+
+-- category_values ๋ฐ์ดํฐ ํ์ธ
+SELECT value_id, value_code, value_label
+FROM category_values
+WHERE table_name = '[ํ
์ด๋ธ๋ช
]'
+ AND column_name = '[์ปฌ๋ผ๋ช
]'
+ AND company_code = 'COMPANY_7';
+```
+
+### Step 5: ์ฑ๋ฒ ๊ท์น ํ์ธ/์์ฑ
+
+```sql
+-- numbering ์ปฌ๋ผ ํ์ธ
+SELECT column_name, column_label, detail_settings
+FROM table_type_columns
+WHERE table_name = '[ํ
์ด๋ธ๋ช
]'
+ AND input_type = 'numbering';
+
+-- numbering_rules ๋ฐ์ดํฐ ํ์ธ
+SELECT rule_id, rule_name, table_name, column_name
+FROM numbering_rules
+WHERE company_code = 'COMPANY_7';
+```
+
+### Step 6: ๊ฒ์ฆ
+
+- [ ] ํ๋ฉด ๋ ๋๋ง ํ์ธ
+- [ ] ์ปดํฌ๋ํธ ๋์ ํ์ธ
+- [ ] ์ ์ฅ/์์ /์ญ์ ํ
์คํธ
+- [ ] ์นดํ
๊ณ ๋ฆฌ ๋๋กญ๋ค์ด ๋์
+- [ ] ์ฑ๋ฒ ๊ท์น ๋์
+
+---
+
+## ํต์ฌ ํ
์ด๋ธ ์คํค๋ง
+
+### screen_layouts_v2
+
+```sql
+CREATE TABLE screen_layouts_v2 (
+ layout_id SERIAL PRIMARY KEY,
+ screen_id INTEGER NOT NULL,
+ company_code VARCHAR(20) NOT NULL,
+ layout_data JSONB NOT NULL DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ UNIQUE(screen_id, company_code)
+);
+```
+
+### category_values
+
+```sql
+-- ํต์ฌ ์ปฌ๋ผ
+value_id, table_name, column_name, value_code, value_label,
+parent_value_id, depth, path, company_code
+```
+
+### numbering_rules + numbering_rule_parts
+
+```sql
+-- numbering_rules ํต์ฌ ์ปฌ๋ผ
+rule_id, rule_name, table_name, column_name, separator,
+reset_period, current_sequence, company_code
+
+-- numbering_rule_parts ํต์ฌ ์ปฌ๋ผ
+rule_id, part_order, part_type, generation_method,
+auto_config, manual_config, company_code
+```
+
+### table_type_columns
+
+```sql
+-- ํต์ฌ ์ปฌ๋ผ
+table_name, column_name, input_type, column_label,
+detail_settings, company_code
+```
+
+---
+
+## ์ฐธ๊ณ ๋ฌธ์
+
+### ํ์ ์ฝ๊ธฐ
+
+1. **[๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md](./๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md)** - ์์ธ ๋ง์ด๊ทธ๋ ์ด์
์ ์ฐจ
+2. **[ํ๋ฉด๊ฐ๋ฐ_ํ์ค_๊ฐ์ด๋.md](../screen-implementation-guide/ํ๋ฉด๊ฐ๋ฐ_ํ์ค_๊ฐ์ด๋.md)** - V2 ํ๋ฉด ๊ฐ๋ฐ ํ์ค
+3. **[SCREEN_DEVELOPMENT_STANDARD.md](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)** - ์๋ฌธ ํ์ค ๊ฐ์ด๋
+
+### ์ฝ๋ ์ฐธ์กฐ
+
+| ํ์ผ | ์ค๋ช
|
+|------|------|
+| `backend-node/src/services/categoryTreeService.ts` | ์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ ์๋น์ค |
+| `backend-node/src/services/numberingRuleService.ts` | ์ฑ๋ฒ ๊ท์น ์๋น์ค |
+| `frontend/lib/registry/components/v2-category-manager/` | V2 ์นดํ
๊ณ ๋ฆฌ ์ปดํฌ๋ํธ |
+| `frontend/lib/registry/components/v2-numbering-rule/` | V2 ์ฑ๋ฒ ์ปดํฌ๋ํธ |
+
+### ๊ด๋ จ ๋ฌธ์
+
+- `docs/V2_์ปดํฌ๋ํธ_๋ถ์_๊ฐ์ด๋.md`
+- `docs/V2_์ปดํฌ๋ํธ_์ฐ๋_๊ฐ์ด๋.md`
+- `docs/DDD1542/COMPONENT_LAYOUT_V2_ARCHITECTURE.md`
+- `docs/DDD1542/COMPONENT_MIGRATION_PLAN.md`
+
+---
+
+## ์ฃผ์์ฌํญ
+
+### ์ ๋ ํ์ง ๋ง ๊ฒ
+
+1. **๊ฐ๋ฐ์๋ฒ โ ๋ณธ์๋ฒ ๋ง์ด๊ทธ๋ ์ด์
** (๋ฐ๋ ๋ฐฉํฅ)
+2. **๋ณธ์๋ฒ ๋ฐ์ดํฐ ์ง์ ์์ ** (SELECT๋ง ํ์ฉ)
+3. **company_code ๋๋ฝ** (๋ฉํฐํ
๋์ ํ์)
+4. **ํ
์ด๋ธ-์ปฌ๋ผ ์ฐ๊ฒฐ ์์ด ์ปดํฌ๋ํธ ๋ฐฐ์น** ("component"๋ก ํ์๋๋ฉด ์คํจ)
+5. **menu_objid ๊ธฐ๋ฐ ์นดํ
๊ณ ๋ฆฌ/์ฑ๋ฒ ์ฌ์ฉ** (V2๋ table_name + column_name ๊ธฐ๋ฐ)
+
+### ๋ฐ๋์ ํ ๊ฒ
+
+1. ๋ง์ด๊ทธ๋ ์ด์
์ **๊ฐ๋ฐ์๋ฒ ๋ฐฑ์
**
+2. ์ปดํฌ๋ํธ ๋ณํ ์ **V2 ์ปดํฌ๋ํธ๋ง ์ฌ์ฉ** (v2- prefix)
+3. ๋ชจ๋ฌ ํ๋ฉด์ **๋ถ๋ชจ ํ๋ฉด์ ํตํฉ**
+4. ์นดํ
๊ณ ๋ฆฌ/์ฑ๋ฒ์ **table_name + column_name ๊ธฐ๋ฐ**
+5. ์ปดํฌ๋ํธ ๋ฐฐ์น ํ **"v2-xxx"๋ก ํ์๋๋์ง ๋ฐ๋์ ํ์ธ**
+
+### ์คํจ ์ฌ๋ก (์ด์ ์์
์)
+
+**๋ฌผ๋ฅ์ ๋ณด๊ด๋ฆฌ โ ์ด์ก์
์ฒด ๊ด๋ฆฌ ๋ง์ด๊ทธ๋ ์ด์
์คํจ**
+
+- **์์ธ**: ์ปดํฌ๋ํธ๋ฅผ ์ง์ ๋ฐฐ์นํ์ฌ "component"๋ก ์์ฑ๋จ
+- **์ฆ์**: ํ๋ฉด์ "component" ๋ผ๋ฒจ ํ์, ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ์คํจ
+- **ํด๊ฒฐ**: ์ผ์ชฝ ํจ๋์์ ํ
์ด๋ธ ์ปฌ๋ผ์ ๋๋๊ทธํ์ฌ "v2-input" ๋ฑ์ผ๋ก ์์ฑ
+
+---
+
+## ๐ง ์ผ๊ด ์์ SQL (overrides.type ๋๋ฝ ๋ฌธ์ )
+
+### ๋ฌธ์ ์ง๋จ ์ฟผ๋ฆฌ
+
+```sql
+-- overrides.type์ด ์๋ ์ปดํฌ๋ํธ ์ ํ์ธ
+SELECT
+ COUNT(DISTINCT sv2.screen_id) as affected_screens,
+ COUNT(*) as affected_components
+FROM screen_layouts_v2 sv2,
+ jsonb_array_elements(sv2.layout_data->'components') as comp
+WHERE (comp->>'url' LIKE '%/v2-input'
+ OR comp->>'url' LIKE '%/v2-select'
+ OR comp->>'url' LIKE '%/v2-date')
+ AND NOT (comp->'overrides' ? 'type');
+```
+
+### ์ผ๊ด ์์ ์ฟผ๋ฆฌ (๊ฐ๋ฐ์๋ฒ์์๋ง!)
+
+```sql
+UPDATE screen_layouts_v2
+SET layout_data = jsonb_set(
+ layout_data,
+ '{components}',
+ (
+ SELECT jsonb_agg(
+ CASE
+ WHEN comp->>'url' LIKE '%/v2-input' AND NOT (comp->'overrides' ? 'type')
+ THEN jsonb_set(comp, '{overrides,type}', '"v2-input"')
+ WHEN comp->>'url' LIKE '%/v2-select' AND NOT (comp->'overrides' ? 'type')
+ THEN jsonb_set(comp, '{overrides,type}', '"v2-select"')
+ WHEN comp->>'url' LIKE '%/v2-date' AND NOT (comp->'overrides' ? 'type')
+ THEN jsonb_set(comp, '{overrides,type}', '"v2-date"')
+ WHEN comp->>'url' LIKE '%/v2-textarea' AND NOT (comp->'overrides' ? 'type')
+ THEN jsonb_set(comp, '{overrides,type}', '"v2-textarea"')
+ ELSE comp
+ END
+ )
+ FROM jsonb_array_elements(layout_data->'components') comp
+ )
+),
+updated_at = NOW()
+WHERE EXISTS (
+ SELECT 1 FROM jsonb_array_elements(layout_data->'components') c
+ WHERE (c->>'url' LIKE '%/v2-input' OR c->>'url' LIKE '%/v2-select'
+ OR c->>'url' LIKE '%/v2-date' OR c->>'url' LIKE '%/v2-textarea')
+ AND NOT (c->'overrides' ? 'type')
+);
+```
+
+### 2026-02-04 ์ผ๊ด ์์ ์คํ ๊ฒฐ๊ณผ
+
+| ํญ๋ชฉ | ์๋ |
+|------|------|
+| ์์ ๋ ํ๋ฉด | 397๊ฐ |
+| ์์ ๋ ์ปดํฌ๋ํธ | 2,455๊ฐ |
+| v2-input | 1,983๊ฐ |
+| v2-select | 336๊ฐ |
+| v2-date | 136๊ฐ |
+
+---
+
+## ๋ง์ด๊ทธ๋ ์ด์
์งํ ๋ก๊ทธ
+
+| ๋ ์ง | ๋ฉ๋ด | ๋ด๋น | ์ํ | ๋น๊ณ |
+|------|------|------|------|------|
+| 2026-02-03 | ํ์ง๊ด๋ฆฌ | DDD1542 | ๋ถ์ ์๋ฃ | ๋ง์ด๊ทธ๋ ์ด์
๋๊ธฐ |
+| 2026-02-03 | ๋ฌผ๋ฅ๊ด๋ฆฌ (์ด์ก์
์ฒด) | ์ด์ ์ ํ | โ ์คํจ | component ์ฐ๊ฒฐ ์ค๋ฅ |
+| 2026-02-03 | ๋ฌธ์ ํ์ต | DDD1542 | โ
์๋ฃ | ํต์ฌ 4๊ฐ ๋ฌธ์ ์ ๋
, ํ์ต๋
ธํธ ์์ฑ |
+| **2026-02-04** | **overrides.type ์์ธ ๋ถ์** | **AI** | **โ
์๋ฃ** | **ํต์ฌ ์์ธ ๋ฐ๊ฒฌ: overrides.type ๋๋ฝ** |
+| **2026-02-04** | **์ ์ฒด ์
๋ ฅํผ ์ผ๊ด ์์ ** | **AI** | **โ
์๋ฃ** | **397๊ฐ ํ๋ฉด, 2,455๊ฐ ์ปดํฌ๋ํธ ์์ ** |
+| | ๋ฌผ๋ฅ๊ด๋ฆฌ | - | ๋ฏธ์์ | |
+| | ์์ฐ๊ด๋ฆฌ | - | ๋ฏธ์์ | |
+| | ์์
๊ด๋ฆฌ | - | ๋ฏธ์์ | |
+
+---
+
+## ๋ค์ ์์
์์ฒญ ์์
+
+๋ค์ AI์๊ฒ ์์ฒญํ ๋ ์ด๋ ๊ฒ ๋งํ๋ฉด ๋ฉ๋๋ค:
+
+```
+"๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_๊ฐ์ด๋.md ์ฝ๊ณ ํ์ง๊ด๋ฆฌ ๋ฉ๋ด ๋ง์ด๊ทธ๋ ์ด์
์งํํด์ค"
+
+"๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_๊ฐ์ด๋.md ์ฐธ๊ณ ํด์ ๋ฌผ๋ฅ๊ด๋ฆฌ ๋ฉ๋ด ๋ถ์ํด์ค"
+
+"๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md ๋ณด๊ณ COMPANY_7_142 ํ๋ฉด V2๋ก ๋ณํํด์ค"
+```
+
+---
+
+## ๋ณ๊ฒฝ ์ด๋ ฅ
+
+| ๋ ์ง | ์์ฑ์ | ๋ด์ฉ |
+|------|--------|------|
+| 2026-02-03 | DDD1542 | ์ด์ ์์ฑ |
+| 2026-02-03 | DDD1542 | ์ปดํฌ๋ํธ-์ปฌ๋ผ ์ฐ๊ฒฐ ์ฃผ์์ฌํญ ์ถ๊ฐ (์ด์ ์คํจ ์์ธ) |
+| 2026-02-03 | DDD1542 | ๊ฐ์ธ ํ์ต๋
ธํธ ์์ฑ (V2_๋ง์ด๊ทธ๋ ์ด์
_ํ์ต๋
ธํธ_DDD1542.md) |
+| **2026-02-04** | **AI** | **ํต์ฌ ์์ธ ๋ฐ๊ฒฌ: overrides.type ํ๋ ๋๋ฝ ๋ฌธ์ ** |
+| **2026-02-04** | **AI** | **์ผ๊ด ์์ SQL ์ถ๊ฐ ๋ฐ 397๊ฐ ํ๋ฉด ์์ ์๋ฃ** |
diff --git a/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md b/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md
new file mode 100644
index 00000000..42ce37f1
--- /dev/null
+++ b/docs/DDD1542/๋ณธ์๋ฒ_๊ฐ๋ฐ์๋ฒ_๋ง์ด๊ทธ๋ ์ด์
_์์ธ๊ฐ์ด๋.md
@@ -0,0 +1,553 @@
+# ๋ณธ์๋ฒ โ ๊ฐ๋ฐ์๋ฒ ๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋
+
+## ๊ฐ์
+
+๋ณธ ๋ฌธ์๋ **๋ณธ์๋ฒ(Production)**์ `screen_layouts` (V1) ๋ฐ์ดํฐ๋ฅผ **๊ฐ๋ฐ์๋ฒ(Development)**์ `screen_layouts_v2` ์์คํ
์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ๋ ์ ์ฐจ๋ฅผ ์ ์ํฉ๋๋ค.
+
+### ๋ง์ด๊ทธ๋ ์ด์
๋ฐฉํฅ
+```
+๋ณธ์๋ฒ (Production) ๊ฐ๋ฐ์๋ฒ (Development)
+โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ
+โ screen_layouts (V1) โ โ โ screen_layouts_v2 โ
+โ - ์ปดํฌ๋ํธ๋ณ ๋ ์ฝ๋ โ โ - ํ๋ฉด๋น 1๊ฐ ๋ ์ฝ๋ โ
+โ - properties JSONB โ โ - layout_data JSONB โ
+โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+### ์ต์ข
๋ชฉํ
+๊ฐ๋ฐ์๋ฒ์์ ์์ฑ ํ **๊ฐ๋ฐ์๋ฒ โ ๋ณธ์๋ฒ**๋ก ๋ฐฐํฌ
+
+---
+
+## 1. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด
+
+### 1.1 screen_layouts (V1) - ๋ณธ์๋ฒ
+
+```sql
+-- ์ปดํฌ๋ํธ๋ณ 1๊ฐ ๋ ์ฝ๋
+CREATE TABLE screen_layouts (
+ layout_id SERIAL PRIMARY KEY,
+ screen_id INTEGER,
+ component_type VARCHAR(50),
+ component_id VARCHAR(100),
+ properties JSONB, -- ๋ชจ๋ ์ค์ ๊ฐ ํฌํจ
+ ...
+);
+```
+
+**ํน์ง:**
+- ํ๋ฉด๋น N๊ฐ ๋ ์ฝ๋ (์ปดํฌ๋ํธ ์๋งํผ)
+- `properties`์ ๋ชจ๋ ์ค์ ์ ์ฅ (defaults + overrides ๊ตฌ๋ถ ์์)
+- `menu_objid` ๊ธฐ๋ฐ ์ฑ๋ฒ/์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ
+
+### 1.2 screen_layouts_v2 - ๊ฐ๋ฐ์๋ฒ
+
+```sql
+-- ํ๋ฉด๋น 1๊ฐ ๋ ์ฝ๋
+CREATE TABLE screen_layouts_v2 (
+ layout_id SERIAL PRIMARY KEY,
+ screen_id INTEGER NOT NULL,
+ company_code VARCHAR(20) NOT NULL,
+ layout_data JSONB NOT NULL DEFAULT '{}'::jsonb,
+ UNIQUE(screen_id, company_code)
+);
+```
+
+**layout_data ๊ตฌ์กฐ:**
+```json
+{
+ "version": "2.0",
+ "components": [
+ {
+ "id": "comp_xxx",
+ "url": "@/lib/registry/components/v2-table-list",
+ "position": { "x": 0, "y": 0 },
+ "size": { "width": 100, "height": 50 },
+ "displayOrder": 0,
+ "overrides": {
+ "tableName": "inspection_standard",
+ "columns": ["id", "name"]
+ }
+ }
+ ],
+ "updatedAt": "2026-02-03T12:00:00Z"
+}
+```
+
+**ํน์ง:**
+- ํ๋ฉด๋น 1๊ฐ ๋ ์ฝ๋
+- `url` + `overrides` ๋ฐฉ์ (Zod ์คํค๋ง defaults์ ๋ณํฉ)
+- `table_name + column_name` ๊ธฐ๋ฐ ์ฑ๋ฒ/์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ (์ ์ญ)
+
+---
+
+## 2. ๋ฐ์ดํฐ ํ์
๊ด๋ฆฌ ๊ตฌ์กฐ (V2)
+
+### 2.1 ํต์ฌ ํ
์ด๋ธ ๊ด๊ณ
+
+```
+table_type_columns (์ปฌ๋ผ ํ์
์ ์)
+โโโ input_type = 'category' โ category_values
+โโโ input_type = 'numbering' โ numbering_rules
+โโโ input_type = 'text', 'date', 'number', etc.
+```
+
+### 2.2 table_type_columns
+
+๊ฐ ํ
์ด๋ธ์ ์ปฌ๋ผ๋ณ ์
๋ ฅ ํ์
์ ์ ์ํฉ๋๋ค.
+
+```sql
+SELECT table_name, column_name, input_type, column_label
+FROM table_type_columns
+WHERE input_type IN ('category', 'numbering');
+```
+
+**์ฃผ์ input_type:**
+| input_type | ์ค๋ช
| ์ฐ๊ฒฐ ํ
์ด๋ธ |
+|------------|------|-------------|
+| text | ํ
์คํธ ์
๋ ฅ | - |
+| number | ์ซ์ ์
๋ ฅ | - |
+| date | ๋ ์ง ์
๋ ฅ | - |
+| category | ์นดํ
๊ณ ๋ฆฌ ๋๋กญ๋ค์ด | category_values |
+| numbering | ์๋ ์ฑ๋ฒ | numbering_rules |
+| entity | ์ํฐํฐ ๊ฒ์ | - |
+
+### 2.3 category_values (์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ)
+
+```sql
+-- ์นดํ
๊ณ ๋ฆฌ ๊ฐ ์กฐํ
+SELECT value_id, table_name, column_name, value_code, value_label,
+ parent_value_id, depth, company_code
+FROM category_values
+WHERE table_name = 'inspection_standard'
+ AND column_name = 'inspection_method'
+ AND company_code = 'COMPANY_7';
+```
+
+**V1 vs V2 ์ฐจ์ด:**
+| ๊ตฌ๋ถ | V1 | V2 |
+|------|----|----|
+| ํค | menu_objid | table_name + column_name |
+| ๋ฒ์ | ํ๋ฉด๋ณ | ์ ์ญ (ํ
์ด๋ธ.์ปฌ๋ผ๋ณ) |
+| ๊ณ์ธต | ๋จ์ผ | 3๋จ๊ณ (๋/์ค/์๋ถ๋ฅ) |
+
+### 2.4 numbering_rules (์ฑ๋ฒ ๊ท์น)
+
+```sql
+-- ์ฑ๋ฒ ๊ท์น ์กฐํ
+SELECT rule_id, rule_name, table_name, column_name, separator,
+ reset_period, current_sequence, company_code
+FROM numbering_rules
+WHERE company_code = 'COMPANY_7';
+```
+
+**์ฐ๊ฒฐ ๋ฐฉ์:**
+```
+table_type_columns.detail_settings = '{"numberingRuleId": "rule-xxx"}'
+ โ
+ numbering_rules.rule_id = "rule-xxx"
+```
+
+---
+
+## 3. ์ปดํฌ๋ํธ ๋งคํ
+
+### 3.1 ๊ธฐ๋ณธ ์ปดํฌ๋ํธ ๋งคํ
+
+| V1 (๋ณธ์๋ฒ) | V2 (๊ฐ๋ฐ์๋ฒ) | ๋น๊ณ |
+|-------------|---------------|------|
+| table-list | v2-table-list | ํ
์ด๋ธ ๋ชฉ๋ก |
+| button-primary | v2-button-primary | ๋ฒํผ |
+| text-input | v2-text-input | ํ
์คํธ ์
๋ ฅ |
+| select-basic | v2-select | ๋๋กญ๋ค์ด |
+| date-input | v2-date-input | ๋ ์ง ์
๋ ฅ |
+| entity-search-input | v2-entity-search | ์ํฐํฐ ๊ฒ์ |
+| tabs-widget | v2-tabs-widget | ํญ |
+
+### 3.2 ํน์ ์ปดํฌ๋ํธ ๋งคํ
+
+| V1 (๋ณธ์๋ฒ) | V2 (๊ฐ๋ฐ์๋ฒ) | ๋ง์ด๊ทธ๋ ์ด์
๋ฐฉ์ |
+|-------------|---------------|-------------------|
+| category-manager | v2-category-manager | table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| numbering-rule | v2-numbering-rule | table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| ๋ชจ๋ฌ ํ๋ฉด | overlay ํตํฉ | ๋ถ๋ชจ ํ๋ฉด์ ํตํฉ |
+
+### 3.3 ๋ชจ๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์ ๋ณ๊ฒฝ
+
+**V1 (๋ณธ์๋ฒ):**
+```
+ํ๋ฉด A (screen_id: 142) - ๊ฒ์ฌ์ฅ๋น๊ด๋ฆฌ
+ โโโ ๋ฒํผ ํด๋ฆญ โ ํ๋ฉด B (screen_id: 143) - ๊ฒ์ฌ์ฅ๋น ๋ฑ๋ก๋ชจ๋ฌ
+```
+
+**V2 (๊ฐ๋ฐ์๋ฒ):**
+```
+ํ๋ฉด A (screen_id: 142) - ๊ฒ์ฌ์ฅ๋น๊ด๋ฆฌ
+ โโโ v2-dialog-form ์ปดํฌ๋ํธ๋ก ๋ชจ๋ฌ ํตํฉ
+```
+
+---
+
+## 4. ๋ง์ด๊ทธ๋ ์ด์
์ ์ฐจ
+
+### 4.1 ์ฌ์ ๋ถ์
+
+```sql
+-- 1. ๋ณธ์๋ฒ ํ๋ฉด ๋ชฉ๋ก ํ์ธ
+SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
+ COUNT(sl.layout_id) as component_count
+FROM screen_definitions sd
+LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+WHERE sd.screen_code LIKE 'COMPANY_7_%'
+ AND sd.screen_name LIKE '%ํ์ง%'
+GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;
+
+-- 2. ๊ฐ๋ฐ์๋ฒ V2 ํ๋ฉด ํํฉ ํ์ธ
+SELECT sd.screen_id, sd.screen_code, sd.screen_name,
+ sv2.layout_data IS NOT NULL as has_v2_layout
+FROM screen_definitions sd
+LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
+WHERE sd.company_code = 'COMPANY_7';
+```
+
+### 4.2 Step 1: screen_definitions ๋๊ธฐํ
+
+```sql
+-- ๋ณธ์๋ฒ์๋ง ์๋ ํ๋ฉด์ ๊ฐ๋ฐ์๋ฒ์ ์ถ๊ฐ
+INSERT INTO screen_definitions (screen_code, screen_name, table_name, company_code, ...)
+SELECT screen_code, screen_name, table_name, company_code, ...
+FROM [๋ณธ์๋ฒ].screen_definitions
+WHERE screen_code NOT IN (SELECT screen_code FROM screen_definitions);
+```
+
+### 4.3 Step 2: V1 โ V2 ๋ ์ด์์ ๋ณํ
+
+```typescript
+// ๋ณํ ๋ก์ง (pseudo-code)
+async function convertV1toV2(screenId: number, companyCode: string) {
+ // 1. V1 ๋ ์ด์์ ์กฐํ
+ const v1Layouts = await getV1Layouts(screenId);
+
+ // 2. V2 ํ์์ผ๋ก ๋ณํ
+ const v2Layout = {
+ version: "2.0",
+ components: v1Layouts.map(v1 => ({
+ id: v1.component_id,
+ url: mapComponentUrl(v1.component_type),
+ position: { x: v1.position_x, y: v1.position_y },
+ size: { width: v1.width, height: v1.height },
+ displayOrder: v1.display_order,
+ overrides: extractOverrides(v1.properties)
+ })),
+ updatedAt: new Date().toISOString()
+ };
+
+ // 3. V2 ํ
์ด๋ธ์ ์ ์ฅ
+ await saveV2Layout(screenId, companyCode, v2Layout);
+}
+
+function mapComponentUrl(v1Type: string): string {
+ const mapping = {
+ 'table-list': '@/lib/registry/components/v2-table-list',
+ 'button-primary': '@/lib/registry/components/v2-button-primary',
+ 'category-manager': '@/lib/registry/components/v2-category-manager',
+ 'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
+ // ... ๊ธฐํ ๋งคํ
+ };
+ return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
+}
+```
+
+### 4.4 Step 3: ์นดํ
๊ณ ๋ฆฌ ๋ฐ์ดํฐ ๋ง์ด๊ทธ๋ ์ด์
+
+```sql
+-- ๋ณธ์๋ฒ ์นดํ
๊ณ ๋ฆฌ ๋ฐ์ดํฐ โ ๊ฐ๋ฐ์๋ฒ category_values
+INSERT INTO category_values (
+ table_name, column_name, value_code, value_label,
+ value_order, parent_value_id, depth, company_code
+)
+SELECT
+ -- V1 ์นดํ
๊ณ ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ table_name + column_name ๊ธฐ๋ฐ์ผ๋ก ๋ณํ
+ 'inspection_standard' as table_name,
+ 'inspection_method' as column_name,
+ value_code,
+ value_label,
+ sort_order,
+ NULL as parent_value_id,
+ 1 as depth,
+ 'COMPANY_7' as company_code
+FROM [๋ณธ์๋ฒ_์นดํ
๊ณ ๋ฆฌ_๋ฐ์ดํฐ];
+```
+
+### 4.5 Step 4: ์ฑ๋ฒ ๊ท์น ๋ง์ด๊ทธ๋ ์ด์
+
+```sql
+-- ๋ณธ์๋ฒ ์ฑ๋ฒ ๊ท์น โ ๊ฐ๋ฐ์๋ฒ numbering_rules
+INSERT INTO numbering_rules (
+ rule_id, rule_name, table_name, column_name,
+ separator, reset_period, current_sequence, company_code
+)
+SELECT
+ rule_id,
+ rule_name,
+ 'inspection_standard' as table_name,
+ 'inspection_code' as column_name,
+ separator,
+ reset_period,
+ 0 as current_sequence, -- ์ํ์ค ์ด๊ธฐํ
+ 'COMPANY_7' as company_code
+FROM [๋ณธ์๋ฒ_์ฑ๋ฒ_๊ท์น];
+```
+
+### 4.6 Step 5: table_type_columns ์ค์
+
+```sql
+-- ์นดํ
๊ณ ๋ฆฌ ์ปฌ๋ผ ์ค์
+UPDATE table_type_columns
+SET input_type = 'category'
+WHERE table_name = 'inspection_standard'
+ AND column_name = 'inspection_method'
+ AND company_code = 'COMPANY_7';
+
+-- ์ฑ๋ฒ ์ปฌ๋ผ ์ค์
+UPDATE table_type_columns
+SET
+ input_type = 'numbering',
+ detail_settings = '{"numberingRuleId": "rule-xxx"}'
+WHERE table_name = 'inspection_standard'
+ AND column_name = 'inspection_code'
+ AND company_code = 'COMPANY_7';
+```
+
+---
+
+## 5. ํ์ง๊ด๋ฆฌ ๋ฉ๋ด ๋ง์ด๊ทธ๋ ์ด์
ํํฉ
+
+### 5.1 ํ๋ฉด ๋งคํ ํํฉ
+
+| ๋ณธ์๋ฒ ์ฝ๋ | ํ๋ฉด๋ช
| ํ
์ด๋ธ | ๊ฐ๋ฐ์๋ฒ ์ํ | ๋น๊ณ |
+|-------------|--------|--------|---------------|------|
+| COMPANY_7_126 | ๊ฒ์ฌ์ ๋ณด ๊ด๋ฆฌ | inspection_standard | โ
V2 ์กด์ฌ | ์ปดํฌ๋ํธ ์ ํ์ธ ํ์ |
+| COMPANY_7_127 | ํ๋ชฉ์ต์
์ค์ | - | โ
V2 ์กด์ฌ | v2-category-manager ์ฌ์ฉ์ค |
+| COMPANY_7_138 | ์นดํ
๊ณ ๋ฆฌ ์ค์ | inspection_standard | โ ๋๋ฝ | V2: table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| COMPANY_7_139 | ์ฝ๋ ์ค์ | inspection_standard | โ ๋๋ฝ | V2: table_name ๊ธฐ๋ฐ์ผ๋ก ๋ณ๊ฒฝ |
+| COMPANY_7_142 | ๊ฒ์ฌ์ฅ๋น ๊ด๋ฆฌ | inspection_equipment_mng | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ ํ์ |
+| COMPANY_7_143 | ๊ฒ์ฌ์ฅ๋น ๋ฑ๋ก๋ชจ๋ฌ | inspection_equipment_mng | โ ๋๋ฝ | COMPANY_7_142์ ํตํฉ |
+| COMPANY_7_144 | ๋ถ๋๊ธฐ์ค ์ ๋ณด | defect_standard_mng | โ ๋๋ฝ | ๋ชจ๋ฌ ํตํฉ ํ์ |
+| COMPANY_7_145 | ๋ถ๋๊ธฐ์ค ๋ฑ๋ก๋ชจ๋ฌ | defect_standard_mng | โ ๋๋ฝ | COMPANY_7_144์ ํตํฉ |
+
+### 5.2 ์นดํ
๊ณ ๋ฆฌ/์ฑ๋ฒ ์ปฌ๋ผ ํํฉ
+
+**inspection_standard:**
+| ์ปฌ๋ผ | input_type | ๋ผ๋ฒจ |
+|------|------------|------|
+| inspection_method | category | ๊ฒ์ฌ๋ฐฉ๋ฒ |
+| unit | category | ๋จ์ |
+| apply_type | category | ์ ์ฉ๊ตฌ๋ถ |
+| inspection_type | category | ์ ํ |
+
+**inspection_equipment_mng:**
+| ์ปฌ๋ผ | input_type | ๋ผ๋ฒจ |
+|------|------------|------|
+| equipment_type | category | ์ฅ๋น์ ํ |
+| installation_location | category | ์ค์น์ฅ์ |
+| equipment_status | category | ์ฅ๋น์ํ |
+
+**defect_standard_mng:**
+| ์ปฌ๋ผ | input_type | ๋ผ๋ฒจ |
+|------|------------|------|
+| defect_type | category | ๋ถ๋์ ํ |
+| severity | category | ์ฌ๊ฐ๋ |
+| inspection_type | category | ๊ฒ์ฌ์ ํ |
+
+---
+
+## 6. ์๋ํ ์คํฌ๋ฆฝํธ
+
+### 6.1 ๋ง์ด๊ทธ๋ ์ด์
์คํ ์คํฌ๋ฆฝํธ
+
+```typescript
+// backend-node/src/scripts/migrateV1toV2.ts
+import { getPool } from "../database/db";
+
+interface MigrationResult {
+ screenCode: string;
+ success: boolean;
+ message: string;
+ componentCount?: number;
+}
+
+async function migrateScreenToV2(
+ screenCode: string,
+ companyCode: string
+): Promise
{
+ const pool = getPool();
+
+ try {
+ // 1. V1 ๋ ์ด์์ ์กฐํ (๋ณธ์๋ฒ์์)
+ const v1Result = await pool.query(`
+ SELECT sl.*, sd.table_name, sd.screen_name
+ FROM screen_layouts sl
+ JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
+ WHERE sd.screen_code = $1
+ ORDER BY sl.display_order
+ `, [screenCode]);
+
+ if (v1Result.rows.length === 0) {
+ return { screenCode, success: false, message: "V1 ๋ ์ด์์ ์์" };
+ }
+
+ // 2. V2 ํ์์ผ๋ก ๋ณํ
+ const components = v1Result.rows
+ .filter(row => row.component_type !== '_metadata')
+ .map(row => ({
+ id: row.component_id || `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ url: mapComponentUrl(row.component_type),
+ position: { x: row.position_x || 0, y: row.position_y || 0 },
+ size: { width: row.width || 100, height: row.height || 50 },
+ displayOrder: row.display_order || 0,
+ overrides: extractOverrides(row.properties, row.component_type)
+ }));
+
+ const layoutData = {
+ version: "2.0",
+ components,
+ migratedFrom: "V1",
+ migratedAt: new Date().toISOString()
+ };
+
+ // 3. ๊ฐ๋ฐ์๋ฒ V2 ํ
์ด๋ธ์ ์ ์ฅ
+ const screenId = v1Result.rows[0].screen_id;
+
+ await pool.query(`
+ INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data)
+ VALUES ($1, $2, $3)
+ ON CONFLICT (screen_id, company_code)
+ DO UPDATE SET layout_data = $3, updated_at = NOW()
+ `, [screenId, companyCode, JSON.stringify(layoutData)]);
+
+ return {
+ screenCode,
+ success: true,
+ message: "๋ง์ด๊ทธ๋ ์ด์
์๋ฃ",
+ componentCount: components.length
+ };
+ } catch (error: any) {
+ return { screenCode, success: false, message: error.message };
+ }
+}
+
+function mapComponentUrl(v1Type: string): string {
+ const mapping: Record = {
+ 'table-list': '@/lib/registry/components/v2-table-list',
+ 'button-primary': '@/lib/registry/components/v2-button-primary',
+ 'text-input': '@/lib/registry/components/v2-text-input',
+ 'select-basic': '@/lib/registry/components/v2-select',
+ 'date-input': '@/lib/registry/components/v2-date-input',
+ 'entity-search-input': '@/lib/registry/components/v2-entity-search',
+ 'category-manager': '@/lib/registry/components/v2-category-manager',
+ 'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
+ 'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
+ 'textarea-basic': '@/lib/registry/components/v2-textarea',
+ };
+ return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
+}
+
+function extractOverrides(properties: any, componentType: string): Record {
+ if (!properties) return {};
+
+ // V2 Zod ์คํค๋ง defaults์ ๋น๊ตํ์ฌ ๋ค๋ฅธ ๊ฐ๋ง ์ถ์ถ
+ // (์ค์ ๊ตฌํ ์ ๊ฐ ์ปดํฌ๋ํธ์ defaultConfig์ ๋น๊ต)
+ const overrides: Record = {};
+
+ // ํ์ ์ค์ ๋ง ์ถ์ถ
+ if (properties.tableName) overrides.tableName = properties.tableName;
+ if (properties.columns) overrides.columns = properties.columns;
+ if (properties.label) overrides.label = properties.label;
+ if (properties.onClick) overrides.onClick = properties.onClick;
+
+ return overrides;
+}
+```
+
+---
+
+## 7. ๊ฒ์ฆ ์ฒดํฌ๋ฆฌ์คํธ
+
+### 7.1 ๋ง์ด๊ทธ๋ ์ด์
์
+
+- [ ] ๋ณธ์๋ฒ ํ๋ฉด ๋ชฉ๋ก ํ์ธ
+- [ ] ๊ฐ๋ฐ์๋ฒ ๊ธฐ์กด V2 ๋ฐ์ดํฐ ๋ฐฑ์
+- [ ] ์ปดํฌ๋ํธ ๋งคํ ํ
์ด๋ธ ๊ฒํ
+- [ ] ์นดํ
๊ณ ๋ฆฌ/์ฑ๋ฒ ๋ฐ์ดํฐ ๋ถ์
+
+### 7.2 ๋ง์ด๊ทธ๋ ์ด์
ํ
+
+- [ ] screen_definitions ๋๊ธฐํ ํ์ธ
+- [ ] screen_layouts_v2 ๋ฐ์ดํฐ ์์ฑ ํ์ธ
+- [ ] ์ปดํฌ๋ํธ ๋ ๋๋ง ํ
์คํธ
+- [ ] ์นดํ
๊ณ ๋ฆฌ ๋๋กญ๋ค์ด ๋์ ํ์ธ
+- [ ] ์ฑ๋ฒ ๊ท์น ๋์ ํ์ธ
+- [ ] ์ ์ฅ/์์ /์ญ์ ๊ธฐ๋ฅ ํ
์คํธ
+
+### 7.3 ๋ชจ๋ฌ ํตํฉ ํ์ธ
+
+- [ ] ๊ธฐ์กด ๋ชจ๋ฌ ํ๋ฉด โ overlay ํตํฉ ์๋ฃ
+- [ ] ๋ถ๋ชจ-์์ ๋ฐ์ดํฐ ์ฐ๋ ํ์ธ
+- [ ] ๋ชจ๋ฌ ์ด๊ธฐ/๋ซ๊ธฐ ๋์ ํ์ธ
+
+---
+
+## 8. ๋กค๋ฐฑ ๊ณํ
+
+๋ง์ด๊ทธ๋ ์ด์
์คํจ ์ ๋กค๋ฐฑ ์ ์ฐจ:
+
+```sql
+-- 1. V2 ๋ ์ด์์ ๋กค๋ฐฑ
+DELETE FROM screen_layouts_v2
+WHERE screen_id IN (
+ SELECT screen_id FROM screen_definitions
+ WHERE screen_code LIKE 'COMPANY_7_%'
+);
+
+-- 2. ์ถ๊ฐ๋ screen_definitions ๋กค๋ฐฑ
+DELETE FROM screen_definitions
+WHERE screen_code IN ('์ ๊ท_์ถ๊ฐ๋_์ฝ๋๋ค')
+ AND company_code = 'COMPANY_7';
+
+-- 3. category_values ๋กค๋ฐฑ
+DELETE FROM category_values
+WHERE company_code = 'COMPANY_7'
+ AND created_at > '[๋ง์ด๊ทธ๋ ์ด์
_์์_์๊ฐ]';
+
+-- 4. numbering_rules ๋กค๋ฐฑ
+DELETE FROM numbering_rules
+WHERE company_code = 'COMPANY_7'
+ AND created_at > '[๋ง์ด๊ทธ๋ ์ด์
_์์_์๊ฐ]';
+```
+
+---
+
+## 9. ์ฐธ๊ณ ์๋ฃ
+
+### ๊ด๋ จ ์ฝ๋ ํ์ผ
+
+- **V2 Category Manager**: `frontend/lib/registry/components/v2-category-manager/`
+- **V2 Numbering Rule**: `frontend/lib/registry/components/v2-numbering-rule/`
+- **Category Service**: `backend-node/src/services/categoryTreeService.ts`
+- **Numbering Service**: `backend-node/src/services/numberingRuleService.ts`
+
+### ๊ด๋ จ ๋ฌธ์
+
+- [V2 ์ปดํฌ๋ํธ ๋ถ์ ๊ฐ์ด๋](../V2_์ปดํฌ๋ํธ_๋ถ์_๊ฐ์ด๋.md)
+- [V2 ์ปดํฌ๋ํธ ์ฐ๋ ๊ฐ์ด๋](../V2_์ปดํฌ๋ํธ_์ฐ๋_๊ฐ์ด๋.md)
+- [ํ๋ฉด ๊ฐ๋ฐ ํ์ค ๊ฐ์ด๋](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)
+- [์ปดํฌ๋ํธ ๋ ์ด์์ V2 ์ํคํ
์ฒ](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md)
+
+---
+
+## ๋ณ๊ฒฝ ์ด๋ ฅ
+
+| ๋ ์ง | ์์ฑ์ | ๋ด์ฉ |
+|------|--------|------|
+| 2026-02-03 | DDD1542 | ์ด์ ์์ฑ |
diff --git a/docs/DDD1542/ํ๋ฉด๊ด๊ณ_์๊ฐํ_๊ฐ์ _๋ณด๊ณ ์.md b/docs/DDD1542/ํ๋ฉด๊ด๊ณ_์๊ฐํ_๊ฐ์ _๋ณด๊ณ ์.md
index 27946afa..aea92243 100644
--- a/docs/DDD1542/ํ๋ฉด๊ด๊ณ_์๊ฐํ_๊ฐ์ _๋ณด๊ณ ์.md
+++ b/docs/DDD1542/ํ๋ฉด๊ด๊ณ_์๊ฐํ_๊ฐ์ _๋ณด๊ณ ์.md
@@ -23,7 +23,8 @@
| ํ
์ด๋ธ๋ช
| ์ฉ๋ | ์ฃผ์ ์ปฌ๋ผ |
|----------|------|----------|
| `screen_definitions` | ํ๋ฉด ์ ์ ์ ๋ณด | `screen_id`, `screen_name`, `table_name`, `company_code` |
-| `screen_layouts` | ํ๋ฉด ๋ ์ด์์/์ปดํฌ๋ํธ ์ ๋ณด | `screen_id`, `properties` (JSONB - componentConfig ํฌํจ) |
+| `screen_layouts` | ํ๋ฉด ๋ ์ด์์/์ปดํฌ๋ํธ ์ ๋ณด (Legacy) | `screen_id`, `properties` (JSONB - componentConfig ํฌํจ) |
+| `screen_layouts_v2` | ํ๋ฉด ๋ ์ด์์/์ปดํฌ๋ํธ ์ ๋ณด (V2) | `screen_id`, `layout_data` (JSONB - components ๋ฐฐ์ด) |
| `screen_groups` | ํ๋ฉด ๊ทธ๋ฃน ์ ๋ณด | `group_id`, `group_code`, `group_name`, `parent_group_id` |
| `screen_group_mappings` | ํ๋ฉด-๊ทธ๋ฃน ๋งคํ | `group_id`, `screen_id`, `display_order` |
@@ -86,9 +87,17 @@ screen_groups (๊ทธ๋ฃน)
โ โ
โ โโโโ screen_definitions (ํ๋ฉด)
โ โ
- โ โโโโ screen_layouts (๋ ์ด์์/์ปดํฌ๋ํธ)
+ โ โโโโ screen_layouts (Legacy)
+ โ โ โ
+ โ โ โโโโ properties.componentConfig
+ โ โ โโโ fieldMappings
+ โ โ โโโ parentDataMapping
+ โ โ โโโ columns.mapping
+ โ โ โโโ rightPanel.relation
+ โ โ
+ โ โโโโ screen_layouts_v2 (V2) โ ํ์ฌ ํ์ค
โ โ
- โ โโโโ properties.componentConfig
+ โ โโโโ layout_data.components[].overrides
โ โโโ fieldMappings
โ โโโ parentDataMapping
โ โโโ columns.mapping
@@ -1120,9 +1129,12 @@ screenSubTables[screenId].subTables.push({
21. [x] ํํฐ ์ฐ๊ฒฐ์ ํฌ์ปค์ฑ ์ ์ด (ํด๋น ํ๋ฉด ํฌ์ปค์ฑ ์์๋ง ํ์)
22. [x] ์ ์ฅ ํ
์ด๋ธ ์ ์ธ ์กฐ๊ฑด ์ถ๊ฐ (table-list + ์ฒดํฌ๋ฐ์ค + openModalWithData)
23. [x] ์ฒซ ์ง์
์ ํฌ์ปค์ฑ ์์ด ์์ (ํธ๋ฆฌ์์ ํ๋ฉด ํด๋ฆญ ์ ๊ทธ๋ฃน๋ง ์ง์
)
-24. [ ] **์ ๊ต์ฐจ์ ์ด์ง๊ฐ ํด๊ฒฐ** (๊ณํ ์ค)
-22. [ ] ๋ฒ๋ก UI ์ถ๊ฐ (์ ํ์ฌํญ)
-23. [ ] ์ฃ์ง ๋ผ๋ฒจ์ ๊ด๊ณ ์ ํ ํ์ (์ ํ์ฌํญ)
+24. [x] **screen_layouts_v2 ์ง์ ์ถ๊ฐ** (rightPanel.relation V2 UNION ์ฟผ๋ฆฌ) โ
2026-01-30
+25. [x] **ํ
์ด๋ธ ๋ถ๋ฅ ์ฐ์ ์์ ์์คํ
** (๋ฉ์ธ > ์๋ธ ์ฐ์ ์์ ์ ์ฉ) โ
2026-01-30
+26. [x] **globalMainTables API ์ถ๊ฐ** (WHERE ์กฐ๊ฑด ๋์ ํ
์ด๋ธ ๋ชฉ๋ก ๋ฐํ) โ
2026-01-30
+27. [ ] **์ ๊ต์ฐจ์ ์ด์ง๊ฐ ํด๊ฒฐ** (๊ณํ ์ค)
+28. [ ] ๋ฒ๋ก UI ์ถ๊ฐ (์ ํ์ฌํญ)
+29. [ ] ์ฃ์ง ๋ผ๋ฒจ์ ๊ด๊ณ ์ ํ ํ์ (์ ํ์ฌํญ)
---
@@ -1682,6 +1694,149 @@ frontend/
---
+## ํ
์ด๋ธ ๋ถ๋ฅ ์ฐ์ ์์ ์์คํ
(2026-01-30)
+
+### ๋ฐฐ๊ฒฝ
+
+๋ง์คํฐ-๋ํ
์ผ ๊ด๊ณ์ ๋ํ
์ผ ํ
์ด๋ธ(์: `user_dept`)์ด ๋ค๋ฅธ ๊ณณ์์ autocomplete ์ฐธ์กฐ๋ก๋ ์ฌ์ฉ๋๋ ๊ฒฝ์ฐ,
+์๋ธ ํ
์ด๋ธ ์์ญ์ ์๋ชป ๋ฐฐ์น๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
+
+### ๋ฌธ์ ์ํฉ
+
+```
+[user_info] - ํ๋ฉด 139์ ๋ํ
์ผ โ ๋ฉ์ธ ํ
์ด๋ธ ์์ญ (O)
+[user_dept] - ํ๋ฉด 162์ ๋ํ
์ผ์ด์ง๋ง autocomplete ์ฐธ์กฐ๋ ์์ โ ์๋ธ ํ
์ด๋ธ ์์ญ (X)
+```
+
+**์์ธ**: ํ
์ด๋ธ ๋ถ๋ฅ ์ ์ฐ์ ์์๊ฐ ์์ด์ ๋จผ์ ๋ฐ๊ฒฌ๋ ๊ด๊ณ ํ์
์ผ๋ก ๋ถ๋ฅ๋จ
+
+### ํด๊ฒฐ์ฑ
: ์ฐ์ ์์ ๊ธฐ๋ฐ ํ
์ด๋ธ ๋ถ๋ฅ
+
+#### ๋ถ๋ฅ ๊ท์น
+
+| ์ฐ์ ์์ | ๋ถ๋ฅ | ์กฐ๊ฑด | ๋น๊ณ |
+|----------|------|------|------|
+| **1์์** | ๋ฉ์ธ ํ
์ด๋ธ | `screen_definitions.table_name` | ์ปดํฌ๋ํธ ์ง์ ์ฐ๊ฒฐ |
+| **1์์** | ๋ฉ์ธ ํ
์ด๋ธ | `v2-split-panel-layout.rightPanel.tableName` | WHERE ์กฐ๊ฑด ๋์ |
+| **2์์** | ์๋ธ ํ
์ด๋ธ | ์กฐ์ธ์ผ๋ก๋ง ์ฐ๊ฒฐ๋ ํ
์ด๋ธ | autocomplete ๋ฑ ์ฐธ์กฐ |
+
+#### ํต์ฌ ๊ท์น
+
+> **๋ฉ์ธ ์กฐ๊ฑด์ ํด๋นํ๋ฉด, ์๋ธ ์กฐ๊ฑด์ด ์์ด๋ ๋ฌด์กฐ๊ฑด ๋ฉ์ธ์ผ๋ก ๋ถ๋ฅ**
+
+### ๋ฐฑ์๋ ๋ณ๊ฒฝ (`screenGroupController.ts`)
+
+#### 1. screen_layouts_v2 ์ง์ ์ถ๊ฐ
+
+`rightPanelQuery`์ V2 ํ
์ด๋ธ UNION ์ถ๊ฐ:
+
+```sql
+-- V1: screen_layouts์์ ์กฐํ
+SELECT ...
+FROM screen_definitions sd
+JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+WHERE sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL
+
+UNION ALL
+
+-- V2: screen_layouts_v2์์ ์กฐํ (v2-split-panel-layout ์ปดํฌ๋ํธ)
+SELECT
+ sd.screen_id,
+ comp->'overrides'->>'type' as component_type,
+ comp->'overrides'->'rightPanel'->'relation' as right_panel_relation,
+ comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table,
+ ...
+FROM screen_definitions sd
+JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id,
+jsonb_array_elements(slv2.layout_data->'components') as comp
+WHERE comp->'overrides'->'rightPanel'->'relation' IS NOT NULL
+```
+
+#### 2. globalMainTables API ์ถ๊ฐ
+
+`getScreenSubTables` ์๋ต์ ์ ์ญ ๋ฉ์ธ ํ
์ด๋ธ ๋ชฉ๋ก ์ถ๊ฐ:
+
+```sql
+-- ๋ชจ๋ ํ๋ฉด์ ๋ฉ์ธ ํ
์ด๋ธ ์์ง
+SELECT DISTINCT table_name as main_table FROM screen_definitions WHERE screen_id = ANY($1)
+UNION
+SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table
+FROM screen_layouts_v2 ...
+```
+
+**์๋ต ๊ตฌ์กฐ:**
+```typescript
+res.json({
+ success: true,
+ data: screenSubTables,
+ globalMainTables: globalMainTables, // ๋ฉ์ธ ํ
์ด๋ธ ๋ชฉ๋ก ์ถ๊ฐ
+});
+```
+
+### ํ๋ก ํธ์๋ ๋ณ๊ฒฝ (`ScreenRelationFlow.tsx`)
+
+#### 1. globalMainTables ์ํ ์ถ๊ฐ
+
+```typescript
+const [globalMainTables, setGlobalMainTables] = useState>(new Set());
+```
+
+#### 2. ์ฐ์ ์์ ๊ธฐ๋ฐ ํ
์ด๋ธ ๋ถ๋ฅ
+
+```typescript
+// 1. globalMainTables๋ฅผ mainTableSet์ ๋จผ์ ์ถ๊ฐ (์ฐ์ ์์ ์ ์ฉ)
+globalMainTables.forEach((tableName) => {
+ if (!mainTableSet.has(tableName)) {
+ mainTableSet.add(tableName);
+ filterTableSet.add(tableName); // ๋ณด๋ผ์ ํ
๋๋ฆฌ
+ }
+});
+
+// 2. ์๋ธ ํ
์ด๋ธ ์์ง (mainTableSet์ ์๋ ๊ฒ๋ง)
+screenSubData.subTables.forEach((subTable) => {
+ if (mainTableSet.has(subTable.tableName)) {
+ return; // ๋ฉ์ธ ํ
์ด๋ธ์ ์๋ธ์์ ์ ์ธ
+ }
+ subTableSet.add(subTable.tableName);
+});
+```
+
+### ์๊ฐ์ ๊ฒฐ๊ณผ
+
+#### ๋ณ๊ฒฝ ์
+
+```
+[ํ๋ฉด ๋
ธ๋๋ค]
+ โ
+ โผ
+[๋ฉ์ธ ํ
์ด๋ธ: dept_info, user_info] โ user_dept ์์
+ โ
+ โผ
+[์๋ธ ํ
์ด๋ธ: user_dept, customer_mng] โ user_dept๊ฐ ์๋ชป ๋ฐฐ์น๋จ
+```
+
+#### ๋ณ๊ฒฝ ํ
+
+```
+[ํ๋ฉด ๋
ธ๋๋ค]
+ โ
+ โผ
+[๋ฉ์ธ ํ
์ด๋ธ: dept_info, user_info, user_dept] โ user_dept ๋ณด๋ผ์ ํ
๋๋ฆฌ
+ โ
+ โผ
+[์๋ธ ํ
์ด๋ธ: customer_mng] โ ์กฐ์ธ ์ฐธ์กฐ์ฉ ํ
์ด๋ธ๋ง
+```
+
+### ๊ด๋ จ ํ์ผ
+
+| ํ์ผ | ๋ณ๊ฒฝ ๋ด์ฉ |
+|------|----------|
+| `backend-node/src/controllers/screenGroupController.ts` | screen_layouts_v2 UNION ์ถ๊ฐ, globalMainTables ๋ฐํ |
+| `frontend/components/screen/ScreenRelationFlow.tsx` | globalMainTables ์ํ, ์ฐ์ ์์ ๋ถ๋ฅ ๋ก์ง |
+| `frontend/components/screen/ScreenNode.tsx` | isFilterTable prop ๋ฐ ๋ณด๋ผ์ ํ
๋๋ฆฌ ์คํ์ผ |
+
+---
+
## ํ๋ฉด ์ค์ ๋ชจ๋ฌ ๊ฐ์ (2026-01-12)
### ๊ฐ์
@@ -1742,4 +1897,6 @@ npm install react-zoom-pan-pinch
- [๋ฉํฐํ
๋์ ๊ตฌํ ๊ฐ์ด๋](.cursor/rules/multi-tenancy-guide.mdc)
- [API ํด๋ผ์ด์ธํธ ์ฌ์ฉ ๊ท์น](.cursor/rules/api-client-usage.mdc)
- [๊ด๋ฆฌ์ ํ์ด์ง ์คํ์ผ ๊ฐ์ด๋](.cursor/rules/admin-page-style-guide.mdc)
+- [ํ๋ฉด ๋ณต์ V2 ๋ง์ด๊ทธ๋ ์ด์
๊ณํ์](../SCREEN_COPY_V2_MIGRATION_PLAN.md) - screen_layouts_v2 ๋ณต์ ๋ก์ง
+- [V2 ์ปดํฌ๋ํธ ๋ง์ด๊ทธ๋ ์ด์
๋ถ์](../V2_COMPONENT_MIGRATION_ANALYSIS.md) - V2 ์ํคํ
์ฒ
diff --git a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
index 7e1afcba..c60f1dfb 100644
--- a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
+++ b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md
@@ -467,9 +467,9 @@ V2 ์ ํ ๋กค๋ฐฑ (ํ์์):
- [x] copyScreens() - Legacy ์ ๊ฑฐ, V2๋ก ๊ต์ฒด โ
2026-01-28
- [x] hasLayoutChangesV2() ํจ์ ์ถ๊ฐ โ
2026-01-28
- [x] updateTabScreenReferences() V2 ์ง์ ์ถ๊ฐ โ
2026-01-28
-- [ ] ๋จ์ ํ
์คํธ ํต๊ณผ
-- [ ] ํตํฉ ํ
์คํธ ํต๊ณผ
-- [ ] V2 ์ ์ฉ ๋ณต์ ๋์ ํ์ธ
+- [x] ๋จ์ ํ
์คํธ ํต๊ณผ โ
2026-01-30
+- [x] ํตํฉ ํ
์คํธ ํต๊ณผ โ
2026-01-30
+- [x] V2 ์ ์ฉ ๋ณต์ ๋์ ํ์ธ โ
2026-01-30
### 9.3 Phase 2 ์๋ฃ ์กฐ๊ฑด
@@ -522,3 +522,4 @@ V2 ์ ํ ๋กค๋ฐฑ (ํ์์):
| 2026-01-28 | ์๋ฎฌ๋ ์ด์
๊ฒ์ฆ - updateTabScreenReferences V2 ์ง์ ์ถ๊ฐ | Claude |
| 2026-01-28 | V2 ๊ฒฝ๋ก ์ง์ ์ถ๊ฐ - action/sections ์ง์ ๊ฒฝ๋ก (componentConfig ์์ด) | Claude |
| 2026-01-30 | **์ค์ ์ฝ๋ ๊ตฌํ ์๋ฃ** - copyScreen(), copyScreens() V2 ์ ํ | Claude |
+| 2026-01-30 | **Phase 1 ํ
์คํธ ์๋ฃ** - ๋จ์/ํตํฉ ํ
์คํธ ํต๊ณผ ํ์ธ | Claude |
\ No newline at end of file
diff --git a/docs/v2-sales-order-modal-layout.json b/docs/v2-sales-order-modal-layout.json
new file mode 100644
index 00000000..6c8287e0
--- /dev/null
+++ b/docs/v2-sales-order-modal-layout.json
@@ -0,0 +1,557 @@
+{
+ "version": "2.0",
+ "screenResolution": {
+ "width": 1400,
+ "height": 900,
+ "name": "์์ฃผ๋ฑ๋ก ๋ชจ๋ฌ",
+ "category": "modal"
+ },
+ "components": [
+ {
+ "id": "section-options",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 20, "z": 1 },
+ "size": { "width": 1360, "height": 80 },
+ "overrides": {
+ "componentConfig": {
+ "title": "",
+ "showHeader": false,
+ "padding": "md",
+ "borderStyle": "solid"
+ }
+ },
+ "displayOrder": 0
+ },
+ {
+ "id": "select-input-method",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 40, "y": 35, "z": 2 },
+ "size": { "width": 300, "height": 40 },
+ "overrides": {
+ "label": "์
๋ ฅ ๋ฐฉ์",
+ "columnName": "input_method",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "customer_first", "label": "๊ฑฐ๋์ฒ ์ฐ์ " },
+ { "value": "item_first", "label": "ํ๋ชฉ ์ฐ์ " }
+ ],
+ "placeholder": "์
๋ ฅ ๋ฐฉ์ ์ ํ"
+ },
+ "displayOrder": 1
+ },
+ {
+ "id": "select-sales-type",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 360, "y": 35, "z": 2 },
+ "size": { "width": 300, "height": 40 },
+ "overrides": {
+ "label": "ํ๋งค ์ ํ",
+ "columnName": "sales_type",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "domestic", "label": "๊ตญ๋ด ํ๋งค" },
+ { "value": "overseas", "label": "ํด์ธ ํ๋งค" }
+ ],
+ "placeholder": "ํ๋งค ์ ํ ์ ํ"
+ },
+ "displayOrder": 2
+ },
+ {
+ "id": "select-price-method",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 680, "y": 35, "z": 2 },
+ "size": { "width": 250, "height": 40 },
+ "overrides": {
+ "label": "๋จ๊ฐ ๋ฐฉ์",
+ "columnName": "price_method",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "standard", "label": "๊ธฐ์ค ๋จ๊ฐ" },
+ { "value": "contract", "label": "๊ณ์ฝ ๋จ๊ฐ" },
+ { "value": "custom", "label": "๊ฐ๋ณ ์
๋ ฅ" }
+ ],
+ "placeholder": "๋จ๊ฐ ๋ฐฉ์"
+ },
+ "displayOrder": 3
+ },
+ {
+ "id": "checkbox-price-edit",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 950, "y": 35, "z": 2 },
+ "size": { "width": 150, "height": 40 },
+ "overrides": {
+ "label": "๋จ๊ฐ ์์ ํ์ฉ",
+ "columnName": "allow_price_edit",
+ "mode": "check",
+ "source": "static",
+ "options": [{ "value": "Y", "label": "ํ์ฉ" }]
+ },
+ "displayOrder": 4
+ },
+
+ {
+ "id": "section-customer-info",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 110, "z": 1 },
+ "size": { "width": 1360, "height": 120 },
+ "overrides": {
+ "componentConfig": {
+ "title": "๊ฑฐ๋์ฒ ์ ๋ณด",
+ "showHeader": true,
+ "padding": "md",
+ "borderStyle": "solid"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 5
+ },
+ {
+ "id": "select-customer",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 40, "y": 155, "z": 3 },
+ "size": { "width": 320, "height": 40 },
+ "overrides": {
+ "label": "๊ฑฐ๋์ฒ *",
+ "columnName": "partner_id",
+ "mode": "dropdown",
+ "source": "entity",
+ "entityTable": "customer_mng",
+ "entityValueColumn": "customer_code",
+ "entityLabelColumn": "customer_name",
+ "searchable": true,
+ "placeholder": "๊ฑฐ๋์ฒ๋ช
์
๋ ฅํ์ฌ ๊ฒ์",
+ "required": true,
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 6
+ },
+ {
+ "id": "input-manager",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 380, "y": 155, "z": 3 },
+ "size": { "width": 240, "height": 40 },
+ "overrides": {
+ "label": "๋ด๋น์",
+ "columnName": "manager_name",
+ "placeholder": "๋ด๋น์",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 7
+ },
+ {
+ "id": "input-delivery-partner",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 640, "y": 155, "z": 3 },
+ "size": { "width": 240, "height": 40 },
+ "overrides": {
+ "label": "๋ฉํ์ฒ",
+ "columnName": "delivery_partner_id",
+ "placeholder": "๋ฉํ์ฒ",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 8
+ },
+ {
+ "id": "input-delivery-address",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 900, "y": 155, "z": 3 },
+ "size": { "width": 460, "height": 40 },
+ "overrides": {
+ "label": "๋ฉํ์ฅ์",
+ "columnName": "delivery_address",
+ "placeholder": "๋ฉํ์ฅ์",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 9
+ },
+
+ {
+ "id": "section-item-first",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 110, "z": 1 },
+ "size": { "width": 1360, "height": 200 },
+ "overrides": {
+ "componentConfig": {
+ "title": "ํ๋ชฉ ๋ฐ ๊ฑฐ๋์ฒ๋ณ ์์ฃผ",
+ "showHeader": true,
+ "padding": "md",
+ "borderStyle": "solid"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "item_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 10
+ },
+
+ {
+ "id": "section-items",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 240, "z": 1 },
+ "size": { "width": 1360, "height": 280 },
+ "overrides": {
+ "componentConfig": {
+ "title": "์ถ๊ฐ๋ ํ๋ชฉ",
+ "showHeader": true,
+ "padding": "md",
+ "borderStyle": "solid"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 11
+ },
+ {
+ "id": "btn-item-search",
+ "url": "@/lib/registry/components/v2-button-primary",
+ "position": { "x": 1140, "y": 245, "z": 5 },
+ "size": { "width": 100, "height": 36 },
+ "overrides": {
+ "label": "ํ๋ชฉ ๊ฒ์",
+ "action": {
+ "type": "openModal",
+ "modalType": "itemSelection"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 12
+ },
+ {
+ "id": "btn-shipping-plan",
+ "url": "@/lib/registry/components/v2-button-primary",
+ "position": { "x": 1250, "y": 245, "z": 5 },
+ "size": { "width": 100, "height": 36 },
+ "overrides": {
+ "label": "์ถํ๊ณํ",
+ "webTypeConfig": {
+ "variant": "destructive"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 13
+ },
+ {
+ "id": "repeater-items",
+ "url": "@/lib/registry/components/v2-repeater",
+ "position": { "x": 40, "y": 290, "z": 3 },
+ "size": { "width": 1320, "height": 200 },
+ "overrides": {
+ "renderMode": "modal",
+ "dataSource": {
+ "tableName": "sales_order_detail",
+ "foreignKey": "order_no",
+ "referenceKey": "order_no"
+ },
+ "columns": [
+ { "field": "part_code", "header": "ํ๋ฒ", "width": 100 },
+ { "field": "part_name", "header": "ํ๋ช
", "width": 150 },
+ { "field": "spec", "header": "๊ท๊ฒฉ", "width": 100 },
+ { "field": "unit", "header": "๋จ์", "width": 80 },
+ { "field": "qty", "header": "์๋", "width": 100, "editable": true },
+ { "field": "unit_price", "header": "๋จ๊ฐ", "width": 100, "editable": true },
+ { "field": "amount", "header": "๊ธ์ก", "width": 100 },
+ { "field": "due_date", "header": "๋ฉ๊ธฐ์ผ", "width": 120, "editable": true }
+ ],
+ "modal": {
+ "sourceTable": "item_info",
+ "sourceColumns": ["part_code", "part_name", "spec", "material", "unit_price"],
+ "filterCondition": {}
+ },
+ "features": {
+ "showAddButton": false,
+ "showDeleteButton": true,
+ "inlineEdit": true
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "input_method",
+ "operator": "=",
+ "value": "customer_first",
+ "action": "show"
+ }
+ },
+ "displayOrder": 14
+ },
+
+ {
+ "id": "section-trade-info",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 530, "z": 1 },
+ "size": { "width": 1360, "height": 150 },
+ "overrides": {
+ "componentConfig": {
+ "title": "๋ฌด์ญ ์ ๋ณด",
+ "showHeader": true,
+ "padding": "md",
+ "borderStyle": "solid"
+ },
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 15
+ },
+ {
+ "id": "select-incoterms",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 40, "y": 575, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "์ธ์ฝํ
์ฆ",
+ "columnName": "incoterms",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "FOB", "label": "FOB" },
+ { "value": "CIF", "label": "CIF" },
+ { "value": "EXW", "label": "EXW" },
+ { "value": "DDP", "label": "DDP" }
+ ],
+ "placeholder": "์ ํ",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 16
+ },
+ {
+ "id": "select-payment-term",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 260, "y": 575, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "๊ฒฐ์ ์กฐ๊ฑด",
+ "columnName": "payment_term",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "TT", "label": "T/T" },
+ { "value": "LC", "label": "L/C" },
+ { "value": "DA", "label": "D/A" },
+ { "value": "DP", "label": "D/P" }
+ ],
+ "placeholder": "์ ํ",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 17
+ },
+ {
+ "id": "select-currency",
+ "url": "@/lib/registry/components/v2-select",
+ "position": { "x": 480, "y": 575, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "ํตํ",
+ "columnName": "currency",
+ "mode": "dropdown",
+ "source": "static",
+ "options": [
+ { "value": "KRW", "label": "KRW (์)" },
+ { "value": "USD", "label": "USD (๋ฌ๋ฌ)" },
+ { "value": "EUR", "label": "EUR (์ ๋ก)" },
+ { "value": "JPY", "label": "JPY (์)" },
+ { "value": "CNY", "label": "CNY (์์)" }
+ ],
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 18
+ },
+ {
+ "id": "input-port-loading",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 40, "y": 625, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "์ ์ ํญ",
+ "columnName": "port_of_loading",
+ "placeholder": "์ ์ ํญ",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 19
+ },
+ {
+ "id": "input-port-discharge",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 260, "y": 625, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "๋์ฐฉํญ",
+ "columnName": "port_of_discharge",
+ "placeholder": "๋์ฐฉํญ",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 20
+ },
+ {
+ "id": "input-hs-code",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 480, "y": 625, "z": 3 },
+ "size": { "width": 200, "height": 40 },
+ "overrides": {
+ "label": "HS Code",
+ "columnName": "hs_code",
+ "placeholder": "HS Code",
+ "conditionalConfig": {
+ "enabled": true,
+ "field": "sales_type",
+ "operator": "=",
+ "value": "overseas",
+ "action": "show"
+ }
+ },
+ "displayOrder": 21
+ },
+
+ {
+ "id": "section-additional",
+ "url": "@/lib/registry/components/v2-section-card",
+ "position": { "x": 20, "y": 690, "z": 1 },
+ "size": { "width": 1360, "height": 130 },
+ "overrides": {
+ "componentConfig": {
+ "title": "์ถ๊ฐ ์ ๋ณด",
+ "showHeader": true,
+ "padding": "md",
+ "borderStyle": "solid"
+ }
+ },
+ "displayOrder": 22
+ },
+ {
+ "id": "input-memo",
+ "url": "@/lib/registry/components/v2-input",
+ "position": { "x": 40, "y": 735, "z": 3 },
+ "size": { "width": 1320, "height": 70 },
+ "overrides": {
+ "label": "๋ฉ๋ชจ",
+ "columnName": "memo",
+ "type": "textarea",
+ "placeholder": "๋ฉ๋ชจ๋ฅผ ์
๋ ฅํ์ธ์"
+ },
+ "displayOrder": 23
+ },
+
+ {
+ "id": "btn-cancel",
+ "url": "@/lib/registry/components/v2-button-primary",
+ "position": { "x": 1180, "y": 840, "z": 5 },
+ "size": { "width": 90, "height": 40 },
+ "overrides": {
+ "label": "์ทจ์",
+ "webTypeConfig": {
+ "variant": "outline"
+ },
+ "action": {
+ "type": "close"
+ }
+ },
+ "displayOrder": 24
+ },
+ {
+ "id": "btn-save",
+ "url": "@/lib/registry/components/v2-button-primary",
+ "position": { "x": 1280, "y": 840, "z": 5 },
+ "size": { "width": 90, "height": 40 },
+ "overrides": {
+ "label": "์ ์ฅ",
+ "action": {
+ "type": "save"
+ }
+ },
+ "displayOrder": 25
+ }
+ ],
+ "gridSettings": {
+ "columns": 12,
+ "gap": 16,
+ "padding": 20,
+ "snapToGrid": true,
+ "showGrid": false
+ }
+}
diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx
index 828d1aca..0ce2bae5 100644
--- a/frontend/app/(main)/screens/[screenId]/page.tsx
+++ b/frontend/app/(main)/screens/[screenId]/page.tsx
@@ -238,7 +238,8 @@ function ScreenViewPage() {
compType?.includes("select") ||
compType?.includes("textarea") ||
compType?.includes("v2-input") ||
- compType?.includes("v2-select");
+ compType?.includes("v2-select") ||
+ compType?.includes("v2-media"); // ๐ ๋ฏธ๋์ด ์ปดํฌ๋ํธ ์ถ๊ฐ
const hasColumnName = !!(comp as any).columnName;
return isInputType && hasColumnName;
});
diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx
index 68fa0cb1..dbb1e923 100644
--- a/frontend/components/common/ScreenModal.tsx
+++ b/frontend/components/common/ScreenModal.tsx
@@ -13,6 +13,7 @@ import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
+import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter";
interface ScreenModalState {
isOpen: boolean;
@@ -322,12 +323,28 @@ export const ScreenModal: React.FC = ({ className }) => {
try {
setLoading(true);
- // ํ๋ฉด ์ ๋ณด์ ๋ ์ด์์ ๋ฐ์ดํฐ ๋ก๋ฉ
- const [screenInfo, layoutData] = await Promise.all([
+ // ํ๋ฉด ์ ๋ณด์ ๋ ์ด์์ ๋ฐ์ดํฐ ๋ก๋ฉ (V2 API ์ฌ์ฉ์ผ๋ก ๊ธฐ๋ณธ๊ฐ ๋ณํฉ)
+ const [screenInfo, v2LayoutData] = await Promise.all([
screenApi.getScreen(screenId),
- screenApi.getLayout(screenId),
+ screenApi.getLayoutV2(screenId),
]);
+ // V2 โ Legacy ๋ณํ (๊ธฐ๋ณธ๊ฐ ๋ณํฉ ํฌํจ)
+ let layoutData: any = null;
+ if (v2LayoutData && isValidV2Layout(v2LayoutData)) {
+ layoutData = convertV2ToLegacy(v2LayoutData);
+ if (layoutData) {
+ // screenResolution์ V2 ๋ ์ด์์์์ ์ง์ ๊ฐ์ ธ์ค๊ธฐ
+ layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution;
+ }
+ }
+
+ // V2 ๋ ์ด์์์ด ์์ผ๋ฉด ๊ธฐ์กด API๋ก fallback
+ if (!layoutData) {
+ console.log("๐ฆ V2 ๋ ์ด์์ ์์, ๊ธฐ์กด API๋ก fallback");
+ layoutData = await screenApi.getLayout(screenId);
+ }
+
// ๐ URL ํ๋ผ๋ฏธํฐ ํ์ธ (์์ ๋ชจ๋)
if (typeof window !== "undefined") {
const urlParams = new URLSearchParams(window.location.search);
@@ -604,23 +621,135 @@ export const ScreenModal: React.FC = ({ className }) => {
transformOrigin: "center center",
}}
>
- {screenData.components.map((component) => {
+ {(() => {
+ // ๐ ๋์ y ์ขํ ์กฐ์ ์ ์ํด ๋จผ์ ์จ๊ฒจ์ง๋ ์ปดํฌ๋ํธ๋ค ํ์
+ const isComponentHidden = (comp: any) => {
+ const cc = comp.componentConfig?.conditionalConfig || comp.conditionalConfig;
+ if (!cc?.enabled || !formData) return false;
+
+ const { field, operator, value, action } = cc;
+ const fieldValue = formData[field];
+
+ let conditionMet = false;
+ switch (operator) {
+ case "=":
+ case "==":
+ case "===":
+ conditionMet = fieldValue === value;
+ break;
+ case "!=":
+ case "!==":
+ conditionMet = fieldValue !== value;
+ break;
+ default:
+ conditionMet = fieldValue === value;
+ }
+
+ return (action === "show" && !conditionMet) || (action === "hide" && conditionMet);
+ };
+
+ // ํ์๋๋ ์ปดํฌ๋ํธ๋ค์ y ๋ฒ์ ์์ง
+ const visibleRanges: { y: number; bottom: number }[] = [];
+ screenData.components.forEach((comp: any) => {
+ if (!isComponentHidden(comp)) {
+ const y = parseFloat(comp.position?.y?.toString() || "0");
+ const height = parseFloat(comp.size?.height?.toString() || "0");
+ visibleRanges.push({ y, bottom: y + height });
+ }
+ });
+
+ // ์จ๊ฒจ์ง๋ ์ปดํฌ๋ํธ์ "์ค์ ๋น ๊ณต๊ฐ" ๊ณ์ฐ (ํ์๋๋ ์ปดํฌ๋ํธ์ ๊ฒน์น์ง ์๋ ์์ญ)
+ const getActualGap = (hiddenY: number, hiddenBottom: number): number => {
+ // ์จ๊ฒจ์ง๋ ์์ญ ์ค ํ์๋๋ ์ปดํฌ๋ํธ์ ๊ฒน์น๋ ๋ถ๋ถ์ ์ ์ธ
+ let gapStart = hiddenY;
+ let gapEnd = hiddenBottom;
+
+ for (const visible of visibleRanges) {
+ // ๊ฒน์น๋ ์์ญ ํ์ธ
+ if (visible.y < gapEnd && visible.bottom > gapStart) {
+ // ๊ฒน์น๋ ๋ถ๋ถ์ ์ ์ธ
+ if (visible.y <= gapStart && visible.bottom >= gapEnd) {
+ // ์์ ํ ๋ฎํ - ๋น ๊ณต๊ฐ ์์
+ return 0;
+ } else if (visible.y <= gapStart) {
+ // ์์ชฝ์ด ๋ฎํ
+ gapStart = visible.bottom;
+ } else if (visible.bottom >= gapEnd) {
+ // ์๋์ชฝ์ด ๋ฎํ
+ gapEnd = visible.y;
+ }
+ }
+ }
+
+ return Math.max(0, gapEnd - gapStart);
+ };
+
+ // ์จ๊ฒจ์ง๋ ์ปดํฌ๋ํธ๋ค์ ์ค์ ๋น ๊ณต๊ฐ ์์ง
+ const hiddenGaps: { bottom: number; gap: number }[] = [];
+ screenData.components.forEach((comp: any) => {
+ if (isComponentHidden(comp)) {
+ const y = parseFloat(comp.position?.y?.toString() || "0");
+ const height = parseFloat(comp.size?.height?.toString() || "0");
+ const bottom = y + height;
+ const gap = getActualGap(y, bottom);
+ if (gap > 0) {
+ hiddenGaps.push({ bottom, gap });
+ }
+ }
+ });
+
+ // bottom ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ ๋ฐ ์ค๋ณต ์ ๊ฑฐ (๊ฐ์ bottom์ ๊ฐ์ฅ ํฐ gap๋ง ์ ์ง)
+ const mergedGaps = new Map();
+ hiddenGaps.forEach(({ bottom, gap }) => {
+ const existing = mergedGaps.get(bottom) || 0;
+ mergedGaps.set(bottom, Math.max(existing, gap));
+ });
+
+ const sortedGaps = Array.from(mergedGaps.entries())
+ .map(([bottom, gap]) => ({ bottom, gap }))
+ .sort((a, b) => a.bottom - b.bottom);
+
+ console.log('๐ [Y์กฐ์ ] visibleRanges:', visibleRanges.filter(r => r.bottom - r.y > 50).map(r => `${r.y}~${r.bottom}`));
+ console.log('๐ [Y์กฐ์ ] hiddenGaps:', sortedGaps);
+
+ // ๊ฐ ์ปดํฌ๋ํธ์ y ์กฐ์ ๊ฐ ๊ณ์ฐ ํจ์
+ const getYOffset = (compY: number, compId?: string) => {
+ let offset = 0;
+ for (const { bottom, gap } of sortedGaps) {
+ // ์ปดํฌ๋ํธ๊ฐ ์จ๊ฒจ์ง ์์ญ ์๋์ ์์ผ๋ฉด ๊ทธ ๋น ๊ณต๊ฐ๋งํผ ์๋ก ์ด๋
+ if (compY > bottom) {
+ offset += gap;
+ }
+ }
+ if (offset > 0 && compId) {
+ console.log(`๐ [Y์กฐ์ ] ${compId}: y=${compY} โ ${compY - offset} (offset=${offset})`);
+ }
+ return offset;
+ };
+
+ return screenData.components.map((component: any) => {
+ // ์จ๊ฒจ์ง๋ ์ปดํฌ๋ํธ๋ ๋ ๋๋ง ์ํจ
+ if (isComponentHidden(component)) {
+ return null;
+ }
+
// ํ๋ฉด ๊ด๋ฆฌ ํด์๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ offset ์กฐ์ ๋ถํ์
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
+
+ // ๐ ๋์ y ์ขํ ์กฐ์ (์จ๊ฒจ์ง ์ปดํฌ๋ํธ ๋์ด๋งํผ ์๋ก ์ด๋)
+ const compY = parseFloat(component.position?.y?.toString() || "0");
+ const yAdjustment = getYOffset(compY, component.id);
// offset์ด 0์ด๋ฉด ์๋ณธ ์์น ์ฌ์ฉ (ํ๋ฉด ๊ด๋ฆฌ ํด์๋ ์ฌ์ฉ ์)
- const adjustedComponent =
- offsetX === 0 && offsetY === 0
- ? component
- : {
- ...component,
- position: {
- ...component.position,
- x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
- y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
- },
- };
+ const adjustedComponent = {
+ ...component,
+ position: {
+ ...component.position,
+ x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
+ y: compY - offsetY - yAdjustment, // ๐ ๋์ ์กฐ์ ์ ์ฉ
+ },
+ };
return (
= ({ className }) => {
companyCode={user?.companyCode}
/>
);
- })}
+ });
+ })()}
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
index 735fb53c..8dc5da89 100644
--- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
+++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx
@@ -335,13 +335,42 @@ export const InteractiveScreenViewerDynamic: React.FC