628 lines
18 KiB
Markdown
628 lines
18 KiB
Markdown
# 컴포넌트 관리 시스템 최종 설계
|
|
|
|
---
|
|
|
|
## 🔒 확정 사항 (변경 금지)
|
|
|
|
| 항목 | 확정 내용 | 비고 |
|
|
|-----|---------|-----|
|
|
| **slot 저장 위치** | `custom_config.slot` | DB 컬럼 아님 |
|
|
| **component_url** | 모든 컴포넌트 **필수** | NULL 허용 안 함 |
|
|
| **멀티테넌시** | 모든 쿼리에 `company_code` 필터 필수 | action 실행/참조 조회 포함 |
|
|
|
|
⚠️ **위 3가지는 개발 중 절대 변경하지 말 것**
|
|
|
|
---
|
|
|
|
## 1. 현재 문제점 (복사본 문제)
|
|
|
|
### 문제 상황
|
|
- 컴포넌트 코드 수정 시 기존 화면에 반영 안 됨
|
|
- JSON에 모든 설정이 저장되어 있어서 코드 변경이 무시됨
|
|
- JSON 구조가 복잡해서 디버깅 어려움
|
|
- 어떤 파일을 수정해야 하는지 찾기 어려움
|
|
|
|
### 핵심 원인: DB에 "복사본"이 생김
|
|
- 화면 저장할 때 컴포넌트 설정 **전체**를 JSON으로 저장
|
|
- 그 순간 DB 안에 **"컴포넌트 복사본"**이 생김
|
|
- 나중에 코드(원본)를 고쳐도, 화면은 DB 복사본을 읽어서 **원본 수정이 안 먹음**
|
|
|
|
### 현재 구조 (문제되는 방식)
|
|
```json
|
|
{
|
|
"componentType": "button-primary",
|
|
"componentConfig": {
|
|
"text": "저장",
|
|
"variant": "primary",
|
|
"backgroundColor": "#111", // 기본값인데도 저장됨 → 복사본
|
|
"textColor": "#fff", // 기본값인데도 저장됨 → 복사본
|
|
...전체 설정...
|
|
}
|
|
}
|
|
```
|
|
- 4,414개 레코드
|
|
- 모든 설정이 JSON에 통째로 저장 (= 복사본)
|
|
|
|
---
|
|
|
|
## 2. 해결 방안 비교
|
|
|
|
### 방안 A: 1개 레코드 (화면당 1개, components 배열)
|
|
|
|
```json
|
|
{
|
|
"components": [
|
|
{ "type": "split-panel-layout", "url": "...", "config": {...} },
|
|
{ "type": "table-list", "url": "...", "config": {...} },
|
|
{ "type": "button", "config": {...} }
|
|
]
|
|
}
|
|
```
|
|
|
|
| 장점 | 단점 |
|
|
|-----|-----|
|
|
| 레코드 수 감소 (4414 → ~200) | JSON 크기 커짐 (10~50KB/화면) |
|
|
| 화면 단위 관리 | 버튼 하나 수정해도 전체 JSON 업데이트 |
|
|
| | 동시 편집 시 충돌 위험 |
|
|
| | 특정 컴포넌트 쿼리 어려움 (JSON 내부 검색) |
|
|
|
|
**결론: 비효율적**
|
|
|
|
---
|
|
|
|
### 방안 B: 다중 레코드 + URL (선택)
|
|
|
|
```sql
|
|
screen_layouts_v3
|
|
├── component_id
|
|
├── component_url = "@/lib/registry/components/split-panel-layout"
|
|
├── custom_config = { 커스텀 설정만 }
|
|
```
|
|
|
|
| 장점 | 단점 |
|
|
|-----|-----|
|
|
| 개별 컴포넌트 수정 가능 | 레코드 수 많음 (기존과 동일) |
|
|
| 부분 업데이트 | |
|
|
| URL로 바로 파일 위치 확인 | |
|
|
| 인덱스 검색 가능 | |
|
|
| 동시 편집 안전 | |
|
|
|
|
**결론: 효율적**
|
|
|
|
---
|
|
|
|
## 3. URL + overrides 방식의 핵심
|
|
|
|
### 핵심 개념
|
|
- **URL = 참조 방식**: "이 컴포넌트의 코드는 어디 파일이냐?"
|
|
- **overrides = 차이값**: "회사/화면별로 다른 값만"
|
|
- **DB는 복사본이 아닌 참조 + 메모**
|
|
|
|
### 저장 구조 비교
|
|
|
|
**AS-IS (복사본 = 문제):**
|
|
```json
|
|
{
|
|
"componentType": "button-primary",
|
|
"componentConfig": {
|
|
"text": "저장",
|
|
"variant": "primary", // 기본값
|
|
"backgroundColor": "#111", // 기본값
|
|
"textColor": "#fff", // 기본값
|
|
...전체...
|
|
}
|
|
}
|
|
```
|
|
|
|
**TO-BE (참조 + 차이값 = 해결):**
|
|
```json
|
|
{
|
|
"component_url": "@/lib/registry/components/button-primary",
|
|
"overrides": {
|
|
"text": "저장",
|
|
"action": { "type": "save" }
|
|
}
|
|
}
|
|
```
|
|
|
|
### 왜 코드 수정이 전체 반영되나?
|
|
|
|
1. 코드(원본)에 defaults 정의: `{ variant: "primary", backgroundColor: "#111" }`
|
|
2. DB에는 overrides만: `{ text: "저장" }`
|
|
3. 렌더링 시 merge: `{ ...defaults, ...overrides }`
|
|
4. 코드의 defaults 수정 → 모든 화면 즉시 반영
|
|
|
|
### 디버깅 효율성
|
|
|
|
**URL 없을 때:**
|
|
```
|
|
1. component_type = "split-panel-layout" 확인
|
|
2. 어디에 파일이 있지? 매핑 찾기
|
|
3. 규칙 추론 또는 설정 파일 확인
|
|
4. 해당 파일로 이동
|
|
```
|
|
→ 3~4단계
|
|
|
|
**URL 있을 때:**
|
|
```
|
|
1. component_url = "@/lib/registry/components/split-panel-layout" 확인
|
|
2. 해당 파일로 바로 이동
|
|
```
|
|
→ 1단계
|
|
|
|
---
|
|
|
|
## 4. 최종 설계
|
|
|
|
### DB 구조
|
|
|
|
```sql
|
|
screen_layouts_v3 (
|
|
layout_id SERIAL PRIMARY KEY,
|
|
screen_id INTEGER,
|
|
component_id VARCHAR(100) UNIQUE NOT NULL,
|
|
component_url VARCHAR(200) NOT NULL, -- 모든 컴포넌트 URL 참조 (권장)
|
|
custom_config JSONB NOT NULL DEFAULT '{}', -- slot, dataSource 등 포함
|
|
parent_id VARCHAR(100), -- 부모 컴포넌트 ID (컨테이너-자식 관계)
|
|
position_x INTEGER DEFAULT 0,
|
|
position_y INTEGER DEFAULT 0,
|
|
width INTEGER DEFAULT 100,
|
|
height INTEGER DEFAULT 100,
|
|
display_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
**주요 컬럼:**
|
|
- `component_url`: 컴포넌트 코드 경로 (필수)
|
|
- `custom_config`: 회사/화면별 차이값 (slot 포함)
|
|
- `parent_id`: 부모 컴포넌트 ID (계층 구조)
|
|
|
|
### component_url 정책
|
|
|
|
**원칙: 모든 컴포넌트는 URL 참조가 가능해야 함**
|
|
|
|
| 구분 | 예시 | component_url | 설명 |
|
|
|-----|-----|--------------|------|
|
|
| 메인 | split-panel, tabs, table-list | `@/lib/.../split-panel-layout` | 코드 수정 시 전체 반영 |
|
|
| 공용 | button, text-input | `@/lib/.../button-primary` | 동일하게 URL 참조 |
|
|
|
|
**참고**:
|
|
- 공용 컴포넌트도 URL로 참조하면 코드 수정 시 전체 반영 가능
|
|
- `NULL` 허용은 마이그레이션 단순화를 위한 선택적 옵션 (권장하지 않음)
|
|
|
|
### 데이터 저장/로드
|
|
|
|
**컴포넌트 파일에 defaults 정의:**
|
|
```typescript
|
|
// @/lib/registry/components/split-panel-layout/index.tsx
|
|
export const defaultConfig = {
|
|
splitRatio: 30,
|
|
resizable: true,
|
|
minSize: 100,
|
|
};
|
|
```
|
|
|
|
**저장 시 (diff만):**
|
|
```json
|
|
// DB에 저장되는 custom_config
|
|
{
|
|
"splitRatio": 50,
|
|
"tableName": "user_info"
|
|
}
|
|
// resizable, minSize는 기본값과 같으므로 저장 안 함
|
|
```
|
|
|
|
**로드 시 (merge):**
|
|
```typescript
|
|
const fullConfig = { ...defaultConfig, ...customConfig };
|
|
// 결과: { splitRatio: 50, resizable: true, minSize: 100, tableName: "user_info" }
|
|
```
|
|
|
|
### Zod 스키마
|
|
|
|
```typescript
|
|
// 컴포넌트별 스키마 (defaults 포함)
|
|
const splitPanelSchema = z.object({
|
|
splitRatio: z.number().default(30),
|
|
resizable: z.boolean().default(true),
|
|
minSize: z.number().default(100),
|
|
tableName: z.string().optional(),
|
|
columns: z.array(z.string()).optional(),
|
|
});
|
|
|
|
// 저장 시: schema.parse(config)로 검증
|
|
// 로드 시: schema.parse(customConfig)로 defaults 적용
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 장점 요약
|
|
|
|
1. **코드 수정 → 전체 반영**
|
|
- 컴포넌트 파일 수정하면 해당 URL 사용하는 모든 화면에 적용
|
|
|
|
2. **JSON 크기 감소**
|
|
- 기본값과 다른 것만 저장
|
|
- 디버깅 시 "뭐가 커스텀인지" 바로 파악
|
|
|
|
3. **새 기능 추가 시 자동 적용**
|
|
- 코드에 새 필드 + default 추가
|
|
- 기존 데이터는 그대로, 로드 시 default 적용
|
|
|
|
4. **디버깅 쉬움**
|
|
- URL 보고 바로 파일 위치 확인
|
|
- 매핑 파일 불필요
|
|
|
|
5. **유지보수 용이**
|
|
- 컴포넌트별로 스키마 관리
|
|
- Zod로 타입 안전성 확보
|
|
|
|
---
|
|
|
|
## 6. 회사별 설정 & 비즈니스 로직 처리
|
|
|
|
### 회사별 UI 차이 (색깔 등)
|
|
|
|
```json
|
|
// A회사
|
|
{ "overrides": { "colorVariant": "blue" } }
|
|
|
|
// B회사
|
|
{ "overrides": { "colorVariant": "red" } }
|
|
```
|
|
|
|
- Zod로 허용 값 제한: `z.enum(["blue", "red", "primary"])`
|
|
- 임의의 hex 허용할지, 토큰만 허용할지 스키마로 강제
|
|
|
|
### 비즈니스 로직 연결 (제어관리 등)
|
|
|
|
**버튼에 함수/코드 직접 붙이면 안 됨** → 다시 복사본 문제 발생
|
|
|
|
**해결: 액션 정의(데이터)만 저장, 실행은 공통 엔진**
|
|
|
|
```json
|
|
{
|
|
"component_url": "@/lib/registry/components/button-primary",
|
|
"overrides": {
|
|
"text": "제어실행",
|
|
"action": {
|
|
"type": "CONTROL_EXECUTE",
|
|
"ruleId": "RULE_001",
|
|
"params": { "targetTable": "user_info" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**실행 흐름:**
|
|
1. 버튼 클릭
|
|
2. 공통 ActionRunner가 `action.type` 확인
|
|
3. `CONTROL_EXECUTE` → 제어관리 로직 실행
|
|
4. `ruleId`, `params`로 실제 동작
|
|
|
|
**장점:**
|
|
- 액션 시스템 버그 수정 → 전 회사 버튼 같이 개선
|
|
- 회사별로는 `ruleId`/`params`만 다르게 저장
|
|
- Zod로 `action` 타입/필수필드 검증 가능
|
|
|
|
---
|
|
|
|
## 7. 구현 순서
|
|
|
|
1. **DB 스키마 변경**
|
|
- `screen_layouts_v3` 테이블 생성
|
|
- `component_url`, `custom_config` 컬럼
|
|
|
|
2. **컴포넌트별 defaults 정의**
|
|
- 각 컴포넌트 파일에 `defaultConfig` export
|
|
|
|
3. **저장 로직**
|
|
- 저장 시 defaults와 비교하여 diff만 저장
|
|
|
|
4. **로드 로직**
|
|
- 로드 시 defaults + customConfig merge
|
|
|
|
5. **마이그레이션**
|
|
- 기존 데이터에서 component_url 추출
|
|
- properties.componentConfig → custom_config 변환
|
|
- (기존 데이터는 일단 전체 저장, 추후 diff로 변환 가능)
|
|
|
|
6. **프론트엔드 수정**
|
|
- 컴포넌트 로딩 시 URL 기반으로 동적 import
|
|
- config merge 로직 적용
|
|
|
|
---
|
|
|
|
## 8. 레코드 개수 원칙
|
|
|
|
### 핵심 원칙
|
|
**컴포넌트 인스턴스 1개 = 레코드 1개**
|
|
|
|
### 현재 문제 (split-panel에 몰아넣기)
|
|
|
|
```
|
|
split-panel-layout 1개 레코드에:
|
|
├── leftPanel 설정 (table-list 역할) → 박제
|
|
├── rightPanel 설정 (card 역할) → 박제
|
|
├── relation, binding 등등 → 박제
|
|
└── 전부 JSON으로 들어감
|
|
```
|
|
|
|
**문제점:**
|
|
- table-list 코드 수정해도 반영 안 됨 (JSON에 박제)
|
|
- 컨테이너 스키마가 계속 비대해짐
|
|
- URL 참조 체계와 충돌
|
|
|
|
### 올바른 구조 (레코드 분리)
|
|
|
|
```
|
|
레코드 1: split-panel-layout (컨테이너)
|
|
└── component_url: @/lib/.../split-panel-layout ← URL 필수 (코드 참조)
|
|
└── parent_id: null
|
|
└── custom_config: { splitRatio: 30 }
|
|
|
|
레코드 2: table-list (왼쪽)
|
|
└── component_url: @/lib/.../table-list
|
|
└── parent_id: "comp_split_001"
|
|
└── custom_config: {
|
|
slot: "left", ← slot은 custom_config 안에
|
|
dataSource: {...},
|
|
selection: { publishKey: "selectedId" }
|
|
}
|
|
|
|
레코드 3: card-display (오른쪽)
|
|
└── component_url: @/lib/.../card-display
|
|
└── parent_id: "comp_split_001"
|
|
└── custom_config: {
|
|
slot: "right", ← slot은 custom_config 안에
|
|
dataSource: { where: { id: { fromContext: "selectedId" } } }
|
|
}
|
|
```
|
|
|
|
**주의**:
|
|
- 컨테이너도 컴포넌트이므로 `component_url` 필수
|
|
- `slot`은 DB 컬럼이 아닌 `custom_config` 안에 저장
|
|
|
|
### 부모-자식 연결 방식
|
|
|
|
| 컬럼 | 위치 | 설명 |
|
|
|-----|-----|-----|
|
|
| `parent_id` | DB 컬럼 | 부모 컴포넌트 ID |
|
|
| `slot` | custom_config 내부 | 슬롯명 (left/right/header/footer) |
|
|
|
|
→ `parent_id`는 DB 컬럼, `slot`은 JSON 안에 → **일관성 유지**
|
|
|
|
**장점:**
|
|
- table-list 코드 수정 → 전체 반영 ✅
|
|
- card-display 코드 수정 → 전체 반영 ✅
|
|
- 컨테이너는 레이아웃만 담당 (설정 폭발 방지)
|
|
- 재사용/확장 용이
|
|
|
|
### 연결 방식
|
|
|
|
**연결 정보는 각 컴포넌트의 custom_config에 저장**, 실행은 공통 컨텍스트 매니저가 처리:
|
|
|
|
```json
|
|
// table-list의 custom_config
|
|
{ "selection": { "publishKey": "selectedId" } }
|
|
|
|
// card-display의 custom_config
|
|
{ "dataSource": { "where": { "id": { "fromContext": "selectedId" } } } }
|
|
```
|
|
|
|
- **저장**: 각 컴포넌트 custom_config에 바인딩 정보
|
|
- **실행**: 공통 ScreenContext가 publish/subscribe 처리
|
|
|
|
---
|
|
|
|
## 9. 마이그레이션 전략
|
|
|
|
### 2단계 전략 (반자동 + 검증)
|
|
|
|
**1단계: 자동 변환**
|
|
```
|
|
split-panel-layout 레코드에서:
|
|
├── properties.componentConfig.leftPanel → 왼쪽 컴포넌트 레코드 생성
|
|
├── properties.componentConfig.rightPanel → 오른쪽 컴포넌트 레코드 생성
|
|
├── properties.componentConfig.relation → 바인딩 설정으로 변환
|
|
└── 원본 → 컨테이너 레코드 (레이아웃만)
|
|
```
|
|
|
|
**2단계: 검증/수동 보정**
|
|
- 특이 케이스 (커스텀 필드, 중첩 구조) 확인
|
|
- 사람이 검증 후 보정
|
|
|
|
**이유**: "완전 자동"은 예외가 많고, "완전 수동"은 시간이 너무 듦
|
|
|
|
---
|
|
|
|
## 10. publish/subscribe 바인딩 설계
|
|
|
|
### 스코프
|
|
**화면(screen) 단위**가 기본
|
|
|
|
**이유**: 같은 key(selectedId)가 다른 화면에서 섞이면 사고
|
|
|
|
### 구현 방식 (React)
|
|
|
|
**권장: ScreenContext 기반**
|
|
|
|
```typescript
|
|
// ScreenContext + 내부 store
|
|
const ScreenContext = createContext<Map<string, any>>();
|
|
|
|
// 사용
|
|
const { publish, subscribe } = useScreenContext();
|
|
|
|
// table-list에서
|
|
publish("selectedId", row.id);
|
|
|
|
// card-display에서
|
|
const selectedId = subscribe("selectedId");
|
|
```
|
|
|
|
**장점:**
|
|
- 화면 언마운트 시 상태 자동 폐기
|
|
- 디버깅 쉬움 ("현재 화면 컨텍스트 값" 표시 가능)
|
|
|
|
---
|
|
|
|
## 11. ActionRunner 설계
|
|
|
|
### 원칙
|
|
- 버튼에는 **"실행할 일의 데이터"만** 저장
|
|
- 실행은 **공통 ActionRunner**가 처리
|
|
|
|
### 구조
|
|
|
|
```typescript
|
|
// action.type은 enum으로 고정 (Zod 검증)
|
|
const actionTypeSchema = z.enum([
|
|
"OPEN_SCREEN",
|
|
"CRUD_SAVE",
|
|
"CRUD_DELETE",
|
|
"CONTROL_EXECUTE",
|
|
"FLOW_EXECUTE",
|
|
"API_CALL",
|
|
]);
|
|
|
|
// payload는 타입별 스키마로 분기
|
|
const actionSchema = z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("OPEN_SCREEN"), screenId: z.number(), filters: z.record(z.any()).optional() }),
|
|
z.object({ type: z.literal("CRUD_SAVE"), tableName: z.string() }),
|
|
z.object({ type: z.literal("CONTROL_EXECUTE"), ruleId: z.string(), params: z.record(z.any()).optional() }),
|
|
z.object({ type: z.literal("FLOW_EXECUTE"), flowId: z.number() }),
|
|
// ...
|
|
]);
|
|
```
|
|
|
|
### 초기 action.type 목록
|
|
|
|
| type | 설명 | payload |
|
|
|-----|-----|---------|
|
|
| `OPEN_SCREEN` | 화면 이동 | `{ screenId, filters? }` |
|
|
| `CRUD_SAVE` | 저장 | `{ tableName }` |
|
|
| `CRUD_DELETE` | 삭제 | `{ tableName }` |
|
|
| `CONTROL_EXECUTE` | 제어관리 실행 | `{ ruleId, params? }` |
|
|
| `FLOW_EXECUTE` | 플로우 실행 | `{ flowId }` |
|
|
| `API_CALL` | 외부/내부 API 호출 | `{ endpoint, method, body? }` (보안/허용 목록 필수) |
|
|
|
|
---
|
|
|
|
## 12. 구현 우선순위
|
|
|
|
### 순서 (권장)
|
|
|
|
| 순서 | 단계 | 설명 |
|
|
|-----|-----|-----|
|
|
| 1 | **데이터 모델/스키마 확정** | component_url 정책, parent_id + slot 위치 |
|
|
| 2 | **프론트 렌더링 파이프라인** | 로드 → merge → Zod → 렌더링 |
|
|
| 3 | **바인딩 컨텍스트 + ActionRunner** | publish/subscribe + 공통 실행 엔진 |
|
|
| 4 | **화면 디자이너 저장 포맷 변경** | "박제 JSON" 방지 (저장 시 차단) |
|
|
| 5 | **마이그레이션 스크립트** | 기존 데이터 → 새 구조 변환 |
|
|
|
|
### 핵심
|
|
- 렌더링이 먼저 되어야 검증 가능
|
|
- 저장 로직을 마지막에 수정해야 "새 박제" 방지
|
|
|
|
---
|
|
|
|
## 13. 주의사항
|
|
|
|
- 기존 화면은 **동일하게 렌더링**되어야 함
|
|
- 마이그레이션 시 데이터 손실 없어야 함
|
|
- 새 테이블(v1)에서 테스트 후 전환
|
|
- **company_code 필터 필수** (멀티테넌시)
|
|
- action.type `API_CALL`은 **허용 목록 필수** (보안)
|
|
|
|
---
|
|
|
|
## 14. 구현 진행 상황
|
|
|
|
### 완료된 작업
|
|
|
|
| 단계 | 내용 | 상태 |
|
|
|-----|-----|-----|
|
|
| 1-1 | `screen_layouts_v1` 테이블 생성 | ✅ 완료 |
|
|
| 1-2 | 복합 인덱스 생성 (company_code, screen_id) | ✅ 완료 |
|
|
| 1-3 | 기존 데이터 마이그레이션 (4,414개) | ✅ 완료 |
|
|
| 1-4 | **split-panel 자식 분리** (leftPanel/rightPanel → 별도 레코드) | ✅ 완료 |
|
|
| 1-5 | **repeat-container 자식 분리** (children → 별도 레코드) | ✅ 완료 |
|
|
| 2-1 | 백엔드 `getLayoutV1` API 구현 | ✅ 완료 |
|
|
| 2-2 | 프론트엔드 `getLayoutV1` API 추가 | ✅ 완료 |
|
|
| 2-3 | Zod 스키마 및 merge 함수 | ✅ 완료 |
|
|
|
|
### 마이그레이션 결과
|
|
|
|
```
|
|
총 레코드: 4,691개
|
|
├── 루트 컴포넌트: 4,414개
|
|
└── 자식 컴포넌트: 277개 (parent_id 있음)
|
|
|
|
slot 분포:
|
|
├── left: 136개
|
|
├── right: 135개
|
|
└── child_0~3: 6개
|
|
|
|
박제 제거:
|
|
├── split-panel의 leftPanel/rightPanel: 0개 (완료)
|
|
├── repeat-container의 children: 0개 (완료)
|
|
└── tabs 내부 components: 13개 (추후 처리)
|
|
```
|
|
|
|
### 샘플 구조 (screen 1383 - 수주등록)
|
|
|
|
```
|
|
comp_lspd9b9m (split-panel-layout)
|
|
├── comp_lspd9b9m_left (table-list)
|
|
│ ├── slot: "left"
|
|
│ └── tableName: "sales_order_mng"
|
|
└── comp_lspd9b9m_right (table-list)
|
|
├── slot: "right"
|
|
└── tableName: "sales_order_detail"
|
|
```
|
|
|
|
### DB 스키마
|
|
|
|
```sql
|
|
CREATE TABLE screen_layouts_v1 (
|
|
layout_id SERIAL PRIMARY KEY,
|
|
screen_id VARCHAR(50) NOT NULL,
|
|
component_id VARCHAR(100) NOT NULL,
|
|
component_url VARCHAR(200) NOT NULL, -- 🔒 필수
|
|
custom_config JSONB NOT NULL DEFAULT '{}', -- slot 포함
|
|
parent_id VARCHAR(100),
|
|
position_x INTEGER NOT NULL DEFAULT 0,
|
|
position_y INTEGER NOT NULL DEFAULT 0,
|
|
width INTEGER NOT NULL DEFAULT 100,
|
|
height INTEGER NOT NULL DEFAULT 100,
|
|
display_order INTEGER DEFAULT 0,
|
|
company_code VARCHAR(20) NOT NULL, -- 🔒 멀티테넌시
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(company_code, screen_id, component_id)
|
|
);
|
|
|
|
-- 인덱스
|
|
CREATE INDEX idx_v1_company_screen ON screen_layouts_v1(company_code, screen_id);
|
|
CREATE INDEX idx_v1_company_parent ON screen_layouts_v1(company_code, parent_id);
|
|
CREATE INDEX idx_v1_component_url ON screen_layouts_v1(component_url);
|
|
```
|
|
|
|
### API 엔드포인트
|
|
|
|
```
|
|
GET /api/screen-management/screens/:screenId/layout-v1
|
|
```
|
|
|
|
### 남은 작업
|
|
|
|
| 단계 | 내용 | 상태 |
|
|
|-----|-----|-----|
|
|
| 3-1 | 바인딩 컨텍스트 (ScreenContext) 구현 | 🔲 대기 |
|
|
| 3-2 | ActionRunner 공통 엔진 구현 | 🔲 대기 |
|
|
| 4 | 화면 디자이너 저장 포맷 변경 | 🔲 대기 |
|
|
| 5 | 컴포넌트별 defaultConfig 정의 | 🔲 대기 |
|