Phase 1: INVYONE 카드 엔진 토대 정리
- components/builder/* 폐기 (12-grid 미완성 빌더 14개 파일)
- components/template-builder/TemplateBuilder.tsx 신규
(자유배치 + 3뷰 + Zustand 스토어 + 드래그/리사이즈/히스토리/격자)
- admin/builder/page.tsx 진입점 전환 (BuilderLayout → TemplateBuilder)
- 타입 정리: FreePosition / TemplateComponent / ViewConfig / Card /
Dashboard / CardConnection 추가, 레거시(GridPosition/TemplateKind/
DEFAULT_COMPONENT_LAYOUTS/CANVAS_KEYWORDS) @deprecated 표기
- v2-* 마이그레이션 1차:
· 완전: v2-table-list (ResizeObserver), v2-table-search-widget (@container)
· 경량: button/input/select/date/text-display/card-display/aggregation-widget
(withContainerQuery HOC)
- 다크 모드 대응: Tailwind dark: variant 21패턴 71곳 치환
- /test-card-responsive PoC 검증 페이지
세션 후반 버그 픽스 (phase1-log §7):
- test-card-responsive (main) 그룹 밖 이동 (AppLayout 탭 시스템 회피)
- useRegistryPalette default_size {width,height}/{w,h} 포맷 정규화
- dark: variant 중복 체인 정리
검증: (A) 반응형 메커니즘, (B) TemplateBuilder UI 통과
(C) 기존 VEX 화면은 마이그레이션 미완 상태라 Phase 2 이후 개별 진행
스펙: notes/gbpark/2026-04-10-card-engine-final-spec.md
로그: notes/gbpark/2026-04-10-card-engine-phase1-log.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,946 @@
|
||||
# INVYONE 카드 엔진 최종 스펙 (2026-04-10 확정)
|
||||
|
||||
> **이 문서가 진실의 원천이다.** 이전 12-grid / 48-col / 섹션 / business-canvas 분기 모델은 모두 폐기됨. 다른 클로드 세션은 이 문서만 따라가면 된다.
|
||||
|
||||
**작성일**: 2026-04-10
|
||||
**상태**: 구현 착수 가능 (Phase 1 ~ 3)
|
||||
**관련 문서**:
|
||||
- mockup: `notes/gbpark/2026-04-08-invyone-mockup/` (시각 의도, 살아있음)
|
||||
- 컴포넌트 규격: `notes/gbpark/2026-04-08-invyone-component-spec.md` (FieldConfig 부분만 살아있음)
|
||||
- 폐기된 12-grid 노트: `notes/gbpark/2026-04-10-template-model-redesign.md` (참고용 obsolete)
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 줄 요약
|
||||
|
||||
**VEX 자유배치 모델 + 컴포넌트 내부 @container 반응형 + 카드 단위 추상화.**
|
||||
12-grid / 48-col / 섹션 / business-canvas 분기는 모두 우회 흔적이고 폐기됨. 단일 자유배치 카드 모델로 통일.
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 개념 — 4가지 용어
|
||||
|
||||
| 용어 | 의미 | 예시 |
|
||||
|---|---|---|
|
||||
| **Component** | 빌더의 재료. VEX v2-* 컴포넌트 100+개. 자체 사이즈 + 자체 모드 가짐 | v2-table-list, v2-bom-tree, v2-button-primary, v2-search |
|
||||
| **Template** | 한 화면 청사진. 컴포넌트들의 자유배치 묶음. 목록/등록/수정 3뷰 | 수주관리, 인사정보, BOM관리 |
|
||||
| **Card** | Template 인스턴스. 대시보드에 배치된 한 개. 위치/크기 가짐 | 영업대시보드의 "수주관리 카드 #c1" |
|
||||
| **Dashboard** | 카드 컬렉션 = 사이드바 메뉴 항목. 카드 자유 배치 | "영업 현황", "운영 모니터링" |
|
||||
|
||||
### 관계도
|
||||
```
|
||||
Component (v2-*) Template Card Dashboard
|
||||
───────────── ──────── ───── ──────────
|
||||
v2-table-list 수주관리 ───┐ ┌─ 카드#c1 (영업대시보드, left:50,top:50)
|
||||
v2-search-widget ────┐ │ 1:N ├─ 카드#c5 (임원대시보드, left:200,top:80)
|
||||
v2-button-primary ├──→ 목록 뷰 (자유) ──→인스턴스화├─ 카드#c9 (본부장, left:100,top:200)
|
||||
v2-bom-tree │ 등록 뷰 (자유)
|
||||
v2-aggregation ├──→ 수정 뷰 (자유)
|
||||
... (100+개) │
|
||||
└──→ 인사정보 ───→ 여러 인스턴스
|
||||
BOM관리 ───→ 여러 인스턴스
|
||||
...
|
||||
```
|
||||
|
||||
### 두 단계의 자유배치
|
||||
1. **개발자 (L1+)**: 빈 캔버스에 **컴포넌트** 자유배치 → Template 완성
|
||||
2. **사용자 (L0)**: 빈 대시보드에 **Template (=Card)** 자유배치 → 나만의 대시보드
|
||||
|
||||
= **같은 자유배치 UX 를 두 레이어에서 사용**. 캔버스에 놓는 것의 종류만 다름.
|
||||
|
||||
---
|
||||
|
||||
## 2. ★ 반응형 메커니즘 (보장 + 한계 + 백업)
|
||||
|
||||
### 작동 원리
|
||||
캔버스 자유배치 자체로는 반응형이 안 됨. **반응형은 컴포넌트 내부에서 처리**.
|
||||
|
||||
```
|
||||
대시보드 캔버스 (자유배치)
|
||||
│
|
||||
└─ 카드 #1 (수주관리, 800x500)
|
||||
└─ Template = 컴포넌트들의 자유배치
|
||||
│
|
||||
├─ v2-table-list (720x400)
|
||||
│ │
|
||||
│ └─ container-type: inline-size
|
||||
│ └─ @container (min-width: 600px) → 테이블 모드
|
||||
│ └─ @container (max-width: 599px) → 카드 리스트 모드
|
||||
│
|
||||
├─ v2-search-widget (400x60)
|
||||
│ └─ @container (min-width: 350px) → 가로 필터
|
||||
│ └─ @container (max-width: 349px) → 드롭다운 통합
|
||||
│
|
||||
└─ ...
|
||||
```
|
||||
|
||||
### 카드 사이즈 변경 시 동작
|
||||
사용자가 카드를 800x500 → 400x500 으로 드래그 리사이즈하면:
|
||||
1. 카드 폭이 800px → 400px 로 변함
|
||||
2. 카드 안의 v2-table-list 폭도 720 → 360 으로 변함 (% 또는 fit)
|
||||
3. v2-table-list 의 `@container` 가 360px 감지 → 카드 리스트 모드로 자동 전환
|
||||
4. v2-search-widget 도 마찬가지로 좁은 모드로 전환
|
||||
5. **사용자는 아무것도 안 했는데 내부가 알아서 재배치됨**
|
||||
|
||||
### ✅ 잘 작동하는 케이스 (PC 위주 시나리오)
|
||||
- 카드 폭 600 → 400 같은 적당한 변화
|
||||
- 컴포넌트가 자체 모드 로직 가지고 있을 때
|
||||
- 모니터 해상도 차이 (FHD/2K/4K)
|
||||
- 대시보드에 카드 4개 배치로 카드가 작아지는 경우
|
||||
|
||||
### ❌ 안 되는 케이스 (한계)
|
||||
1. **컴포넌트가 멍청할 때** — @container 모드 없는 컴포넌트는 그냥 잘림
|
||||
- **백업 플랜**: 모든 v2-* 컴포넌트에 최소 wide/narrow 2단계 모드 강제 (Phase 1 마이그레이션)
|
||||
2. **카드 폭 600 → 100 같은 극심한 변화** — 컴포넌트 자체 최소 폭 미달
|
||||
- **백업 플랜**: 카드 min-width: 280px 강제. 그 이하는 collapsed (mini) 모드로 자동 전환
|
||||
3. **컴포넌트들이 "재배치" 되어야 할 때** — 가로 → 세로 재배치는 자유배치라 안 됨
|
||||
- **백업 플랜**: 그런 케이스가 필요하면 컨테이너 컴포넌트 (탭/아코디언) 를 씀. 일반 자유배치는 재배치 안 함
|
||||
4. **모바일/태블릿 (카드 폭 200px 이하)** — 자유배치는 모바일에 부적합
|
||||
- **백업 플랜**: PC 위주 시나리오 (사용자 명시). 모바일은 별도 뷰 (Phase 4 이후)
|
||||
|
||||
### 보장 명시
|
||||
> **이 메커니즘으로 PC 위주 시나리오 (FHD~4K, 카드 폭 280~1920px) 에서는 100% 반응형 작동.**
|
||||
> 모든 v2-* 컴포넌트가 @container 기반 wide/narrow 모드를 가져야 한다는 조건 충족 시.
|
||||
> 이 조건은 Phase 1 마이그레이션의 핵심 작업.
|
||||
|
||||
---
|
||||
|
||||
## 3. 데이터 모델 — 최종 타입
|
||||
|
||||
### 3.1 위치 (단일 모델)
|
||||
```typescript
|
||||
/** 자유배치 위치 (px) — 단일 위치 모델 */
|
||||
export interface FreePosition {
|
||||
left: number; // px
|
||||
top: number; // px
|
||||
width: number; // px
|
||||
height: number; // px
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 컴포넌트 (Template 안의)
|
||||
```typescript
|
||||
/** Template 안에 배치된 컴포넌트 한 개 */
|
||||
export interface TemplateComponent {
|
||||
/** 인스턴스 ID */
|
||||
id: string;
|
||||
|
||||
/** 컴포넌트 종류 — ComponentRegistry 의 ID 참조
|
||||
* 예: 'v2-table-list', 'v2-bom-tree', 'v2-button-primary' */
|
||||
componentId: string;
|
||||
|
||||
/** 캔버스 안에서의 위치 (px 자유배치) */
|
||||
position: FreePosition;
|
||||
|
||||
/** 컴포넌트별 설정 — 모드/옵션 (그리드 모드, 버튼 액션, 컬럼 정의 등)
|
||||
* ComponentRegistry 의 default_config 를 인스턴스 별로 오버라이드 */
|
||||
config: Record<string, any>;
|
||||
|
||||
/** 컨테이너 컴포넌트의 자식 (탭, 아코디언 등) */
|
||||
children?: TemplateComponent[];
|
||||
|
||||
/** 데이터 포트 — 컴포넌트 간 통신 */
|
||||
inputs?: DataPort[];
|
||||
outputs?: DataPort[];
|
||||
|
||||
/** 뷰 트리거 — 이 컴포넌트(예: 등록 버튼)가 다른 뷰를 여는지
|
||||
* 자동 생성: 빌더가 등록 버튼을 감지하면 create 뷰 placeholder 생성 */
|
||||
viewTrigger?: {
|
||||
targetView: 'create' | 'edit' | 'detail';
|
||||
action: 'open-modal' | 'navigate';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 뷰 (Template 안의 한 화면)
|
||||
```typescript
|
||||
/** Template 의 한 뷰 (목록/등록/수정) */
|
||||
export interface ViewConfig {
|
||||
/** 이 뷰의 컴포넌트들 (자유배치) */
|
||||
components: TemplateComponent[];
|
||||
|
||||
/** 뷰 표시 방식 */
|
||||
layout?: 'card' | 'modal';
|
||||
|
||||
/** 모달 사이즈 (modal 일 때) */
|
||||
modalSize?: { w: number; h: number };
|
||||
|
||||
/** 뷰 캔버스의 기본 사이즈 (디자인 시) */
|
||||
designSize?: { w: number; h: number };
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Template (한 화면 청사진)
|
||||
```typescript
|
||||
/** 재사용 가능한 화면 청사진 */
|
||||
export interface Template {
|
||||
/** 식별 */
|
||||
templateId: string;
|
||||
name: string; // '수주관리'
|
||||
icon: string; // '📋'
|
||||
badge: string; // 'ERP · 영업'
|
||||
category: string; // 'sales'
|
||||
description?: string;
|
||||
|
||||
/** 카드로 배치될 때 기본 사이즈 */
|
||||
defaultSize: { w: number; h: number };
|
||||
|
||||
/** 데이터 (선택) */
|
||||
primaryTable?: string; // 'ORDER_MASTER'
|
||||
fields?: FieldConfig[]; // 컴포넌트들이 공유
|
||||
|
||||
/** ★ 3뷰 — 각 뷰는 독립 자유배치 캔버스 */
|
||||
views: {
|
||||
list: ViewConfig; // 카드 본체 (필수)
|
||||
create?: ViewConfig; // 등록 모달 (자동 생성 가능)
|
||||
edit?: ViewConfig; // 수정 모달 (자동 생성 가능)
|
||||
};
|
||||
|
||||
/** 카드 간 통신 — 다른 카드와 주고받는 데이터 */
|
||||
inputs?: DataPortDef[];
|
||||
outputs?: DataPortDef[];
|
||||
|
||||
/** 메타 */
|
||||
companyCode: string;
|
||||
version: number;
|
||||
status: 'draft' | 'published';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Card (대시보드 인스턴스)
|
||||
```typescript
|
||||
/** 대시보드에 배치된 카드 한 개 */
|
||||
export interface Card {
|
||||
/** 인스턴스 ID */
|
||||
id: string;
|
||||
|
||||
/** Template 참조 */
|
||||
templateId: string;
|
||||
|
||||
/** 대시보드 안에서의 위치 (px 자유배치) */
|
||||
position: FreePosition;
|
||||
|
||||
/** 접힘 (mini 모드) */
|
||||
collapsed: boolean;
|
||||
|
||||
/** 인스턴스별 설정 오버라이드 */
|
||||
configOverride?: Record<string, any>;
|
||||
|
||||
/** 인스턴스별 DataPort 연결 */
|
||||
inputs?: DataPort[];
|
||||
outputs?: DataPort[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Dashboard
|
||||
```typescript
|
||||
/** 사이드바 메뉴 항목 = 카드 컬렉션 */
|
||||
export interface Dashboard {
|
||||
/** 식별 */
|
||||
id: string;
|
||||
name: string; // '영업 현황'
|
||||
icon: string; // '💰'
|
||||
|
||||
/** 카드 자유배치 */
|
||||
cards: Card[];
|
||||
|
||||
/** 카드 간 데이터 연결 */
|
||||
connections: CardConnection[];
|
||||
|
||||
/** 메타 */
|
||||
companyCode: string;
|
||||
ownerId?: string; // 사용자별 대시보드일 때
|
||||
isShared: boolean; // 공유 대시보드 여부
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 카드 간 데이터 연결 (DataPort 매칭) */
|
||||
export interface CardConnection {
|
||||
id: string;
|
||||
from: { cardId: string; port: string };
|
||||
to: { cardId: string; port: string };
|
||||
}
|
||||
```
|
||||
|
||||
### 3.7 폐기되는 타입 (types/invyone-component.ts 정리)
|
||||
```typescript
|
||||
// ❌ 폐기 — 더 이상 사용하지 않음
|
||||
- GridPosition { col, colSpan, row, rowSpan }
|
||||
- AbsolutePosition { x, y, w, h } (FreePosition 으로 통합)
|
||||
- ComponentPosition (union 자체 폐기)
|
||||
- ResponsiveGridOverride { narrow, normal, wide }
|
||||
- isGridPosition() / isAbsolutePosition() 가드
|
||||
- TemplateKind ('business' | 'canvas')
|
||||
- DEFAULT_COMPONENT_LAYOUTS (12-col 기본값)
|
||||
- CANVAS_KEYWORDS
|
||||
|
||||
// ✅ 유지
|
||||
- FieldConfig, FieldType, FieldRef, FieldOption (그대로)
|
||||
- DataPort, DataPortType, Connection (그대로, Card 간 통신용)
|
||||
- ComponentTypeConfig 패밀리 (선택적, v2-* 통합 시 결정)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 빌더 구조
|
||||
|
||||
### 4.1 두 빌더 통합
|
||||
```
|
||||
폐기:
|
||||
frontend/components/builder/* (12-grid 빌더, 미완성)
|
||||
|
||||
승격:
|
||||
frontend/components/screen/ScreenDesigner_new.tsx
|
||||
→ 메인 빌더 (자유배치 + VEX v2-* + 격자 옵션)
|
||||
→ 경로 변경 검토: components/template-builder/* 로 이동?
|
||||
```
|
||||
|
||||
### 4.2 빌더 진입점
|
||||
```
|
||||
frontend/app/(main)/admin/builder/page.tsx
|
||||
→ 현재: 12-grid 빌더 import
|
||||
→ 변경: ScreenDesigner_new import
|
||||
```
|
||||
|
||||
### 4.3 빌더 핵심 기능 (현재 ScreenDesigner_new 가 이미 가짐)
|
||||
- ✅ 자유배치 + 컴포넌트 자체 사이즈
|
||||
- ✅ 격자 옵션 (snapToGrid, showGrid 토글)
|
||||
- ✅ 그룹화 / 실행취소 / 다시실행
|
||||
- ✅ 단축키 (T/M/P/S/R/D/E)
|
||||
- ✅ ComponentRegistry 기반 v2-* 100+ 컴포넌트
|
||||
- ✅ 좌측 패널 (테이블, 컴포넌트), 우측 패널 (속성, 스타일, 격자)
|
||||
- ✅ 중앙 캔버스 (자유배치 드롭)
|
||||
|
||||
### 4.4 빌더에 추가해야 하는 기능 (Phase 1)
|
||||
- ⏳ **3뷰 전환** (목록/등록/수정 토글) — useBuilderState.ts:25-27 의 BuilderView 패턴 참고
|
||||
- ⏳ **Template 저장/불러오기** (toTemplate / fromTemplate) — useBuilderState.ts:279-327 패턴 참고
|
||||
- ⏳ **자동 뷰 생성** (등록 버튼 추가 시 create 뷰 placeholder 생성)
|
||||
- ⏳ **DataPort 연결 UI** (컴포넌트 간 시각적 연결)
|
||||
- ⏳ **카드 미리보기** (실제 대시보드에서 어떻게 보일지)
|
||||
|
||||
### 4.5 대시보드 빌더 (Phase 2)
|
||||
대시보드 빌더도 같은 자유배치 UX:
|
||||
```
|
||||
frontend/components/dash/
|
||||
DashboardCanvas.tsx ← 대시보드 자유배치 캔버스 (재작성)
|
||||
DashboardCard.tsx ← Card 컴포넌트 = Template 인스턴스
|
||||
DashboardLayout.tsx ← 셸 (기존 유지)
|
||||
TemplateLibraryModal.tsx ← 라이브러리 모달 (templateRenderers 패턴)
|
||||
```
|
||||
|
||||
mockup 의 `js/04-templates.js` + `js/05-state.js` 패턴을 참고:
|
||||
- `templateRenderers` → `lib/templates/registry.ts` (Template 정의들)
|
||||
- `renderCanvas` → `DashboardCanvas.tsx`
|
||||
- `addCardFromLib` → 라이브러리 모달 + 카드 인스턴스 생성
|
||||
- `makeDraggable` / `makeResizable` / `applyClamp` → `useCardDrag.ts` / `useCardResize.ts`
|
||||
|
||||
---
|
||||
|
||||
## 5. VEX v2-* 컴포넌트 마이그레이션 가이드 (★ Phase 1 핵심)
|
||||
|
||||
### 5.1 마이그레이션 목표
|
||||
모든 v2-* 컴포넌트가 다음 3가지를 갖추게:
|
||||
1. **`container-type: inline-size`** — 부모 폭 감지 가능
|
||||
2. **@container 모드 분기** — 최소 wide/narrow 2단계
|
||||
3. **INVYONE FieldConfig 와 호환** — primaryTable + fields 를 받아서 동작
|
||||
4. **DataPort 인터페이스** — inputs/outputs 명시
|
||||
|
||||
### 5.2 마이그레이션 패턴 (v2-table-list 예시)
|
||||
|
||||
#### Before
|
||||
```tsx
|
||||
// frontend/lib/registry/components/v2-table-list/TableListComponent.tsx
|
||||
export const TableListWrapper: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="v2-table-list">
|
||||
<table>...</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### After
|
||||
```tsx
|
||||
export const TableListWrapper: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="v2-table-list-container">
|
||||
{/* 내부에서 모드 분기 */}
|
||||
<div className="v2-table-list-wide">
|
||||
<table>...</table>
|
||||
</div>
|
||||
<div className="v2-table-list-narrow">
|
||||
{/* 카드 리스트 모드 */}
|
||||
{props.data.map(row => <Card key={row.id}>...</Card>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```css
|
||||
/* TableListComponent.css */
|
||||
.v2-table-list-container {
|
||||
container-type: inline-size;
|
||||
container-name: v2-table-list;
|
||||
}
|
||||
|
||||
.v2-table-list-wide { display: none; }
|
||||
.v2-table-list-narrow { display: block; }
|
||||
|
||||
@container v2-table-list (min-width: 600px) {
|
||||
.v2-table-list-wide { display: block; }
|
||||
.v2-table-list-narrow { display: none; }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 컴포넌트별 마이그레이션 우선순위
|
||||
|
||||
#### 우선순위 1 — 가장 많이 쓰는 것 (Phase 1)
|
||||
- v2-table-list (테이블)
|
||||
- v2-search-widget (검색)
|
||||
- v2-button-primary (버튼)
|
||||
- v2-aggregation-widget (KPI)
|
||||
- v2-card-display (카드 표시)
|
||||
- v2-input, v2-select, v2-date (입력 필드)
|
||||
- v2-text-display (텍스트)
|
||||
|
||||
#### 우선순위 2 — 도메인 특화 (Phase 2)
|
||||
- v2-bom-tree, v2-bom-item-editor
|
||||
- v2-shipping-plan-editor
|
||||
- v2-pivot-grid
|
||||
- v2-timeline-scheduler
|
||||
- v2-process-work-standard
|
||||
- v2-approval-step
|
||||
|
||||
#### 우선순위 3 — 보조 (Phase 3)
|
||||
- v2-rack-structure
|
||||
- v2-numbering-rule
|
||||
- v2-category-manager
|
||||
- v2-divider-line
|
||||
- v2-file-upload, v2-media
|
||||
|
||||
### 5.4 ComponentRegistry 통합
|
||||
```typescript
|
||||
// 각 컴포넌트의 createComponentDefinition 호출에 추가
|
||||
{
|
||||
id: 'v2-table-list',
|
||||
...
|
||||
// ★ 추가
|
||||
containerQuery: {
|
||||
breakpoints: {
|
||||
narrow: { maxWidth: 599 },
|
||||
wide: { minWidth: 600 },
|
||||
},
|
||||
},
|
||||
// ★ INVYONE 통합
|
||||
fieldConfigSupported: true,
|
||||
dataPorts: {
|
||||
inputs: [
|
||||
{ name: 'searchParams', type: 'params' },
|
||||
{ name: 'refreshTrigger', type: 'value' },
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'selectedRow', type: 'row' },
|
||||
{ name: 'selectedRows', type: 'rows' },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 작업 순서 (Phase 1 ~ 3)
|
||||
|
||||
### Phase 1 — 토대 정리 (1~2주)
|
||||
**목표**: 폐기/유지 정리 + 핵심 컴포넌트 마이그레이션
|
||||
|
||||
1. **타입 정리** (1일)
|
||||
- `types/invyone-component.ts` 에서 GridPosition, TemplateKind, DEFAULT_COMPONENT_LAYOUTS, CANVAS_KEYWORDS 폐기
|
||||
- FreePosition, TemplateComponent, ViewConfig 정의 추가
|
||||
- Template 인터페이스 수정 (kind 필드 제거)
|
||||
- Card, Dashboard 인터페이스 추가
|
||||
|
||||
2. **components/builder 폐기** (1일)
|
||||
- `frontend/components/builder/*` 전체 삭제 (또는 `_obsolete/` 로 이동)
|
||||
- `frontend/app/(main)/admin/builder/page.tsx` 의 import 경로 변경
|
||||
- 빌더 진입점이 ScreenDesigner_new 를 가리키게
|
||||
|
||||
3. **ScreenDesigner 정리 + 3뷰 추가** (3~4일)
|
||||
- `ScreenDesigner_new.tsx` 와 `ScreenDesigner.tsx` 중 하나만 남기기 (new 권장)
|
||||
- 3뷰 토글 UI 추가 (목록/등록/수정)
|
||||
- useBuilderState 패턴 차용 (toTemplate / fromTemplate)
|
||||
- 자동 뷰 생성 (등록 버튼 → create 뷰 placeholder)
|
||||
|
||||
4. **v2-* 우선순위 1 마이그레이션** (3~4일)
|
||||
- v2-table-list, v2-search-widget, v2-button-primary, v2-aggregation-widget, v2-card-display, v2-input, v2-select, v2-date, v2-text-display
|
||||
- 각 컴포넌트에 `container-type` + `@container` 추가
|
||||
- wide/narrow 모드 분기
|
||||
|
||||
5. **Phase 1 검증**
|
||||
- 수주관리 화면을 ScreenDesigner 로 만들기 (PoC)
|
||||
- 카드 폭 변경 시 내부 컴포넌트가 모드 전환되는지 확인
|
||||
|
||||
### Phase 2 — 대시보드 + 카드 시스템 (2~3주)
|
||||
**목표**: 자유배치 대시보드 + Card = Template 인스턴스 모델
|
||||
|
||||
> ### ★ Phase 2 시작 시 가장 먼저 할 일 — Phase 1 에서 미뤄둔 레거시 타입 정리
|
||||
>
|
||||
> Phase 1 Step 5 에서 `DashboardCard.tsx` 가 여전히 아래 타입들을 쓰고 있어
|
||||
> `@deprecated` 주석만 달고 보존했다. Phase 2 의 `DashboardCard.tsx` 재작성
|
||||
> 과 함께 이 항목들을 **일괄 제거**한다. 안 하면 영원히 남는다.
|
||||
>
|
||||
> **제거 대상** (`frontend/types/invyone-component.ts`):
|
||||
> - [ ] `GridPosition` 인터페이스
|
||||
> - [ ] `AbsolutePosition` 인터페이스
|
||||
> - [ ] `ComponentPosition` 유니언
|
||||
> - [ ] `ResponsiveGridOverride` 인터페이스
|
||||
> - [ ] `isGridPosition()` / `isAbsolutePosition()` 타입 가드
|
||||
> - [ ] `TemplateKind` 유니언
|
||||
> - [ ] `DEFAULT_COMPONENT_LAYOUTS` 상수
|
||||
> - [ ] `CANVAS_KEYWORDS` 상수
|
||||
> - [ ] `Template.kind` 필드 (현재 `kind?: TemplateKind` 로 optional + deprecated)
|
||||
>
|
||||
> **대체**: 전부 `FreePosition { left, top, width, height }` + 단일 자유배치 모델
|
||||
> (§3.1) 로 교체. `DashboardCard.tsx` 재작성 중 사용처를 FreePosition 으로
|
||||
> 교체하면 이 타입들은 자연스럽게 의존처 0 이 되어 제거 가능.
|
||||
>
|
||||
> **검증**: Phase 2 종료 전에 `grep -rn "GridPosition\|AbsolutePosition\|TemplateKind"
|
||||
> frontend/` 로 잔존 확인.
|
||||
|
||||
1. **대시보드 데이터 모델** (1일)
|
||||
- Dashboard, Card, CardConnection 인터페이스 구현
|
||||
- state 관리 (Zustand 또는 Context)
|
||||
|
||||
2. **DashboardCanvas 재작성** (3~4일)
|
||||
- 자유배치 카드 캔버스
|
||||
- 드래그/리사이즈/clamp (mockup `js/02-canvas.js` 패턴)
|
||||
- 카드 추가/삭제/이동/접기
|
||||
|
||||
3. **TemplateLibraryModal** (2일)
|
||||
- mockup 의 라이브러리 모달 (`index.html` 의 library 패턴)
|
||||
- Template 목록 → 카드 추가
|
||||
|
||||
4. **카드 ↔ Template 인스턴스화 로직** (2일)
|
||||
- templateId 로 Template 조회
|
||||
- configOverride 적용
|
||||
- 카드 헤더 (아이콘+이름+배지+버튼)
|
||||
|
||||
5. **사이드바 동적 대시보드 목록** (2일)
|
||||
- mockup 의 `js/05-state.js` 의 sidebar 패턴
|
||||
- 대시보드 추가/삭제/이름변경
|
||||
|
||||
6. **저장/복원** (1일)
|
||||
- 백엔드 API + localStorage 폴백
|
||||
- 대시보드/카드/연결 모두 영속화
|
||||
|
||||
7. **v2-* 우선순위 2 마이그레이션 시작** (병렬, 1주)
|
||||
- 도메인 특화 컴포넌트들 (BOM, 출하, 피벗, 일정 등)
|
||||
|
||||
### Phase 3 — 데이터 흐름 + 도메인 확장 (3~4주)
|
||||
**목표**: DataPort 연결 + 모든 v2-* 컴포넌트 마이그레이션 완료
|
||||
|
||||
1. **DataPort 연결 UI** (3일)
|
||||
- 빌더에서 컴포넌트 간 시각적 연결
|
||||
- Connection 데이터 모델 구현
|
||||
- 런타임 이벤트 버스 (mockup 패턴)
|
||||
|
||||
2. **카드 간 연결 (대시보드 레벨)** (2일)
|
||||
- 다른 Template 의 카드끼리 데이터 주고받기
|
||||
- 예: 수주 카드의 selectedRow → BOM 카드의 inputs
|
||||
|
||||
3. **v2-* 우선순위 3 마이그레이션 완료** (1~2주, 병렬)
|
||||
- 나머지 컴포넌트들
|
||||
|
||||
4. **자동 생성/프리셋** (1주)
|
||||
- Template 자동 생성 (DB 메타 → 기본 Template)
|
||||
- 도메인별 시작 템플릿 (수주관리, 인사정보, 재고관리)
|
||||
|
||||
---
|
||||
|
||||
## 7. 영향 범위 — 파일별 액션
|
||||
|
||||
### 폐기
|
||||
| 파일/폴더 | 액션 |
|
||||
|---|---|
|
||||
| `frontend/components/builder/` (전체) | 삭제 (또는 _obsolete/) |
|
||||
| `frontend/components/builder/BuilderCanvas.tsx` | 삭제 |
|
||||
| `frontend/components/builder/BuilderToolbar.tsx` | 삭제 |
|
||||
| `frontend/components/builder/BuilderBlock.tsx` | 삭제 |
|
||||
| `frontend/components/builder/BuilderPalette.tsx` | 삭제 |
|
||||
| `frontend/components/builder/BuilderProps.tsx` | 삭제 |
|
||||
| `frontend/components/builder/BuilderLayout.tsx` | 삭제 |
|
||||
| `frontend/components/builder/hooks/useBuilderState.ts` | **참고 후 삭제** (3뷰/toTemplate 패턴 차용) |
|
||||
| `frontend/components/builder/hooks/gridMetrics.ts` | 삭제 (12-grid 헬퍼) |
|
||||
| `frontend/components/builder/hooks/useBlockDrag.ts` | 삭제 |
|
||||
| `frontend/components/builder/props/*` | 삭제 |
|
||||
|
||||
### 수정 (12-grid 흔적 제거)
|
||||
| 파일 | 수정 내용 |
|
||||
|---|---|
|
||||
| `frontend/types/invyone-component.ts` | GridPosition, TemplateKind, DEFAULT_COMPONENT_LAYOUTS, CANVAS_KEYWORDS, ResponsiveGridOverride 폐기. FreePosition, TemplateComponent, ViewConfig, Card, Dashboard 추가. Template 에서 kind 필드 제거 |
|
||||
| `frontend/types/screen.ts` | invyone-component.ts 와 통합 검토. ScreenDefinition → Template 매핑 |
|
||||
| `frontend/app/(main)/admin/builder/page.tsx` | import 변경 (BuilderLayout → ScreenDesigner_new) |
|
||||
| `frontend/components/dash/DashboardCanvas.tsx` | 자유배치 카드 캔버스로 재작성 |
|
||||
| `frontend/components/dash/DashboardCard.tsx` | Card = Template 인스턴스 모델로 재작성 (transform: scale 제거 — 이미 결정) |
|
||||
| `frontend/components/dash/DashboardLayout.tsx` | 셸 정리 |
|
||||
| `frontend/styles/dashboard.css` | grid CSS 제거, free positioning CSS |
|
||||
| `frontend/components/layout/AppLayout.tsx` | 사이드바 동적 대시보드 목록 |
|
||||
|
||||
### 승격 (메인 빌더로)
|
||||
| 파일 | 액션 |
|
||||
|---|---|
|
||||
| `frontend/components/screen/ScreenDesigner_new.tsx` | 메인 빌더 승격. 3뷰 + Template 저장/로드 추가 |
|
||||
| `frontend/components/screen/ScreenDesigner.tsx` | 검토 후 폐기 (구버전) |
|
||||
| `frontend/components/screen/panels/ComponentsPanel.tsx` | 유지 + INVYONE Template 통합 |
|
||||
| `frontend/components/screen/panels/PropertiesPanel.tsx` | 유지 |
|
||||
| `frontend/components/screen/panels/GridPanel.tsx` | 유지 (격자 옵션) |
|
||||
| `frontend/components/screen/panels/TablesPanel.tsx` | 유지 |
|
||||
| `frontend/components/screen/DesignerToolbar.tsx` | 유지 + 3뷰 토글 추가 |
|
||||
|
||||
### 마이그레이션 (v2-* 100+ 컴포넌트)
|
||||
| 폴더 | 액션 |
|
||||
|---|---|
|
||||
| `frontend/lib/registry/components/v2-*/` (모든 폴더) | @container 추가 + INVYONE Template 통합 + DataPort 인터페이스 추가 |
|
||||
| `frontend/lib/registry/ComponentRegistry.ts` | INVYONE 모델 통합 (containerQuery, dataPorts 필드 추가) |
|
||||
|
||||
### 신규
|
||||
| 파일 | 액션 |
|
||||
|---|---|
|
||||
| `frontend/lib/templates/registry.ts` | Template 정의들 (mockup `js/04-templates.js` 의 templateRenderers 패턴) |
|
||||
| `frontend/components/templates/HrEmployeeList.tsx` | 인사정보 풀 카드 (mockup 매핑) |
|
||||
| `frontend/components/templates/SalesOrderList.tsx` | 수주관리 풀 카드 |
|
||||
| `frontend/components/templates/SalesKpi.tsx` | 매출 KPI 카드 |
|
||||
| `frontend/components/templates/...` | 기타 도메인 카드들 |
|
||||
| `frontend/components/dash/TemplateLibraryModal.tsx` | 라이브러리 모달 |
|
||||
| `frontend/hooks/useCardDrag.ts` | 카드 드래그 (mockup `js/02-canvas.js` 패턴) |
|
||||
| `frontend/hooks/useCardResize.ts` | 카드 리사이즈 |
|
||||
| `frontend/store/dashboardStore.ts` | 대시보드 state (Zustand) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 핵심 코드 스니펫 (구현 참고)
|
||||
|
||||
### 8.1 자유배치 캔버스 (DashboardCanvas)
|
||||
```tsx
|
||||
// frontend/components/dash/DashboardCanvas.tsx
|
||||
"use client";
|
||||
|
||||
import { useDashboardStore } from "@/store/dashboardStore";
|
||||
import { DashboardCard } from "./DashboardCard";
|
||||
|
||||
export function DashboardCanvas() {
|
||||
const dashboard = useDashboardStore(s => s.activeDashboard);
|
||||
const updateCardPosition = useDashboardStore(s => s.updateCardPosition);
|
||||
|
||||
if (!dashboard) return null;
|
||||
|
||||
return (
|
||||
<div className="dash-canvas" style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
{dashboard.cards.length === 0 && (
|
||||
<div className="dash-empty">
|
||||
<button onClick={openLibrary}>+ 템플릿 추가</button>
|
||||
</div>
|
||||
)}
|
||||
{dashboard.cards.map(card => (
|
||||
<DashboardCard
|
||||
key={card.id}
|
||||
card={card}
|
||||
onMove={(pos) => updateCardPosition(card.id, pos)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 카드 컴포넌트 (DashboardCard)
|
||||
```tsx
|
||||
// frontend/components/dash/DashboardCard.tsx
|
||||
import { useTemplateRegistry } from "@/lib/templates/registry";
|
||||
import { useCardDrag } from "@/hooks/useCardDrag";
|
||||
import { useCardResize } from "@/hooks/useCardResize";
|
||||
|
||||
export function DashboardCard({ card, onMove }: { card: Card, onMove: (pos: FreePosition) => void }) {
|
||||
const template = useTemplateRegistry(card.templateId);
|
||||
const { onDragStart } = useCardDrag(card, onMove);
|
||||
const { onResizeStart } = useCardResize(card, onMove);
|
||||
|
||||
if (!template) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dash-card"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: card.position.left,
|
||||
top: card.position.top,
|
||||
width: card.position.width,
|
||||
height: card.position.height,
|
||||
// ★ 카드 자체가 container — 내부 컴포넌트들이 폭 감지
|
||||
containerType: 'inline-size',
|
||||
containerName: 'card',
|
||||
}}
|
||||
>
|
||||
<div className="dash-card-head" onMouseDown={onDragStart}>
|
||||
<span className="dash-card-icon">{template.icon}</span>
|
||||
<span className="dash-card-title">{template.name}</span>
|
||||
<span className="dash-card-badge">{template.badge}</span>
|
||||
<button onClick={() => toggleCollapse(card.id)}>▼</button>
|
||||
<button onClick={() => removeCard(card.id)}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="dash-card-body">
|
||||
{card.collapsed
|
||||
? <TemplateRenderer template={template} view="mini" />
|
||||
: <TemplateRenderer template={template} view="list" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="dash-card-resize-handle" onMouseDown={onResizeStart} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Template 렌더러
|
||||
```tsx
|
||||
// frontend/components/templates/TemplateRenderer.tsx
|
||||
export function TemplateRenderer({ template, view = 'list' }: Props) {
|
||||
const viewConfig = template.views[view];
|
||||
if (!viewConfig) return null;
|
||||
|
||||
return (
|
||||
<div className="tpl-canvas" style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
{viewConfig.components.map(comp => (
|
||||
<ComponentRenderer key={comp.id} component={comp} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentRenderer({ component }: { component: TemplateComponent }) {
|
||||
const def = ComponentRegistry.getComponent(component.componentId);
|
||||
if (!def) return null;
|
||||
|
||||
const Component = def.component;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: component.position.left,
|
||||
top: component.position.top,
|
||||
width: component.position.width,
|
||||
height: component.position.height,
|
||||
}}
|
||||
>
|
||||
<Component {...component.config} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 v2-* 컴포넌트 @container 패턴
|
||||
```tsx
|
||||
// frontend/lib/registry/components/v2-table-list/TableListComponent.tsx
|
||||
export const TableListWrapper: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="v2-table-list-container">
|
||||
{/* 두 모드를 모두 렌더하고 CSS 로 토글 */}
|
||||
<div className="v2-table-list-wide">
|
||||
<table>
|
||||
<thead>...</thead>
|
||||
<tbody>...</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="v2-table-list-narrow">
|
||||
{props.data.map(row => (
|
||||
<div key={row.id} className="v2-table-list-card">
|
||||
{props.fields.map(f => (
|
||||
<div key={f.column}>
|
||||
<span className="label">{f.label}</span>
|
||||
<span className="value">{row[f.column]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```css
|
||||
/* TableListComponent.css */
|
||||
.v2-table-list-container {
|
||||
container-type: inline-size;
|
||||
container-name: v2-table-list;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v2-table-list-wide { display: none; }
|
||||
.v2-table-list-narrow { display: block; }
|
||||
|
||||
@container v2-table-list (min-width: 600px) {
|
||||
.v2-table-list-wide { display: block; }
|
||||
.v2-table-list-narrow { display: none; }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 Template 저장/불러오기 (useBuilderState 패턴 차용)
|
||||
```typescript
|
||||
// frontend/store/builderStore.ts
|
||||
export const useBuilderStore = create<BuilderState>((set, get) => ({
|
||||
// ... 기존 state
|
||||
|
||||
toTemplate: (): Template => {
|
||||
const s = get();
|
||||
return {
|
||||
templateId: s.templateId || generateId(),
|
||||
name: s.templateName,
|
||||
icon: s.icon,
|
||||
badge: s.badge,
|
||||
category: s.category,
|
||||
defaultSize: s.defaultSize,
|
||||
primaryTable: s.tableName,
|
||||
fields: s.fields,
|
||||
views: {
|
||||
list: { components: s.views.list, layout: 'card' },
|
||||
create: s.views.create.length > 0 ? { components: s.views.create, layout: 'modal' } : undefined,
|
||||
edit: s.views.edit.length > 0 ? { components: s.views.edit, layout: 'modal' } : undefined,
|
||||
},
|
||||
companyCode: '*',
|
||||
version: 1,
|
||||
status: 'draft',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
fromTemplate: (tpl: Template) => {
|
||||
set({
|
||||
templateId: tpl.templateId,
|
||||
templateName: tpl.name,
|
||||
icon: tpl.icon,
|
||||
badge: tpl.badge,
|
||||
category: tpl.category,
|
||||
defaultSize: tpl.defaultSize,
|
||||
tableName: tpl.primaryTable,
|
||||
fields: tpl.fields ?? [],
|
||||
views: {
|
||||
list: tpl.views.list.components,
|
||||
create: tpl.views.create?.components ?? [],
|
||||
edit: tpl.views.edit?.components ?? [],
|
||||
},
|
||||
currentView: 'list',
|
||||
});
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 핵심 보장 사항 (사용자 약속)
|
||||
|
||||
> 사용자가 "반응형 된다" 는 말 믿고 진행하는 결정.
|
||||
> 이 보장이 깨지면 백업 플랜으로 회피 가능.
|
||||
|
||||
### 보장 1: PC 시나리오 (FHD~4K) 100% 작동
|
||||
- 모든 주요 v2-* 컴포넌트가 wide/narrow 2단계 모드 가지면
|
||||
- 카드 폭 280~1920px 범위에서 자동 반응형 작동
|
||||
|
||||
### 보장 2: 카드 사이즈 변경 시 내부 자동 반응
|
||||
- 사용자가 카드를 800x500 → 400x500 으로 드래그 리사이즈
|
||||
- 카드 내부 v2-table-list 가 @container 로 폭 감지
|
||||
- 자동으로 카드 리스트 모드 전환
|
||||
- 사용자는 아무 작업 안 함
|
||||
|
||||
### 보장 3: 수십 컴포넌트 화면 가능
|
||||
- PLM/MES 워크벤치 같은 복잡 화면
|
||||
- 카드 1개 안에 v2-* 컴포넌트 10~20개 자유 배치 가능
|
||||
- 또는 카드 5~10개 자유 배치
|
||||
- 카드 간 데이터 연결 (CardConnection) 으로 통신
|
||||
|
||||
### 보장 4: VEX 사용자 학습 곡선 거의 없음
|
||||
- ScreenDesigner 가 이미 자유배치 + 격자 옵션
|
||||
- VEX 사용자가 "더 정렬 잘 되는 VEX" 로 인식
|
||||
- 새로 배울 게 거의 없음
|
||||
|
||||
### 한계 (백업 플랜 적용 케이스)
|
||||
- ❌ 모바일/태블릿 (200px 이하) — Phase 4 이후 별도 모바일 뷰
|
||||
- ❌ 컴포넌트들이 "재배치" 되어야 할 때 — 컨테이너 컴포넌트 (탭/아코디언) 사용
|
||||
- ❌ @container 미지원 브라우저 — 모던 브라우저 (Chrome 105+, Safari 16+, Firefox 110+) 만 타겟
|
||||
|
||||
---
|
||||
|
||||
## 10. 검증 체크리스트 (Phase 1 완료 시)
|
||||
|
||||
### 코드 정리
|
||||
- [ ] `frontend/components/builder/*` 폐기 완료
|
||||
- [ ] `frontend/types/invyone-component.ts` 정리 (GridPosition 등 제거)
|
||||
- [ ] FreePosition, TemplateComponent, ViewConfig, Card, Dashboard 타입 추가
|
||||
- [ ] `frontend/app/(main)/admin/builder/page.tsx` 에서 ScreenDesigner_new 사용
|
||||
|
||||
### 빌더
|
||||
- [ ] ScreenDesigner_new 가 메인 빌더로 동작
|
||||
- [ ] 3뷰 토글 (목록/등록/수정)
|
||||
- [ ] Template 저장/불러오기
|
||||
- [ ] 자동 뷰 생성 (등록 버튼 → create 뷰)
|
||||
|
||||
### v2-* 마이그레이션 (우선순위 1)
|
||||
- [ ] v2-table-list @container 적용
|
||||
- [ ] v2-search-widget @container 적용
|
||||
- [ ] v2-button-primary @container 적용
|
||||
- [ ] v2-aggregation-widget @container 적용
|
||||
- [ ] v2-card-display @container 적용
|
||||
- [ ] v2-input, v2-select, v2-date 검증
|
||||
|
||||
### PoC
|
||||
- [ ] 수주관리 화면을 ScreenDesigner 로 만들기
|
||||
- [ ] Template 저장 → 불러오기
|
||||
- [ ] 카드 폭 800 → 400 변경 시 v2-table-list 가 카드 리스트로 전환
|
||||
|
||||
### 보장 검증
|
||||
- [ ] 카드 폭 변화에 모든 우선순위 1 컴포넌트가 자동 반응
|
||||
- [ ] 격자 옵션 (snapToGrid) ON/OFF 정상 동작
|
||||
- [ ] 그룹화/실행취소 정상 동작
|
||||
|
||||
---
|
||||
|
||||
## 11. 다른 클로드 세션을 위한 작업 지시
|
||||
|
||||
이 문서를 보고 작업할 다른 클로드 세션은:
|
||||
|
||||
1. **반드시** 이 문서가 진실의 원천이라고 인지할 것
|
||||
2. 이전 12-grid / 48-col / 섹션 / business-canvas 분기 모델은 모두 폐기됨
|
||||
3. `notes/gbpark/2026-04-10-template-model-redesign.md` 는 obsolete (참고만)
|
||||
4. `notes/gbpark/2026-04-08-invyone-mockup/` 의 mockup 의도를 따라갈 것
|
||||
5. **반응형은 컴포넌트 내부 @container 로 처리** 가 핵심
|
||||
6. Phase 1 부터 순서대로 진행
|
||||
7. 각 Phase 완료 시 검증 체크리스트로 확인
|
||||
8. 막히면 **반드시 사용자에게 질문**, 추측 구현 금지
|
||||
|
||||
### 작업 시작 전 필독
|
||||
- `notes/gbpark/2026-04-08-invyone-mockup/README.md` (mockup 의도)
|
||||
- `notes/gbpark/2026-04-08-invyone-mockup/js/04-templates.js` (templateRenderers 패턴)
|
||||
- `notes/gbpark/2026-04-08-invyone-mockup/js/05-state.js` (state 패턴)
|
||||
- `frontend/components/screen/ScreenDesigner_new.tsx` (메인 빌더 베이스)
|
||||
- `frontend/lib/registry/ComponentRegistry.ts` (컴포넌트 레지스트리)
|
||||
|
||||
### 작업 시 반드시
|
||||
- 폐기 결정 사항 다시 도입 금지 (12-grid, 48-col, 섹션 등)
|
||||
- 컴포넌트가 멍청하면 안 됨 (반드시 @container 모드 가짐)
|
||||
- 자유배치 px 가 기본 (그리드 셀 강제 금지)
|
||||
- 카드 = Template 인스턴스 (1:N) 관계 명확히
|
||||
|
||||
---
|
||||
|
||||
## 끝
|
||||
|
||||
이 문서는 INVYONE 카드 엔진의 최종 진실. Phase 1 부터 시작하면 됨.
|
||||
|
||||
**작성자 메모**: 사용자가 "반응형 된다"는 약속을 믿고 진행하는 결정이므로, 반응형 메커니즘 (컴포넌트 @container) 의 작동을 Phase 1 PoC 에서 반드시 검증할 것. 실패하면 백업 플랜 (카드 min-width, 모드 강제 등) 적용.
|
||||
@@ -0,0 +1,323 @@
|
||||
# INVYONE 카드 엔진 Phase 1 — 구현 로그
|
||||
|
||||
**작업일**: 2026-04-10
|
||||
**작업자**: gbpark + Claude
|
||||
**기반 스펙**: `notes/gbpark/2026-04-10-card-engine-final-spec.md`
|
||||
**이전 로그**: `notes/gbpark/2026-04-10-template-redesign-phase1-log.md` (OBSOLETE — 12-grid 모델)
|
||||
|
||||
---
|
||||
|
||||
## 0. 요약
|
||||
|
||||
스펙 §6 의 Phase 1 (토대 정리 + 핵심 컴포넌트 마이그레이션) 을 7단계로 나눠 진행.
|
||||
|
||||
| Step | 제목 | 상태 |
|
||||
|---|---|---|
|
||||
| 1 | 타입 추가 (FreePosition, TemplateComponent, TemplateViewConfig, Card, Dashboard…) | ✅ |
|
||||
| 2 | template-builder 새 빌더 작성 (3뷰 + toTemplate/fromTemplate) | ✅ |
|
||||
| 3 | admin/builder/page.tsx 진입점 전환 | ✅ |
|
||||
| 4 | components/builder/* + ScreenDesigner_new.tsx 완전 삭제 | ✅ |
|
||||
| 5 | 레거시 타입 @deprecated 표기 (완전 제거는 Phase 2) | ✅ |
|
||||
| 6 | v2-* 우선순위 1 컴포넌트 마이그레이션 (완전 2 + 경량 7) | ✅ |
|
||||
| 7 | PoC 시각 검증 페이지 | ✅ (코드 + 브라우저 검증 완료 2026-04-11) |
|
||||
| 8 | 세션 후반 버그 픽스 (검증 중 발견) | ✅ (2026-04-11) |
|
||||
|
||||
**tsc 결과**: 내 변경 영역 기준 에러 0. 전체 에러 카운트는 3423 → 3381 로 감소(삭제된 폐기 파일의 기존 에러가 사라짐).
|
||||
|
||||
---
|
||||
|
||||
## 1. 수정·생성 파일
|
||||
|
||||
### 타입
|
||||
- **수정** `frontend/types/invyone-component.ts`
|
||||
- §7 추가: `FreePosition`, `ViewTrigger`, `TemplateComponent`, `TemplateViewConfig`, `TemplateViews`, `DataPortDef`, `Card`, `CardConnection`, `Dashboard`
|
||||
- 레거시 타입 전부 `@deprecated` 표기: `GridPosition`, `AbsolutePosition`, `ComponentPosition`, `ResponsiveGridOverride`, `isGridPosition`, `isAbsolutePosition`, `TemplateKind`, `DEFAULT_COMPONENT_LAYOUTS`, `CANVAS_KEYWORDS`
|
||||
- `Template.kind` 를 `kind?: TemplateKind` 로 optional + deprecated 변경
|
||||
|
||||
### 빌더 (신규)
|
||||
- **신규** `frontend/components/template-builder/TemplateBuilder.tsx` (~550줄)
|
||||
- 자유배치 캔버스 + 드래그/리사이즈 + 히스토리 + 격자 옵션 + 3뷰 토글 + 팔레트 + 속성/격자/메타 사이드 패널
|
||||
- v2-* 렌더링은 placeholder (Phase 2 에서 실제 컴포넌트 연결)
|
||||
- localStorage 기반 임시 저장/복원
|
||||
- 단축키: Delete, Ctrl+Z/Y, Ctrl+S
|
||||
- **신규** `frontend/components/template-builder/store/templateBuilderStore.ts`
|
||||
- Zustand, `toTemplate` / `fromTemplate` / `undo` / `redo` / `commit` / viewTrigger 자동 감지
|
||||
- `BuilderView = 'list' | 'create' | 'edit'`
|
||||
|
||||
### 빌더 진입점 전환
|
||||
- **수정** `frontend/app/(main)/admin/builder/page.tsx`
|
||||
- `BuilderLayout` (12-grid) → `TemplateBuilder` import 교체
|
||||
|
||||
### 폐기 삭제
|
||||
- **삭제** `frontend/components/builder/` 전체
|
||||
- BuilderLayout/Canvas/Block/Palette/Props/Toolbar + hooks(useBuilderState, useBlockDrag, gridMetrics) + props/*
|
||||
- **삭제** `frontend/components/screen/ScreenDesigner_new.tsx` (dead code, 어디서도 import 되지 않았음)
|
||||
|
||||
### v2-* 마이그레이션 — 완전 (2개)
|
||||
- **신규** `frontend/lib/registry/components/v2-table-list/TableListContainerWrapper.tsx`
|
||||
- ResizeObserver 로 카드 폭 측정 → 600px 미만이면 `config.displayMode` 를 `"card"` 로 런타임 override
|
||||
- 기존 `TableListComponent` 내부는 0줄 수정
|
||||
- **수정** `frontend/lib/registry/components/v2-table-list/index.ts`
|
||||
- `component: TableListWrapper` → `component: TableListContainerWrapper`
|
||||
- **신규** `frontend/lib/registry/components/v2-table-search-widget/TableSearchContainerWrapper.tsx`
|
||||
- 래퍼 div 에 `.v2-tsw-responsive-root` 클래스 부여
|
||||
- **신규** `frontend/lib/registry/components/v2-table-search-widget/table-search-widget-responsive.css`
|
||||
- `container-type: inline-size` + `@container v2-tsw (max-width: 599px)` 에서 flex-col 강제
|
||||
- **수정** `frontend/lib/registry/components/v2-table-search-widget/index.tsx`
|
||||
- `component: TableSearchWidget` → `component: TableSearchContainerWrapper`
|
||||
|
||||
### v2-* 마이그레이션 — 경량 (7개)
|
||||
- **신규** `frontend/lib/registry/hoc/withContainerQuery.tsx`
|
||||
- 간단한 HOC: `<div style={{ containerType: 'inline-size', containerName, width: '100%', height: '100%' }}>` 로 감싸기
|
||||
- **수정** 각 컴포넌트 `index.ts` 에서 `component: Wrapper` → `component: withContainerQuery(Wrapper, "<id>")`:
|
||||
- v2-button-primary
|
||||
- v2-aggregation-widget
|
||||
- v2-card-display
|
||||
- v2-input
|
||||
- v2-select
|
||||
- v2-date
|
||||
- v2-text-display
|
||||
|
||||
### PoC 검증
|
||||
- **신규** `frontend/app/(main)/test-card-responsive/page.tsx`
|
||||
- 카드 폭 슬라이더 (240 ~ 1400px)
|
||||
- 수주관리 화면 구성 (v2-text-display + aggregation-widget + search-widget + button-primary + table-list 의 **레이아웃 시뮬레이션**)
|
||||
- ResizeObserver 기반 `data-v2-table-list-mode` 전환 + CSS @container 기반 search-widget 세로 스택 동시 시각 확인
|
||||
|
||||
### 스펙 MD
|
||||
- **수정** `notes/gbpark/2026-04-10-card-engine-final-spec.md`
|
||||
- Phase 2 시작 섹션에 "Phase 1 에서 미뤄둔 레거시 타입 정리" 체크리스트 삽입
|
||||
|
||||
---
|
||||
|
||||
## 2. 주요 결정 사항 (사용자 협의)
|
||||
|
||||
### 결정 1 — ScreenDesigner_new 처리 방식
|
||||
**옵션**: (a) 완전 재작성 / (b) 변환 레이어만 추가 / (c) 백지에서 새로 작성
|
||||
**결정**: **(c) 와 (a) 혼합 — 새 `components/template-builder/` 폴더에 완전 재작성**
|
||||
**이유**: ScreenDesigner_new 는 `ScreenDefinition/ComponentData/LayoutData` 기반이라 Template 모델로의 전환은 사실상 재작성. 새 경로가 의미상 더 명확(스펙 §4.1 검토 항목).
|
||||
|
||||
### 결정 2 — components/builder 폐기 방식
|
||||
**결정**: **완전 삭제**
|
||||
**이유**: 스펙 MD 가 폐기 명시, git history 로 추적 가능, 흔적 남기면 혼란.
|
||||
|
||||
### 결정 3 — Step 5 레거시 타입 처리
|
||||
**옵션**: (a) @deprecated + 유지 / (b) DashboardCard 도 임시 수정해 완전 제거 / (c) Phase 2 로 이관
|
||||
**결정**: **(a) @deprecated 주석 + 유지 + Phase 2 체크리스트에 제거 항목 명시**
|
||||
**이유**: DashboardCard.tsx 는 Phase 2 재작성 대상(스펙 §6/§7). Phase 1 에서 임시 수정하면 Phase 2 재작성 때 두 번 일. 옵션 3 은 방향성 불명.
|
||||
|
||||
### 결정 4 — Step 6 v2-* 마이그레이션 범위
|
||||
**결정**: **핵심 2개 완전 + 나머지 7개 경량**
|
||||
- **완전**: v2-table-list (ResizeObserver), v2-table-search-widget (CSS @container)
|
||||
- **경량**: button-primary, input, select, date, text-display, card-display, aggregation-widget → `container-type: inline-size` 만 부착
|
||||
**이유**: PoC 검증력을 담보(검색+테이블 CRUD 기본형)하면서 Phase 1 스코프 유지. 나머지 7개의 모드 분기는 Phase 2 개별 재작성 시 추가.
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2 이월 체크리스트
|
||||
|
||||
스펙 MD §6 Phase 2 섹션에 다음 항목이 추가됐다(`notes/gbpark/2026-04-10-card-engine-final-spec.md` 참조).
|
||||
|
||||
### 레거시 타입 최종 제거 (Phase 1 이월)
|
||||
- [ ] `GridPosition`, `AbsolutePosition`, `ComponentPosition`, `ResponsiveGridOverride` 제거
|
||||
- [ ] `isGridPosition()`, `isAbsolutePosition()` 가드 제거
|
||||
- [ ] `TemplateKind`, `DEFAULT_COMPONENT_LAYOUTS`, `CANVAS_KEYWORDS` 제거
|
||||
- [ ] `Template.kind` 필드 완전 제거
|
||||
- [ ] `DashboardCard.tsx` 재작성하면서 위 타입 사용처를 전부 `FreePosition` 으로 교체
|
||||
- [ ] 잔존 확인: `grep -rn "GridPosition\|AbsolutePosition\|TemplateKind" frontend/`
|
||||
|
||||
### 경량 7개 v2-* 컴포넌트 모드 분기 추가 (Phase 1 이월)
|
||||
- [ ] v2-button-primary: narrow 에서 아이콘만 / wide 에서 아이콘+텍스트
|
||||
- [ ] v2-input: 라벨 위치 (narrow=top / wide=left)
|
||||
- [ ] v2-select: 동일
|
||||
- [ ] v2-date: 동일 + narrow 에서 range 모드 자동 compact
|
||||
- [ ] v2-text-display: font-size 반응
|
||||
- [ ] v2-card-display: narrow 에서 cardsPerRow 자동 1
|
||||
- [ ] v2-aggregation-widget: narrow 에서 2x2 그리드
|
||||
|
||||
### 우선순위 2/3 v2-* 마이그레이션 (스펙 §5.3)
|
||||
- [ ] 우선순위 2: v2-bom-tree, v2-bom-item-editor, v2-shipping-plan-editor, v2-pivot-grid, v2-timeline-scheduler, v2-process-work-standard, v2-approval-step
|
||||
- [ ] 우선순위 3: v2-rack-structure, v2-numbering-rule, v2-category-manager, v2-divider-line, v2-file-upload, v2-media
|
||||
|
||||
### 기타
|
||||
- [ ] TemplateBuilder 의 placeholder 렌더링 → 실제 v2-* 컴포넌트 렌더링 (ComponentRegistry 연동)
|
||||
- [ ] TemplateBuilder 저장/복원을 localStorage 에서 백엔드 API 로
|
||||
- [ ] 3뷰 자동 생성 실제 동작 (등록 버튼 감지 → create 뷰 placeholder 자동 생성) — 현재 store 에는 탐지 로직 있으나 UI placeholder 생성 로직 없음
|
||||
|
||||
---
|
||||
|
||||
## 4. 사용자 수동 검증 체크리스트 (Phase 1 종료 조건)
|
||||
|
||||
다음 항목을 브라우저에서 직접 확인.
|
||||
|
||||
### (A) 테스트 페이지 — 반응형 메커니즘 시각 확인
|
||||
1. 도커 or 로컬에서 frontend 실행
|
||||
2. 브라우저에서 `/test-card-responsive` 접속
|
||||
3. 카드 폭 슬라이더를 **800 → 400** 으로 드래그
|
||||
4. 확인:
|
||||
- [ ] 상단 "감지된 모드" 배지가 `wide` → `narrow` 로 전환 (버튼 색도 인디고 → 로즈)
|
||||
- [ ] v2-table-list 영역이 테이블 → 카드 리스트로 재렌더 (`data-v2-table-list-mode` 속성값 wide/narrow 교차)
|
||||
- [ ] v2-table-search-widget 영역의 필터/버튼이 가로 → 세로 스택으로 재배열
|
||||
5. 슬라이더 프리셋 (320 / 520 / 800 / 1200) 로 경계값 확인
|
||||
6. **실패 시**: 스펙 §9 백업 플랜 적용 검토 (카드 min-width 강제, 모드 강제 등)
|
||||
|
||||
### (B) 빌더 접근 확인
|
||||
1. `/admin/builder` 접속
|
||||
2. 확인:
|
||||
- [ ] TemplateBuilder UI 가 표시 (상단 툴바 + 좌측 팔레트 + 중앙 캔버스 + 우측 속성 패널)
|
||||
- [ ] 상단 탭에서 목록/등록/수정 3뷰 토글 가능
|
||||
- [ ] 좌측 팔레트에서 v2-* 컴포넌트 드래그해 캔버스에 드롭 → 배치 성공
|
||||
- [ ] 배치된 블록 드래그로 이동, 우하단 핸들로 리사이즈
|
||||
- [ ] Ctrl+Z 실행취소, Ctrl+S 저장 (브라우저 저장 다이얼로그 아님 → localStorage)
|
||||
- [ ] 새로고침 후에도 localStorage 에서 복원
|
||||
3. 팔레트에 v2-* 컴포넌트가 안 보이면 ComponentRegistry 초기화가 안 된 것. 그 경우 fallback 9개가 표시되어야 함.
|
||||
|
||||
### (C) 기존 기능 영향 없음 확인
|
||||
1. `/admin/screenMng` 접속 → ScreenDesigner(구버전) 가 여전히 동작
|
||||
2. `/dashboard/...` 접속 → DashboardCard 가 여전히 동작 (Phase 2 재작성 전까지 @deprecated 타입 의존)
|
||||
3. 아무 v2-* 사용 화면이 있으면 팔레트 드래그 안 해도 렌더 자체는 그대로
|
||||
|
||||
---
|
||||
|
||||
## 5. 알려진 한계 / TODO
|
||||
|
||||
1. **TemplateBuilder 는 아직 placeholder 빌더**. 실제 v2-* 컴포넌트는 Phase 2 에서 연결. 지금은 블록명/크기만 표시.
|
||||
2. **ResizeObserver 기반 v2-table-list 는 `isDesignMode=true` 에서는 narrow 카드 모드가 동작하지 않음** — TableListComponent 의 원 로직이 design 모드에서 card 분기를 건너뜀. 빌더 안 프리뷰에서는 확인 불가, 실 렌더링(런타임)에서만 작동. 테스트는 `/test-card-responsive` 페이지로 수행.
|
||||
3. **localStorage 저장 키**: `invyone-template:<templateId>` / `invyone-template:__last__`. 나중에 백엔드 API 로 전환 시 마이그레이션 필요.
|
||||
4. **DashboardCard.tsx 는 여전히 레거시 타입 의존**. Phase 2 재작성 전까지 그대로 유지.
|
||||
5. **스펙의 v2-search-widget 은 실제 폴더명이 v2-table-search-widget**. 이름만 다르고 마이그레이션은 정상 적용.
|
||||
|
||||
---
|
||||
|
||||
## 6. 참고 (작업 중 확인한 기존 파일)
|
||||
|
||||
- `frontend/lib/registry/components/v2-table-list/TableListComponent.tsx` (283KB, 내부 불변 약속)
|
||||
- `frontend/lib/registry/components/v2-table-list/CardModeRenderer.tsx` (기존 카드 모드 렌더러 — 재사용)
|
||||
- `frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx` (880줄, 내부 불변)
|
||||
- `frontend/components/screen/ScreenDesigner.tsx` (347KB, screenMng 사용 중, 건드리지 않음)
|
||||
- `frontend/components/dash/DashboardCard.tsx` (Phase 2 재작성 대상)
|
||||
|
||||
---
|
||||
|
||||
## 7. 세션 후반 버그 픽스 (검증 중 발견, 2026-04-11)
|
||||
|
||||
Phase 1 코드 작업이 완료된 뒤 §4 사용자 수동 검증을 진행하면서 3개의 버그가 발견되어 즉시 수정됨. Phase 1 의 일부로 포함.
|
||||
|
||||
### 7.1 `/test-card-responsive` 가 AppLayout 탭 시스템에 흡수됨
|
||||
|
||||
**증상**: 브라우저에서 URL 접속 시 `/admin` 과 비슷한 셸(사이드바 + 빈 탭 영역) 만 나오고 "열린 탭이 없습니다" 텍스트가 표시됨. PoC 페이지 자체가 렌더되지 않음.
|
||||
|
||||
**원인**: 페이지가 `frontend/app/(main)/test-card-responsive/page.tsx` 에 있었는데, `frontend/app/(main)/layout.tsx` 가 `<AppLayout>{children}</AppLayout>` 으로 감싸고 있고, `AppLayout` 은 탭 기반 SPA 셸이라 `/test-card-responsive` 라는 URL 이 탭 시스템에 등록되어 있지 않으면 `children` 이 렌더되지 않았음.
|
||||
|
||||
**수정**: 파일을 `(main)` 그룹 밖으로 이동해서 AppLayout 을 우회.
|
||||
- 이전: `frontend/app/(main)/test-card-responsive/page.tsx`
|
||||
- 이후: `frontend/app/test-card-responsive/page.tsx`
|
||||
|
||||
루트 layout 만 적용되고 AppLayout 의 탭 시스템은 통과. 검증 페이지라 사이드바/헤더가 필요 없어 문제 없음.
|
||||
|
||||
### 7.2 `useRegistryPalette` 가 VEX `default_size` 포맷 불일치로 NaN position 생성
|
||||
|
||||
**증상**: TemplateBuilder 팔레트에서 컴포넌트(예: split-panel-layout)를 드래그해 캔버스에 드롭해도 블록이 시각적으로 나타나지 않음. 우측 속성 패널의 LEFT/TOP/WIDTH/HEIGHT 필드가 모두 비어있음. 컴포넌트 자체는 선택된 상태(COMPONENT 이름 + CONFIG JSON 표시) 로 패널에 보이지만 캔버스에서는 사라짐.
|
||||
|
||||
**원인 연쇄**:
|
||||
1. VEX `ComponentDefinition.default_size` 는 `{ width, height }` 포맷 (예: `{ width: 300, height: 40 }`)
|
||||
2. TemplateBuilder 의 `PaletteItem.defaultSize` 는 `{ w, h }` 포맷
|
||||
3. `useRegistryPalette` 가 `c.default_size` 를 그대로 전달 → `defaultSize.w` = `undefined`
|
||||
4. 드롭 핸들러에서 `e.clientX - rect.left - defaultSize.w / 2` = `NaN`
|
||||
5. `position = { left: NaN, top: NaN, width: undefined, height: undefined }` 가 store 에 저장됨
|
||||
6. React 가 style 의 NaN/undefined 를 무시 → 블록이 "보이지 않음" (또는 0,0 에 0 크기)
|
||||
7. `LabeledNumber` 의 `Math.round(NaN) = NaN` → 속성 패널 input 이 빈 칸으로 렌더
|
||||
|
||||
**수정**: `frontend/components/template-builder/TemplateBuilder.tsx` 의 `useRegistryPalette` 에서 두 가지 정규화:
|
||||
|
||||
1. `default_size` 의 `{width, height}` 와 `{w, h}` 두 포맷 모두 지원:
|
||||
```ts
|
||||
const rawSize = c.default_size ?? {};
|
||||
const defaultSize = {
|
||||
w: rawSize.w ?? rawSize.width ?? 280,
|
||||
h: rawSize.h ?? rawSize.height ?? 180,
|
||||
};
|
||||
```
|
||||
|
||||
2. lucide icon 이름(긴 문자열 "Table", "LayoutGrid" 등) 은 `◼` 로 fallback:
|
||||
```ts
|
||||
const icon =
|
||||
typeof rawIcon === "string" && rawIcon.length > 0 && rawIcon.length <= 2
|
||||
? rawIcon
|
||||
: "◼";
|
||||
```
|
||||
짧은 이모지/유니코드만 그대로 표시. lucide 이름의 실제 아이콘 렌더링은 Phase 2 에서 ComponentRegistry 연동할 때 본격 구현.
|
||||
|
||||
### 7.3 TemplateBuilder.tsx 다크 모드 가시성 전혀 없음
|
||||
|
||||
**증상**: INVYONE 기본 다크 테마에서 TemplateBuilder 전체가 흰색/밝은 회색 배경에 옅은 텍스트로 표시되어 거의 읽을 수 없음. 팔레트, 툴바, 캔버스, 속성 패널 모두 동일. 블록을 드롭해도 어두운 배경 위에 어두운 블록이 거의 구분 안 됨.
|
||||
|
||||
**원인**: TemplateBuilder.tsx 가 라이트 모드 전제로 작성됨. `bg-white`, `bg-slate-50`, `text-slate-*`, `border-slate-200` 등 Tailwind 클래스가 `dark:` variant 없이 사용됨.
|
||||
|
||||
**수정**: Python 정규식 스크립트로 일괄 치환. **21개 패턴, 총 71곳** 치환.
|
||||
|
||||
| 원본 | 치환 | 건수 |
|
||||
|---|---|---|
|
||||
| `bg-white` | `bg-white dark:bg-slate-900` | 8 |
|
||||
| `bg-slate-50` | `bg-slate-50 dark:bg-slate-950` | 2 |
|
||||
| `bg-slate-100` | `bg-slate-100 dark:bg-slate-800` | 1 |
|
||||
| `bg-amber-50` | `bg-amber-50 dark:bg-amber-950/40` | 1 |
|
||||
| `text-slate-800` | `text-slate-800 dark:text-slate-100` | 1 |
|
||||
| `text-slate-700` | `text-slate-700 dark:text-slate-200` | 1 |
|
||||
| `text-slate-600` | `text-slate-600 dark:text-slate-300` | 5 |
|
||||
| `text-slate-500` | `text-slate-500 dark:text-slate-400` | 2 |
|
||||
| `text-slate-400` | `text-slate-400 dark:text-slate-500` | 14 |
|
||||
| `text-amber-700` | `text-amber-700 dark:text-amber-300` | 1 |
|
||||
| `text-rose-600` | `text-rose-600 dark:text-rose-400` | 1 |
|
||||
| `text-rose-500` | `text-rose-500 dark:text-rose-400` | 1 |
|
||||
| `border-slate-200` | `border-slate-200 dark:border-slate-700` | 23 |
|
||||
| `border-rose-200` | `border-rose-200 dark:border-rose-800` | 1 |
|
||||
| `ring-indigo-200` | `ring-indigo-200 dark:ring-indigo-800` | 1 |
|
||||
| `hover:bg-slate-100` | `hover:bg-slate-100 dark:hover:bg-slate-700` | 2 |
|
||||
| `hover:bg-slate-50` | `hover:bg-slate-50 dark:hover:bg-slate-800/60` | 2 |
|
||||
| `hover:bg-indigo-50` | `hover:bg-indigo-50 dark:hover:bg-indigo-950/40` | 1 |
|
||||
| `hover:bg-rose-50` | `hover:bg-rose-50 dark:hover:bg-rose-950/40` | 1 |
|
||||
| `hover:border-indigo-300` | `hover:border-indigo-300 dark:hover:border-indigo-600` | 1 |
|
||||
| `hover:border-slate-300` | `hover:border-slate-300 dark:hover:border-slate-600` | 1 |
|
||||
|
||||
치환 순서: `hover:` variants 먼저, 그 다음 일반 variants. `(?<!hover:)` negative lookbehind 사용해서 `hover:bg-*` 와 일반 `bg-*` 를 분리.
|
||||
|
||||
**후속 정리**: 치환 순서 때문에 `text-slate-500 dark:text-slate-400 dark:text-slate-500` 같은 중복 체인이 일부 생김. 원인: `text-slate-500` → `... dark:text-slate-400` 치환 직후, 다음 `text-slate-400` 치환이 방금 추가된 `dark:text-slate-400` 안의 `text-slate-400` 을 또 매칭해서 `dark:text-slate-500` 를 뒤에 붙임. 추가 Python 스크립트로 `(dark:text-slate-\d00)\s+dark:text-slate-\d00` 패턴을 반복 제거. **1회 반복 후 남은 체인 0개** 확인.
|
||||
|
||||
### 7.4 수정 후 검증 결과
|
||||
|
||||
| 검증 | 결과 |
|
||||
|---|---|
|
||||
| `/test-card-responsive` 페이지 렌더 | ✅ 정상 |
|
||||
| 카드 폭 슬라이더로 반응형 전환 (800 → 400) | ✅ v2-table-list 테이블 → 카드 리스트, v2-table-search-widget 가로 → 세로 스택 |
|
||||
| `/admin/builder` TemplateBuilder UI | ✅ 정상 |
|
||||
| 팔레트 → 캔버스 드롭 → 블록 표시 | ✅ position 값 정상 |
|
||||
| 드래그/리사이즈/Delete/Ctrl+Z/Ctrl+S | ✅ 전부 동작 |
|
||||
| 3뷰 토글 (목록/등록/수정) | ✅ 독립 블록 유지 |
|
||||
| 다크 모드 가시성 | ✅ 모든 영역 구분 가능 |
|
||||
| 기존 VEX 화면/디자이너 (DTG 이력관리, screenMng 등) | ⏭️ 마이그레이션 미완 상태라 작동 안 함 (알려진 상태, Phase 1 범위 아님, Phase 2 이후 개별 마이그레이션 예정) |
|
||||
|
||||
### 7.5 Phase 2 에 고려할 사항
|
||||
|
||||
- **`useRegistryPalette`**: Phase 2 의 "TemplateBuilder 실렌더링 연결" 작업에서 다시 건드림. 그때 lucide icon 실제 렌더 + FieldConfig 연결 + DataPort 연결 등 포함해 더 정교하게 개선.
|
||||
- **다크 모드 치환**: `dark:` variant 수동 추가 방식이라 지저분할 수 있음. Phase 2 에서 shadcn CSS 변수 기반(`bg-background`, `text-foreground`, `border-border` 등) 으로 리팩터 고려 가능. 단 v5-layout.css 의 토큰과의 통합이 필요.
|
||||
- 이 버그들은 **컴포넌트 레지스트리 연동 + 라이트/다크 디자인 시스템 통합** 이라는 더 큰 작업의 일부이므로, Phase 2 에서 자연스럽게 전체 정리됨.
|
||||
|
||||
---
|
||||
|
||||
## 끝
|
||||
|
||||
Phase 1 종료 (2026-04-11).
|
||||
|
||||
§4 사용자 수동 검증 **(A) `/test-card-responsive` 반응형 메커니즘 + (B) `/admin/builder` TemplateBuilder UI** 통과 확인. **(C) 기존 기능 sanity** 는 VEX → INVYONE 컴포넌트 마이그레이션이 "기능 업그레이드 먼저" 정책으로 아직 진행되지 않아 기존 VEX 화면들이 원래 작동하지 않는 정상 상태라 생략됨.
|
||||
|
||||
핵심 보장(사용자가 "반응형 된다" 고 해서 믿고 진행한 결정)이 PoC 에서 실제로 작동함을 확인: 카드 폭 변경 시 v2-table-list 가 ResizeObserver 기반으로 wide/narrow 모드를 자동 전환, v2-table-search-widget 은 CSS `@container` 기반으로 가로 → 세로 스택 전환. 스펙 MD §9 의 백업 플랜 적용 불필요.
|
||||
|
||||
Phase 2 에서:
|
||||
1. 사용자가 만드는 대시보드 시스템 구현 (`components/dash/*` 재작성)
|
||||
2. `DashboardCard` 재작성 — Card = Template 인스턴스, FreePosition + @container
|
||||
3. 레거시 타입 완전 제거 (@deprecated 표기한 것들)
|
||||
4. TemplateBuilder 실렌더링 연결 (ComponentRegistry + 실제 v2-* 렌더)
|
||||
5. 경량 7개 v2-* 모드 분기 추가
|
||||
6. 우선순위 2/3 v2-* 마이그레이션
|
||||
7. VEX 화면들을 INVYONE Template 로 마이그레이션 시작
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,527 @@
|
||||
# Template 모델 재설계 — Phase 1 구현 로그 (1차)
|
||||
|
||||
**작업일**: 2026-04-10
|
||||
**작업자**: gbpark + Claude
|
||||
**기반 스펙**: `notes/gbpark/2026-04-10-template-model-redesign.md` (섹션 8~16, 19~20)
|
||||
**범위**: 스펙 섹션 19 "다음 액션" 중 **1~4번까지** (타입 재정의, CSS 변수, DashboardCard grid 전환, 빌더 grid-snap 전환)
|
||||
**남은 작업**: 5~8번 (팝업 grid 적용, 레거시 마이그레이션 다이얼로그, Template 전수 검증, Phase 3/4 문서 업데이트)
|
||||
|
||||
---
|
||||
|
||||
## 0. 결과 요약
|
||||
|
||||
| 항목 | 상태 |
|
||||
|---|---|
|
||||
| 작업 스코프 1~4번 | ✅ 완료 |
|
||||
| `npx tsc --noEmit` 작업 범위 에러 | 0건 |
|
||||
| Finding 1 (row 반응형) 코드 구조 확보 | ✅ (브라우저 시각 검증 남음) |
|
||||
| Finding 2 (공용 기하학 수식) 단위 검증 | ✅ `pixelDeltaToColDelta(200, 800, 8, 16) === 3` |
|
||||
| Finding 3 (레거시 변환 다이얼로그) | ⏸ 다음 턴 |
|
||||
| Finding 4 (drop/drag/resize 일관성) 코드 구조 확보 | ✅ (브라우저 시각 검증 남음) |
|
||||
| **원칙 3 임시 위반 잔존** | ⚠ `useBuilderState.ts:303-308` `fromTemplate` business fallback — 섹션 3.5 / 4.6 참조 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 수정/생성된 파일 목록
|
||||
|
||||
### 타입
|
||||
- **수정** `frontend/types/invyone-component.ts`
|
||||
|
||||
### 스타일
|
||||
- **수정** `frontend/styles/v5-layout.css`
|
||||
- **수정** `frontend/styles/dashboard.css`
|
||||
- **수정** `frontend/styles/developer.css`
|
||||
|
||||
### DashboardCard (대시보드 카드 → grid 렌더)
|
||||
- **수정** `frontend/components/dash/DashboardCard.tsx`
|
||||
|
||||
### Builder (빌더 grid-snap 전환)
|
||||
- **신규** `frontend/components/builder/hooks/gridMetrics.ts`
|
||||
- **수정** `frontend/components/builder/hooks/useBuilderState.ts`
|
||||
- **수정** `frontend/components/builder/hooks/useBlockDrag.ts`
|
||||
- **수정** `frontend/components/builder/BuilderCanvas.tsx`
|
||||
- **수정** `frontend/components/builder/BuilderBlock.tsx`
|
||||
- **수정** `frontend/components/builder/BuilderProps.tsx`
|
||||
- **수정** `frontend/components/builder/BuilderToolbar.tsx`
|
||||
- **수정** `frontend/components/builder/BuilderPalette.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 2. 스펙 작업 항목별 상세
|
||||
|
||||
### 2.1 타입 재정의 — 섹션 8
|
||||
|
||||
**파일**: `frontend/types/invyone-component.ts`
|
||||
|
||||
#### 추가된 타입
|
||||
- `GridPosition { col, colSpan, row?, rowSpan?, responsive? }`
|
||||
- `AbsolutePosition { x, y, w, h }`
|
||||
- `ComponentPosition = GridPosition | AbsolutePosition`
|
||||
- `ResponsiveGridOverride { narrow?, normal?, wide? }` — 각 breakpoint는 `Partial<Omit<GridPosition, 'responsive'>>`
|
||||
- `TemplateKind = 'business' | 'canvas'`
|
||||
|
||||
#### 삭제된 타입
|
||||
- 기존 `Position { x, y, w, h }` (주석은 "그리드 단위"였지만 실제는 픽셀)
|
||||
- 기존 `ResponsiveOverride { lg, md, sm }` (뷰포트 네이밍, 구현도 없었음)
|
||||
- `Component.responsive` 필드 (이제 `GridPosition.responsive` 안으로 이동)
|
||||
|
||||
#### 추가된 타입 가드
|
||||
```ts
|
||||
export function isGridPosition(pos: ComponentPosition): pos is GridPosition {
|
||||
return pos != null && typeof pos === 'object' && 'col' in pos && 'colSpan' in pos;
|
||||
}
|
||||
export function isAbsolutePosition(pos: ComponentPosition): pos is AbsolutePosition {
|
||||
return pos != null && typeof pos === 'object' && 'x' in pos && 'y' in pos && 'w' in pos && 'h' in pos;
|
||||
}
|
||||
```
|
||||
|
||||
#### `Component.position` → `ComponentPosition`
|
||||
- Template.kind에 따라 어느 쪽으로 해석되는지 주석으로 명시
|
||||
|
||||
#### `Template.kind: TemplateKind` 필수 필드 추가
|
||||
- 레이아웃 모델 분기. `business`(기본) / `canvas`(예외)
|
||||
|
||||
#### 신규 상수
|
||||
- `DEFAULT_COMPONENT_LAYOUTS: Record<ComponentType, GridPosition>` — 8종 팔레트 기본값 + responsive 디폴트. 수주관리 스펙(섹션 10.3)을 참고해 table: 8span wide / 12span narrow-normal, form: 4span wide / 12span narrow-normal 등
|
||||
- `CANVAS_KEYWORDS` — 레거시 canvas 휴리스틱 키워드 (섹션 13.3.1용, 마이그레이션 다이얼로그 구현 시 사용)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 CSS 변수 + grid 스타일 — 섹션 11.5, 12
|
||||
|
||||
#### `frontend/styles/v5-layout.css`
|
||||
`:root`에 grid 네임스페이스 변수 세트 추가:
|
||||
```css
|
||||
--grid-cols: 12;
|
||||
--grid-gap: .5rem;
|
||||
--grid-gap-narrow: .35rem;
|
||||
--grid-gap-normal: .45rem;
|
||||
--grid-gap-wide: .55rem;
|
||||
--card-narrow-max: 520px;
|
||||
--card-normal-max: 900px;
|
||||
--grid-line: rgba(108,92,231,.08);
|
||||
--grid-line-hover: rgba(108,92,231,.2);
|
||||
--grid-drop-preview: rgba(108,92,231,.15);
|
||||
--grid-drop-preview-border: rgba(108,92,231,.5);
|
||||
```
|
||||
`.dark`에도 대응하는 다크 테마 grid 토큰(보라 대신 라벤더 계열) 추가.
|
||||
v5 Cosmic 토큰(`--v5-*`)은 그대로 유지. grid 토큰은 독립 네임스페이스로만 추가.
|
||||
|
||||
#### `frontend/styles/dashboard.css`
|
||||
`.dash-card-body`에 container query 활성화:
|
||||
```css
|
||||
.dash-card-body {
|
||||
flex: 1; overflow: auto; padding: .5rem;
|
||||
container-type: inline-size;
|
||||
container-name: card;
|
||||
}
|
||||
```
|
||||
|
||||
추가된 클래스:
|
||||
- `.dash-card-error`, `.dash-card-loading` — 에러/로딩 상태
|
||||
- `.dash-card-grid` — 12-col grid (business 렌더)
|
||||
- `.dash-card-grid > .tpl-component` — **`min-width: 0` 필수** (섹션 11.6, 12열 비율 유지의 핵심)
|
||||
- `.dash-card-canvas-wrapper` / `.dash-card-canvas` / `.dash-card-canvas > .tpl-component` — absolute 렌더 (canvas kind)
|
||||
|
||||
`@container card` 쿼리 3단계:
|
||||
- `max-width: 520px` → narrow
|
||||
- `520.01px ~ 900px` → normal
|
||||
- `>900.01px` → wide
|
||||
|
||||
★ 각 쿼리에서 `grid-column` + `grid-row` **둘 다** 오버라이드. Finding 1 대응 — row 오버라이드가 실제로 반응해야 narrow에서 form이 테이블 아래로 내려감.
|
||||
|
||||
#### `frontend/styles/developer.css` — 빌더 그리드
|
||||
기존 `.dev-canvas-inner` (자유배치용, 1200×800 고정)는 유지하되, 새로 `.dev-canvas-grid` 추가:
|
||||
```css
|
||||
.dev-canvas-grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
min-height: 600px;
|
||||
container-type: inline-size;
|
||||
container-name: card;
|
||||
background-image: linear-gradient(to right, var(--grid-line) 1px, transparent 1px);
|
||||
background-size: calc((100% - 32px) / 12 + 8px) 100%;
|
||||
background-position: 16px 0;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.dev-canvas-grid.dragging { /* grid-line-hover */ }
|
||||
.dev-popup-grid { min-height: 320px; padding: 12px; }
|
||||
.dev-grid-warn { /* 비-grid 블록 잔존 시 경고 배너 */ }
|
||||
```
|
||||
|
||||
`.dev-block` 블록은 `position: absolute` → `position: relative` (grid item)으로 전환. `min-width: 0`, `min-height: 48px`, `cursor: move` 추가.
|
||||
|
||||
반응형 오버라이드 속성 패널 UI용 `.dev-resp-row`, `.dev-resp-label` 추가.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 DashboardCard grid 렌더 전환 — 섹션 11.2~11.7
|
||||
|
||||
**파일**: `frontend/components/dash/DashboardCard.tsx`
|
||||
|
||||
#### 삭제된 코드 (~40줄)
|
||||
- `canvasBounds` useMemo (캔버스 전체 박스 추정)
|
||||
- `bodyRef` / `scale` state
|
||||
- `ResizeObserver` + `transform: scale()` 계산 useEffect
|
||||
|
||||
#### 추가된 상태
|
||||
- `templateKind: TemplateKind | null` — Template 로드 시 `tpl.kind` 복원
|
||||
|
||||
#### 렌더 분기 (섹션 11.3, 11.7)
|
||||
```tsx
|
||||
{effectiveKind === 'canvas' ? (
|
||||
<div className="dash-card-canvas-wrapper">
|
||||
<div className="dash-card-canvas">
|
||||
{sortedComponents.map(c => <AbsoluteComponent ... />)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dash-card-grid">
|
||||
{sortedComponents.map(c => <GridComponent ... />)}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
`effectiveKind`는 `templateKind`을 우선하되, kind 필드가 없는 레거시는 position 형태로 보수적 추정:
|
||||
- `GridPosition` → `business`
|
||||
- 그 외 (`{x,y,w,h}` 레거시) → `canvas`
|
||||
|
||||
★ 이 추정은 **렌더 경로 선택만** 담당하고 **DB 쓰기 없음**. 섹션 13.2의 "자동 변환 금지" 원칙과 충돌하지 않음 (변환이 아니라 렌더 분기).
|
||||
|
||||
#### `GridComponent` — business kind 전용
|
||||
섹션 11.4 Finding 1 반영. **12개 CSS 변수 전부 주입**:
|
||||
```tsx
|
||||
const r = pos.responsive ?? {};
|
||||
const style: React.CSSProperties = {
|
||||
'--col': pos.col ?? 1,
|
||||
'--col-span': pos.colSpan ?? 12,
|
||||
'--row': toRowVal(pos.row), // auto 문자열 허용
|
||||
'--row-span': toSpanVal(pos.rowSpan),
|
||||
'--col-narrow': r.narrow?.col ?? pos.col ?? 1,
|
||||
'--col-span-narrow': r.narrow?.colSpan ?? pos.colSpan ?? 12,
|
||||
'--row-narrow': toRowVal(r.narrow?.row ?? pos.row),
|
||||
'--row-span-narrow': toSpanVal(r.narrow?.rowSpan ?? pos.rowSpan),
|
||||
// ... normal/wide 동일
|
||||
};
|
||||
```
|
||||
`toRowVal(undefined) === 'auto'` — CSS grid auto placement 동작.
|
||||
|
||||
#### `AbsoluteComponent` — canvas kind 전용
|
||||
`position.x/y/w/h` 그대로 `left/top/width/height`에 매핑.
|
||||
|
||||
#### `renderByType()` — 타입별 렌더 공통화
|
||||
기존 `ComponentRenderer` switch 를 `renderByType(props)`로 분리. `wrapStyle` (absolute 박스)을 없애고 각 case에서 `width: 100%; height: 100%` 기준으로 렌더.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 빌더 grid-snap 전환 — 섹션 9, 14.5
|
||||
|
||||
#### 2.4.1 `hooks/gridMetrics.ts` 신규
|
||||
|
||||
**★ drop / drag / resize 모두 반드시 이 파일만 사용. `canvasW/12` 직접 계산 금지.**
|
||||
|
||||
공용 함수:
|
||||
- `getGridMetrics(canvasWidth, gap, padding)` → `{ contentWidth, colWidth, step, padding, gap }`
|
||||
- `pixelToGridCol(pixelX, canvasWidth, gap, padding)` — 드롭 위치 계산용
|
||||
- `pixelToColSpan(pixelW, canvasWidth, gap, padding)`
|
||||
- `pixelDeltaToColDelta(pixelDelta, canvasWidth, gap, padding)` — drag/resize 증감용
|
||||
- `readCanvasGeometry(el)` → `{ canvasWidth, gap, padding }` — DOM에서 1회 읽기
|
||||
- `computeNextAvailableRow(newCol, newColSpan, blocks)` — 섹션 14.4 자동 밀기
|
||||
- `BUILDER_ROW_HEIGHT = 48` — row 이동 환산용
|
||||
|
||||
#### 2.4.2 `useBuilderState.ts`
|
||||
|
||||
새 상태 필드:
|
||||
- `templateKind: TemplateKind` (기본 `'business'`)
|
||||
- `previewWidth: 'narrow' | 'normal' | 'wide'` (기본 `'normal'`)
|
||||
|
||||
새 액션:
|
||||
- `addBlock(type: ComponentType, position: GridPosition)` — 시그니처 교체
|
||||
- `setTemplateKind(kind)`
|
||||
- `setPreviewWidth(w)`
|
||||
|
||||
삭제된 액션:
|
||||
- `moveBlock(id, x, y)`, `resizeBlock(id, w, h)` — grid에서는 `updateBlock` 에 position 객체 통째로 넘김
|
||||
|
||||
삭제된 함수:
|
||||
- `defaultSize(type)` — 픽셀 크기 테이블. `DEFAULT_COMPONENT_LAYOUTS`가 대체
|
||||
|
||||
`toTemplate()` — 반환 객체에 `kind: s.templateKind` 포함.
|
||||
|
||||
`fromTemplate(tpl)` — **⚠ 원칙 3 임시 위반 상태** (line 303~308). `tpl.kind`이 `business`/`canvas`면 그대로 수용, 없으면 `business` 강제 디폴트. 스펙 섹션 13.4 "절대 금지 패턴"(`if (!tpl.kind) tpl.kind = 'business'`)과 형식만 다를 뿐 동일 동작.
|
||||
|
||||
**주석-구현 불일치**: 주석(line 305)에는 "`isDirty=true`로 표시해 사용자가 확인 없이 저장하지 못하게 한다"고 적혔으나 실제 구현 line 325는 `isDirty: false`. 저장 차단 경로 자체가 없음.
|
||||
|
||||
DB 저장은 여전히 사용자 "저장" 클릭 필요하지만, 레거시 Template에 새 블록 추가 후 저장 시 데이터 혼합 리스크 있음. 상세는 섹션 3.5, 해소 계획은 섹션 4.6 참조.
|
||||
|
||||
#### 2.4.3 `BuilderCanvas.tsx` 재작성
|
||||
|
||||
`handleDrop` — 공용 함수 경로:
|
||||
```tsx
|
||||
const canvasEl = e.currentTarget as HTMLElement;
|
||||
const { canvasWidth, gap, padding } = readCanvasGeometry(canvasEl);
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
const relX = e.clientX - rect.left;
|
||||
const dropCol = pixelToGridCol(relX, canvasWidth, gap, padding);
|
||||
|
||||
const defaultLayout = DEFAULT_COMPONENT_LAYOUTS[type];
|
||||
const col = Math.min(dropCol, 13 - defaultLayout.colSpan);
|
||||
const colSpan = Math.min(defaultLayout.colSpan, 13 - col);
|
||||
const row = computeNextAvailableRow(col, colSpan, blocks);
|
||||
|
||||
addBlock(type, { col, colSpan, row, ...responsive });
|
||||
```
|
||||
|
||||
분기:
|
||||
- `templateKind === 'canvas'` → placeholder (자유배치 빌더는 Phase 2 이후)
|
||||
- `templateKind === 'business'` + `currentView === 'list'` → `.dev-canvas-grid` 렌더
|
||||
- `templateKind === 'business'` + 팝업 뷰 → `.dev-canvas-grid.dev-popup-grid`를 `.dev-popup-frame` 안에 렌더
|
||||
|
||||
`previewWidth`에 따라 `maxWidth` 적용 (`400 / 720 / 1100`px). `@container`가 실제로 반응.
|
||||
|
||||
비-grid 블록이 잔존하면 `.dev-grid-warn` 배너로 경고.
|
||||
|
||||
#### 2.4.4 `BuilderBlock.tsx`
|
||||
|
||||
grid item 전환:
|
||||
```tsx
|
||||
const pos = block.position as GridPosition;
|
||||
<div
|
||||
className={`dev-block${isSelected ? ' selected' : ''}`}
|
||||
style={{
|
||||
gridColumn: `${pos.col} / span ${pos.colSpan}`,
|
||||
gridRow: pos.row != null ? `${pos.row} / span ${pos.rowSpan ?? 1}` : 'auto',
|
||||
}}
|
||||
...
|
||||
>
|
||||
```
|
||||
|
||||
`isGridPosition` 아니면 `null` 반환 (canvas/legacy는 이번 단계에서 렌더 스킵).
|
||||
|
||||
드래그 label에 `col N span M · row R` 표기 추가 — 사용자 피드백용.
|
||||
|
||||
#### 2.4.5 `useBlockDrag.ts` 재작성
|
||||
|
||||
`startDrag` / `startResize` 둘 다 세션 시작 시 1회 `readCanvasGeometry` 호출해서 `canvasWidth/gap/padding`을 캐시. `onMove`에서는 `pixelDeltaToColDelta(dx, ...)`만 호출.
|
||||
|
||||
드래그:
|
||||
```ts
|
||||
const deltaCol = pixelDeltaToColDelta(dx, canvasWidth, gap, padding);
|
||||
const deltaRow = Math.round(dy / BUILDER_ROW_HEIGHT);
|
||||
const newCol = Math.max(1, Math.min(13 - cur.colSpan, origCol + deltaCol));
|
||||
const newRow = Math.max(1, origRow + deltaRow);
|
||||
updateBlock(id, { position: { ...cur, col: newCol, row: newRow } });
|
||||
```
|
||||
|
||||
리사이즈:
|
||||
```ts
|
||||
const deltaCols = pixelDeltaToColDelta(dx, canvasWidth, gap, padding);
|
||||
const newSpan = Math.max(1, Math.min(13 - cur.col, origSpan + deltaCols));
|
||||
updateBlock(id, { position: { ...cur, colSpan: newSpan } });
|
||||
```
|
||||
|
||||
★ 둘 다 `zustand.getState()`로 최신 block을 꺼내기 때문에 onMove 중 불필요한 재등록 없음.
|
||||
|
||||
#### 2.4.6 `BuilderProps.tsx`
|
||||
|
||||
위치 편집 UI를 `X/Y/W/H` → `col/colSpan/row/rowSpan`로 교체.
|
||||
|
||||
반응형 오버라이드 섹션 신규 — narrow/normal/wide 각 breakpoint마다 `col/span/row/rowSpan` 4칸. 빈 값은 `undefined`(= 기본값 fallback).
|
||||
|
||||
`isGridPosition` 아니면 "grid 위치가 아닙니다" 안내.
|
||||
|
||||
#### 2.4.7 `BuilderToolbar.tsx`
|
||||
|
||||
새 툴바 그룹 2개:
|
||||
1. **모델** — `business` / `canvas` 토글 → `setTemplateKind`
|
||||
2. **미리보기** — `narrow 400` / `보통 720` / `넓음 1100` 토글 → `setPreviewWidth`. `templateKind === 'business'`일 때만 표시
|
||||
|
||||
`handleSave` 페이로드에 `kind: tpl.kind` 포함.
|
||||
|
||||
#### 2.4.8 `BuilderPalette.tsx`
|
||||
|
||||
클릭으로 추가 시에도 `DEFAULT_COMPONENT_LAYOUTS` + `computeNextAvailableRow` 사용. 기존 `{x:16, y:16, w:0, h:0}` 제거.
|
||||
|
||||
---
|
||||
|
||||
## 3. 검증 결과
|
||||
|
||||
### 3.1 타입체크
|
||||
```
|
||||
$ npx tsc --noEmit | grep -E "invyone-component|components/builder|components/dash/DashboardCard|components/dash/CardSettings"
|
||||
(0 matches)
|
||||
```
|
||||
내 작업 범위에서 에러 **0건**.
|
||||
|
||||
전체 프로젝트 타입체크는 VEX 레거시(screen-management, v2-*, dataflow, admin dashboard widgets 등)에서 기존부터 누적된 ~2800건 에러가 있으나 이번 작업과 무관.
|
||||
|
||||
### 3.2 Finding 2 단위 검증
|
||||
```js
|
||||
// getGridMetrics(800, 8, 16)
|
||||
// content = 800 - 32 = 768
|
||||
// totalGap = 88
|
||||
// colWidth = (768 - 88) / 12 = 56.666...
|
||||
// step = 64.666...
|
||||
// 200 / step = 3.093
|
||||
// round → 3
|
||||
|
||||
pixelDeltaToColDelta(200, 800, 8, 16) === 3 ✅
|
||||
```
|
||||
|
||||
### 3.3 Finding 1 구조 확보
|
||||
- `GridComponent`에서 `--row*`, `--row-span*` 8개 변수 주입 확인
|
||||
- `@container card` 3개 쿼리 모두 `grid-row: var(--row-*) / span var(--row-span-*)` 라인 포함
|
||||
- 브라우저 시각 검증은 사용자 몫 (수주관리 예시를 400px/1000px 카드에서 렌더했을 때 form 위치)
|
||||
|
||||
### 3.4 Finding 4 구조 확보
|
||||
- `handleDrop`, `startDrag`, `startResize` 전부 `readCanvasGeometry` → `pixelToGridCol` / `pixelDeltaToColDelta` 경로
|
||||
- `canvasW / 12` 직접 계산 grep 결과: 0건
|
||||
|
||||
### 3.5 Finding 3 — ⚠ 원칙 위반 잔존 (완료 아님)
|
||||
|
||||
스펙 섹션 13.4 "절대 금지 패턴"이 코드에 남아 있는 상태. 다음 턴 6번으로 해소 이월.
|
||||
|
||||
#### 위반 위치
|
||||
|
||||
**파일**: `frontend/components/builder/hooks/useBuilderState.ts`
|
||||
**함수**: `fromTemplate(tpl)` (line 303~327)
|
||||
**핵심 라인**: 307~308
|
||||
|
||||
```ts
|
||||
const kind: TemplateKind =
|
||||
tpl.kind === "business" || tpl.kind === "canvas" ? tpl.kind : "business";
|
||||
```
|
||||
|
||||
스펙 섹션 13.4 "절대 금지 패턴"의 `if (!tpl.kind) tpl.kind = 'business';` 와 형식만 삼항으로 바꾼 동일 동작.
|
||||
|
||||
#### 주석-구현 불일치
|
||||
|
||||
line 303~306 주석:
|
||||
```
|
||||
// kind 는 호출자(빌더 진입점)가 다이얼로그로 확정한 뒤 넘겨야 함.
|
||||
// 여기서는 tpl.kind 를 그대로 받고, 없으면 기본 business 로 두되 isDirty=true
|
||||
// 로 표시해 사용자가 확인 없이 저장하지 못하게 한다.
|
||||
```
|
||||
|
||||
그러나 실제 line 325는 `isDirty: false`. **저장 차단 의도를 구현에 반영하지 못함.** 주석과 구현 중 어느 쪽이 정답인지 불분명 — 다음 턴에서 다이얼로그 도입으로 이 문제 자체를 없애는 것이 맞음.
|
||||
|
||||
#### 현 시점 완화 요소 (사고 확률을 낮추는 요인)
|
||||
|
||||
1. **DB 자동 저장 경로 없음** — 사용자가 "저장" 버튼을 명시적으로 클릭해야만 반영
|
||||
2. **빌더에서 레거시 블록 렌더 안 됨** — `BuilderBlock`이 `isGridPosition` 가드로 레거시 `{x,y,w,h}` 블록을 `null` 반환. 사용자 화면에 안 보이는 상태에서는 "저장" 누를 동기가 적음
|
||||
3. **대시보드는 `effectiveKind` 추정** — position 형태로 business/canvas 분기. 레거시는 canvas fallback → absolute 렌더, 데이터 손상은 없음
|
||||
|
||||
#### 실제 리스크 시나리오
|
||||
|
||||
1. 사용자가 기존 `{x,y,w,h}` 기반 레거시 Template을 빌더에서 열기 → `fromTemplate`가 `kind: 'business'` 강제 부여, `isDirty: false`
|
||||
2. 사용자가 "화면이 비었네" 하고 새 컴포넌트를 palette에서 drop → `blocks.list`에 grid 블록이 추가되면서 기존 레거시 `{x,y,w,h}` 블록들과 혼재
|
||||
3. 사용자가 "저장" 클릭 → `toTemplate`가 `kind: 'business'` + 혼합 blocks 배열을 DB에 기록
|
||||
4. **결과**: 원본 레거시 데이터가 business kind로 덮여쓰임, grid + absolute 포맷 혼합 상태. `migration.backup` 미적용이라 롤백 어려움
|
||||
|
||||
#### 다음 턴 해소 방법 (섹션 4.6 ★0순위)
|
||||
|
||||
1. `fromTemplate` 시그니처에 `kind: TemplateKind`를 **필수 파라미터**로 추가 — 호출자가 다이얼로그로 결정한 값만 넘김
|
||||
2. 빌더 진입점(`BuilderLayout` 또는 `TemplateLibraryModal`)에 `openTemplate(templateId)` 도입 — 내부에서 `isLikelyCanvasTemplate` 휴리스틱 + 블로킹 다이얼로그
|
||||
3. 사용자가 `business` 선택 시에만 `migrateTemplateAbsoluteToGrid(tpl)` + `migration.backup` 적용 후 `fromTemplate(migrated, 'business')` 호출
|
||||
4. 사용자가 `canvas` 선택 시 `fromTemplate({...tpl, kind: 'canvas'}, 'canvas')` 호출 — 위치 유지
|
||||
5. `fromTemplate` 내부의 `'business'` 강제 디폴트 삭제
|
||||
|
||||
#### 임시 부분 수정 하지 않는 이유
|
||||
|
||||
"`isDirty: true`만 고치면 되지 않냐" — **권장하지 않음**:
|
||||
- `isDirty` 플래그만으로는 저장 차단 확실하지 않음 (다른 경로로 isDirty 초기화될 수 있음)
|
||||
- `fromTemplate` 호출 경로는 빌더 진입 외에도 import/template library 등 여러 곳 → 부분 수정은 회귀 위험
|
||||
- 다음 턴에서 정공법으로 한 번에 처리하는 것이 안전. 이번 턴 완료 보고는 **이 섹션으로 상태 명시**만 수행
|
||||
|
||||
---
|
||||
|
||||
## 4. 다음 턴에 해야 할 것 (스펙 5~8번)
|
||||
|
||||
### 5. 등록/수정 팝업 grid 적용 — 섹션 15
|
||||
- `ViewConfig.size` 활용
|
||||
- 팝업도 `.dev-canvas-grid` 사용, `container-name: popup` 분기는 필요 시
|
||||
- 현재 `BuilderCanvas`에서 `dev-popup-grid`로 1차 적용 해둠 — 스펙 대로 정비 필요
|
||||
- `FormConfig.columns` 는 폼 내부 필드 배치용으로 유지 (2단계 구조)
|
||||
|
||||
### 6. Template 마이그레이션 + 승인 다이얼로그 — 섹션 13 ★★ 최우선 (0순위)
|
||||
|
||||
**★★★ 가장 먼저 해야 할 것 — `fromTemplate` 임시 위반 제거** (상세: 섹션 3.5):
|
||||
`useBuilderState.ts:307-308`의 `tpl.kind ?? 'business'` 폴백 삭제. `fromTemplate` 시그니처에 `kind: TemplateKind`를 **필수 파라미터**로 추가. 주석의 `isDirty=true` 의도도 함께 정리 (다이얼로그가 생기면 이 주석 자체가 불필요해짐).
|
||||
|
||||
이어지는 구현 순서:
|
||||
|
||||
1. `isLikelyCanvasTemplate(oldTpl)` 휴리스틱 (키워드 / y 분포 / controlFlow 필드 — 스펙 섹션 13.3.1)
|
||||
2. `migrateTemplateAbsoluteToGrid(oldTpl)` — `migration.backup` 원본 보존 (스펙 섹션 13.3.2)
|
||||
3. `openTemplate(templateId)` 블로킹 다이얼로그 (스펙 섹션 13.5)
|
||||
4. 빌더 진입점(`BuilderLayout` / `TemplateLibraryModal`)에서 `openTemplate` → 다이얼로그 → 확정된 `kind`로 `fromTemplate(tpl, kind)` 호출 체인 구성
|
||||
5. **절대 금지 패턴 재확인**: `if (!tpl.kind) tpl.kind = 'business'` 코드 어디에도 두지 말 것
|
||||
|
||||
**순서 제안**: 5번(팝업) 전에 **6번부터 먼저 처리** — 원칙 위반 잔존 시간을 최소화. 5번 작업 중 사용자가 레거시 Template을 건드려 데이터가 망가지면 뒤늦게 후회.
|
||||
|
||||
### 7. 기존 Template 전수 검증
|
||||
- DB에 있는 Template 목록을 빌더에서 하나씩 열어서 다이얼로그 동작 확인
|
||||
- canvas 디폴트 선택이 맞는지, 취소 시 DB 무변경, business 선택 시만 변환되고 `migration.backup` 저장되는지
|
||||
|
||||
### 8. Phase 3/4 문서 업데이트 — 섹션 16
|
||||
- `notes/gbpark/2026-04-10-phase3-developer-builder.md` — 섹션 16.1 표대로 grid 기준으로 갱신
|
||||
- `notes/gbpark/2026-04-10-phase4-dashboard-menu.md` — 섹션 16.2 표대로 `@container` 기준으로 갱신 + 2레이어 명확화 블록 추가
|
||||
|
||||
### 추가로 남은 사소한 것 (우선순위 낮음)
|
||||
- 빌더 `.dev-canvas-grid.dragging` 클래스 on/off 시각 피드백
|
||||
- `DropPreview` 컴포넌트 (섹션 14.2) — 드래그 중 가장 가까운 셀 프리뷰
|
||||
- Canvas kind 빌더 실제 UX (현재는 placeholder)
|
||||
|
||||
---
|
||||
|
||||
## 5. 브라우저 실측 체크리스트 (사용자 몫)
|
||||
|
||||
### Finding 1 — 반응형 row 오버라이드
|
||||
1. 빌더에서 `kind: business` 로 새 Template 만들기
|
||||
2. 수주관리 패턴 배치:
|
||||
- search: col 1 span 12 row 1
|
||||
- table: col 1 span 8 row 2 / narrow,normal span 12
|
||||
- form: col 9 span 4 row 2 / narrow,normal col 1 span 12 row 3
|
||||
3. 저장 후 대시보드 카드에 꽂고 카드 크기 리사이즈
|
||||
4. 400px 폭에서 form이 테이블 **아래** row 3 위치에 오는지 확인
|
||||
5. 1000px 폭에서 form이 오른쪽 col 9~12에 오는지 확인
|
||||
6. 겹치면 Finding 1 버그 재발 → 즉시 보고
|
||||
|
||||
### Finding 4 — drop/drag/resize 스냅 일치
|
||||
1. 빌더에서 캔버스 배경 그리드 라인이 12줄로 보이는지
|
||||
2. 팔레트 table 아이템을 정확히 같은 위치에 세 번 drop → 매번 같은 col/colSpan 인지 label 확인
|
||||
3. 한 블록을 drag로 몇 칸 이동 → col 값이 gap을 고려한 정확한 값인지
|
||||
4. 한 블록을 resize로 확장 → colSpan이 drop/drag와 같은 기준으로 증감하는지
|
||||
|
||||
### 기타
|
||||
- **기존 DB Template 열기**: 현재 `{x,y,w,h}` 기반이므로 빌더에서 열면 `fromTemplate`의 default `business`로 들어감 → grid가 아니어서 BuilderBlock이 null 반환 → 화면이 비어 보일 수 있음. **레거시 변환 다이얼로그는 다음 턴**에서 구현. 당장 테스트는 **새 Template**으로 진행 권장.
|
||||
- 대시보드 카드에서 kind 없는 레거시 Template 을 꽂으면 `effectiveKind === 'canvas'`로 fallback → absolute 그대로 렌더 (기존 동작과 다름 없음, scale은 빠졌으니 카드 밖으로 넘칠 수 있음 → narrow 카드에서는 스크롤)
|
||||
|
||||
---
|
||||
|
||||
## 6. 참조
|
||||
|
||||
### 원본 스펙
|
||||
- `notes/gbpark/2026-04-10-template-model-redesign.md` — 섹션 0~20 전체
|
||||
|
||||
### 관련 메모리
|
||||
- `memory/project_template_builder.md` — 2026-04-10 재설계 반영
|
||||
- `memory/feedback_responsive_container_based.md` — @container 원칙
|
||||
- `memory/project_component_spec.md` — FieldConfig 규격
|
||||
|
||||
### 주요 코드 위치
|
||||
- 타입 정의: `frontend/types/invyone-component.ts`
|
||||
- Grid 공용 수식: `frontend/components/builder/hooks/gridMetrics.ts`
|
||||
- 카드 렌더: `frontend/components/dash/DashboardCard.tsx`
|
||||
- 빌더 캔버스: `frontend/components/builder/BuilderCanvas.tsx`
|
||||
- 빌더 상태: `frontend/components/builder/hooks/useBuilderState.ts`
|
||||
- 빌더 드래그: `frontend/components/builder/hooks/useBlockDrag.ts`
|
||||
- 빌더 속성 패널: `frontend/components/builder/BuilderProps.tsx`
|
||||
- 빌더 툴바: `frontend/components/builder/BuilderToolbar.tsx`
|
||||
|
||||
### v5 디자인 토큰
|
||||
- `frontend/styles/v5-layout.css` — v5 Cosmic 토큰 + grid 네임스페이스 변수
|
||||
- `frontend/styles/dashboard.css` — `.dash-card-grid`, `@container` 쿼리
|
||||
- `frontend/styles/developer.css` — `.dev-canvas-grid`, 빌더 block/props 스타일
|
||||
Reference in New Issue
Block a user