# Phase 3: 개발자 빌더 — 수동 템플릿 구성

> **목적**: 개발자가 테이블을 선택하고, FieldConfig 기반 컴포넌트를 배치하여 Template JSON을 만드는 빌더 UI 구현
> **전제 조건**: Phase 1 (DB 메타 API) + Phase 2 (FcTable/FcForm/FcSearch 컴포넌트) 완료
> **산출물**: 개발자 빌더 페이지 + Template CRUD API + Template JSON 저장/로드
> **다음 단계**: Phase 4에서 사용자가 이 Template을 대시보드(=메뉴)에 카드로 배치

---

## 1. 핵심 흐름

```
개발자 모드 진입 (톱니바퀴 버튼)
  → 테이블 선택 (드롭다운)
  → FieldConfig[] 자동 로드 (Phase 1 API)
  → 팔레트에서 컴포넌트 선택 → 캔버스에 배치
  → 속성 패널에서 설정 조정 (필드 ON/OFF, 컬럼 순서, config 등)
  → 3뷰 탭 전환 (목록/등록/수정)
  → 저장 → Template JSON이 DB에 저장됨
```

**자동생성(⚡ 버튼)과 프리셋은 Phase 6에서 구현. 이 단계에서는 수동 구성만.**

---

## 2. 현재 존재하는 것

| 파일 | 상태 | 활용 |
|---|---|---|
| `notes/gbpark/2026-04-08-invyone-mockup/js/08-admin-builder.js` | **mockup 완성** | UI/UX 참조 (진실의 원천) |
| `notes/gbpark/2026-04-08-invyone-mockup/index.html` (admin-builder 섹션) | **mockup 완성** | HTML 구조 참조 |
| `notes/gbpark/2026-04-08-invyone-mockup/css/09-developer.css` | **mockup 완성** | 개발자 모드 스타일 참조 |
| `frontend/types/invyone-component.ts` | **타입 완성** | Template, Component, ViewConfig 등 |
| `frontend/components/screen/ScreenDesigner.tsx` | 7,986줄 (VEX식) | 패턴 참고용, 직접 사용 안 함 |
| `backend-spring/.../TemplateStandardController.java` | 기존 VEX 템플릿 API | 확장 또는 신규 생성 |

---

## 3. 빌더 UI 레이아웃

mockup의 `developer.html` + `08-admin-builder.js` 기반. 3패널 IDE 스타일.

```
┌─────────────────────────────────────────────────────────┐
│ 헤더: [INVYONE] [DEV] [테이블 선택 ▼] [뷰 탭: 목록|등록|수정] [저장] [미리보기]  │
├──────────┬────────────────────────────┬──────────────────┤
│ 팔레트    │ 캔버스                      │ 속성 패널         │
│ (180px)  │ (유동)                      │ (260px)          │
│          │                            │                  │
│ ── 데이터 │ ┌──────────────────────┐   │ 컴포넌트 정보     │
│ 📊 테이블 │ │ [검색 필터]           │   │ ─────────────    │
│ 🔍 검색   │ │                      │   │ 종류: 테이블      │
│ 📝 폼    │ │ [데이터 테이블]        │   │ 이름: 수주 목록   │
│          │ │                      │   │                  │
│ ── 액션   │ │                      │   │ 위치·크기         │
│ 🔘 버튼   │ └──────────────────────┘   │ X: 16  Y: 58    │
│          │ ┌──────────────────────┐   │ W: 854 H: 380   │
│ ── 표시   │ │ [페이지네이션]        │   │                  │
│ 📌 제목   │ └──────────────────────┘   │ 표시할 컬럼       │
│ ── 구분   │                            │ ☑ 수주번호        │
│                                        │ ☑ 수주일          │
│ 빈 슬롯   │                            │ ☑ 거래처          │
│          │                            │ ☐ 비고            │
├──────────┴────────────────────────────┴──────────────────┤
│ 상태바: 블록 5개 · order_management_test · 수정됨           │
└─────────────────────────────────────────────────────────┘
```

### 3.1 팔레트 (좌측 180px)

**8종 컴포넌트** (아키텍처 문서 Section 3.2 확정):

| 아이콘 | 이름 | ComponentType | 드래그→캔버스 |
|---|---|---|---|
| 📊 | 데이터 테이블 | `table` | FcTable 배치 |
| 🔍 | 검색 필터 | `search` | FcSearch 배치 |
| 📝 | 입력 폼 | `form` | FcForm 배치 |
| 🔘 | 버튼 | `button` | FcButton 배치 |
| 📈 | 통계 카드 | `stats` | 통계 카드 배치 |
| 📌 | 제목/텍스트 | `title` | 텍스트 배치 |
| ── | 구분선 | `divider` | 구분선 배치 |
| 📊 | 차트 | `chart` | (Phase 2+에서 확장) |

드래그앤드롭으로 캔버스에 추가. 팔레트 아이템은 mockup의 `dev-pal-item` 스타일 참고.

### 3.2 캔버스 (중앙)

- 절대 좌표 배치 (position: absolute)
- 블록(컴포넌트)은 점선 보더 + 라벨
- 블록 클릭 → 선택 (파란 보더)
- 블록 드래그 → 이동 (Shift = 8px 스냅)
- 블록 우하단 핸들 → 리사이즈
- 블록 안에 Phase 2의 FcTable/FcForm/FcSearch가 프리뷰로 렌더됨

### 3.3 속성 패널 (우측 260px)

블록 선택 시 해당 컴포넌트의 설정을 편집:

**공통 속성:**
- 컴포넌트 종류 (readonly)
- 이름 (label)
- 위치: X, Y, W, H (숫자 입력, 캔버스와 양방향 동기화)

**타입별 속성 (invyone-component.ts의 Config 타입):**

| ComponentType | 설정 항목 |
|---|---|
| `table` | 표시할 컬럼 체크리스트, 페이지 크기, 선택 모드, 인라인 편집, 자동 로드, 정렬 |
| `form` | 표시할 필드 체크리스트, 컬럼 수(1/2/3), 섹션 구분, 저장 방식(INSERT/UPDATE/UPSERT) |
| `search` | 검색 대상 필드 체크리스트, 날짜 범위, 초기화 버튼, 자동 검색 |
| `button` | 버튼 텍스트, 액션 종류(12종), 스타일(5종), 확인 메시지 |
| `title` | 텍스트, 글꼴 크기, 글꼴 두께, 정렬 |
| `stats` | 통계 항목 목록 (라벨, 컬럼, 집계 방식) |
| `divider` | 선 스타일 (solid/dashed/dotted) |

**필드 체크리스트** (table/form/search 공통):
- 현재 선택된 테이블의 FieldConfig[] 표시
- 체크박스로 visible ON/OFF
- 드래그로 순서 변경
- 클릭하면 필드 상세 설정 열림 (label, width, required 등)

### 3.4 3뷰 탭

헤더에 `[목록 | 등록 | 수정]` 탭:

- **목록 (list)**: 검색 + 테이블 + 버튼바 + 페이지네이션
- **등록 (create)**: 팝업 오버레이 안에 폼 + 버튼
- **수정 (edit)**: 등록과 비슷, `extends: 'create'` 가능

목록은 캔버스 직접 배치, 등록/수정은 팝업 프레임 안에서 배치.

mockup 참조: `08-admin-builder.js`의 `abSwitchView()`, `.ab-popup-overlay` 구조.

---

## 4. Template JSON 구조

`frontend/types/invyone-component.ts`의 `Template` 인터페이스 그대로:

```typescript
interface Template {
  templateId: string;
  name: string;
  category: string;
  description?: string;
  primaryTable: string;
  fields: FieldConfig[];        // 모든 뷰가 공유하는 유일한 필드 정의
  views: {
    list: ViewConfig;           // { components: Component[] }
    create: ViewConfig;
    edit: ViewConfig;           // extends: 'create' 가능
  };
  connections: Connection[];    // DataPort 연결 (Phase 2의 DataPort)
  companyCode: string;
  version: number;
  status: 'draft' | 'published';
  createdAt: string;
  updatedAt: string;
}
```

---

## 5. 백엔드 API 설계

### 5.1 Template CRUD

**`GET /api/templates`** — 템플릿 목록
```
Response: { data: [{ templateId, name, category, primaryTable, status, updatedAt }] }
```

**`GET /api/templates/{templateId}`** — 템플릿 상세 (Template JSON 전체)
```
Response: { data: Template }
```

**`POST /api/templates`** — 템플릿 생성
```
Request:  Template (templateId 제외, 서버에서 생성)
Response: { data: { templateId: "tpl_xxx" } }
```

**`PUT /api/templates/{templateId}`** — 템플릿 수정
```
Request:  Template (전체)
Response: { data: Template }
```

**`DELETE /api/templates/{templateId}`** — 템플릿 삭제

**`PUT /api/templates/{templateId}/publish`** — 템플릿 게시 (draft → published)

### 5.2 DB 테이블

**`templates` 테이블 (신규 생성):**

```sql
CREATE TABLE templates (
    template_id     VARCHAR(50) PRIMARY KEY,
    name            VARCHAR(200) NOT NULL,
    category        VARCHAR(50),
    description     TEXT,
    primary_table   VARCHAR(100) NOT NULL,
    fields          JSONB NOT NULL,         -- FieldConfig[] JSON
    views           JSONB NOT NULL,         -- { list, create, edit } JSON
    connections     JSONB DEFAULT '[]',     -- Connection[] JSON
    company_code    VARCHAR(20) NOT NULL DEFAULT '*',
    version         INTEGER DEFAULT 1,
    status          VARCHAR(20) DEFAULT 'draft',  -- draft | published
    created_by      VARCHAR(50),
    created_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_by      VARCHAR(50),
    updated_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_active       VARCHAR(1) DEFAULT 'Y'
);

CREATE INDEX idx_templates_company ON templates(company_code);
CREATE INDEX idx_templates_table ON templates(primary_table);
CREATE INDEX idx_templates_status ON templates(status);
```

### 5.3 Java 구현

**★ 패턴: 전부 BaseService + Map<String, Object>. 엔티티 클래스/DTO 없음.**

**신규 파일:**

| 파일 | 역할 |
|---|---|
| `TemplateController.java` | `/api/templates/*` 엔드포인트 |
| `TemplateService.java` | Template CRUD + 버전 관리 |
| `TemplateMapper.xml` | MyBatis SQL (JSONB 읽기/쓰기) |

**주의:** 기존 `TemplateStandardController`는 VEX용이므로 별도로 신규 생성. 이름 충돌 방지.

**TemplateService.java 구조:**

```java
@Service
@Slf4j
public class TemplateService extends BaseService {

    @Autowired
    private CommonService commonService;

    public Map<String, Object> getTemplateList(Map<String, Object> params) {
        commonService.applyPagination(params);
        int totalCount = sqlSession.selectOne("template.getTemplateListCnt", params);
        List<Map<String, Object>> list = sqlSession.selectList("template.getTemplateList", params);
        return commonService.buildListResponse(list, totalCount, params);
    }

    public Map<String, Object> getTemplateInfo(Map<String, Object> params) {
        return sqlSession.selectOne("template.getTemplateInfo", params);
    }

    @Transactional
    public Map<String, Object> insertTemplate(Map<String, Object> params) {
        // template_id 자동 생성 (UUID)
        // fields, views, connections → ObjectMapper로 JSON 문자열 변환 후 #{fields}::jsonb
        sqlSession.insert("template.insertTemplate", params);
        return params;
    }

    @Transactional
    public void updateTemplate(Map<String, Object> params) {
        sqlSession.update("template.updateTemplate", params);
    }

    @Transactional
    public int deleteTemplate(Map<String, Object> params) {
        return sqlSession.update("template.deleteTemplate", params);
    }
}
```

**★ 파일명: `template.xml`, namespace: `template` (덕일 스타일)**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="template">

  <select id="getTemplateList" parameterType="map" resultType="map">
    SELECT
        TEMPLATE_ID
        , NAME
        , CATEGORY
        , PRIMARY_TABLE
        , STATUS
        , UPDATED_DATE
    FROM TEMPLATES
    WHERE 1=1
      AND IS_ACTIVE != 'D'
    <include refid="common.companyCodeFilter"/>
    <include refid="common.dynamicOrderBy"/>
    <include refid="common.pagination"/>
  </select>

  <select id="getTemplateListCnt" parameterType="map" resultType="int">
    SELECT COUNT(*)
    FROM TEMPLATES
    WHERE 1=1
      AND IS_ACTIVE != 'D'
    <include refid="common.companyCodeFilter"/>
  </select>

  <select id="getTemplateInfo" parameterType="map" resultType="map">
    SELECT
        TEMPLATE_ID
        , NAME
        , CATEGORY
        , DESCRIPTION
        , PRIMARY_TABLE
        , FIELDS
        , VIEWS
        , CONNECTIONS
        , COMPANY_CODE
        , VERSION
        , STATUS
        , CREATED_BY
        , CREATED_DATE
        , UPDATED_BY
        , UPDATED_DATE
    FROM TEMPLATES
    WHERE TEMPLATE_ID = #{template_id}
      AND IS_ACTIVE != 'D'
  </select>

  <insert id="insertTemplate" parameterType="map">
    INSERT INTO TEMPLATES (
        TEMPLATE_ID
        , NAME
        , CATEGORY
        , DESCRIPTION
        , PRIMARY_TABLE
        , FIELDS
        , VIEWS
        , CONNECTIONS
        , COMPANY_CODE
        , VERSION
        , STATUS
        , CREATED_BY
        , CREATED_DATE
    ) VALUES (
        #{template_id}
        , #{name}
        , #{category}
        , #{description}
        , #{primary_table}
        , #{fields}::jsonb
        , #{views}::jsonb
        , #{connections}::jsonb
        , #{company_code}
        , 1
        , 'draft'
        , #{user_id}
        , NOW()
    )
  </insert>

  <update id="updateTemplate" parameterType="map">
    UPDATE TEMPLATES
    SET
        NAME = #{name}
        , CATEGORY = #{category}
        , DESCRIPTION = #{description}
        , FIELDS = #{fields}::jsonb
        , VIEWS = #{views}::jsonb
        , CONNECTIONS = #{connections}::jsonb
        , VERSION = VERSION + 1
        , UPDATED_BY = #{user_id}
        , UPDATED_DATE = NOW()
    WHERE TEMPLATE_ID = #{template_id}
  </update>

  <update id="deleteTemplate" parameterType="map">
    UPDATE TEMPLATES
    SET IS_ACTIVE = 'D'
        , UPDATED_BY = #{user_id}
        , UPDATED_DATE = NOW()
    WHERE TEMPLATE_ID = #{template_id}
  </update>

</mapper>
```

---

## 6. 프론트엔드 구현

### 6.1 파일 구조

```
frontend/app/(main)/admin/builder/
  └── page.tsx                    ← 빌더 메인 페이지

frontend/components/builder/
  ├── BuilderLayout.tsx           ← 3패널 레이아웃
  ├── BuilderPalette.tsx          ← 좌측 팔레트 (8종 컴포넌트)
  ├── BuilderCanvas.tsx           ← 중앙 캔버스 (블록 배치/드래그/리사이즈)
  ├── BuilderProps.tsx            ← 우측 속성 패널
  ├── BuilderToolbar.tsx          ← 상단 툴바 (테이블 선택, 뷰 탭, 저장/미리보기)
  ├── BuilderBlock.tsx            ← 캔버스 위의 개별 블록
  ├── BuilderViewTabs.tsx         ← 3뷰 탭 (목록/등록/수정)
  ├── BuilderPopupFrame.tsx       ← 등록/수정 팝업 편집 프레임
  ├── props/                      ← 타입별 속성 패널
  │   ├── TableProps.tsx
  │   ├── FormProps.tsx
  │   ├── SearchProps.tsx
  │   ├── ButtonProps.tsx
  │   ├── TitleProps.tsx
  │   ├── StatsProps.tsx
  │   └── FieldListEditor.tsx     ← 필드 체크리스트 (공통)
  └── hooks/
      ├── useBuilderState.ts      ← 빌더 상태 관리 (Zustand)
      └── useBlockDrag.ts         ← 블록 드래그/리사이즈 로직

frontend/lib/api/template.ts      ← Template CRUD API 클라이언트
```

### 6.2 빌더 상태 (Zustand)

```typescript
interface BuilderState {
  // 테이블/필드
  tableName: string | null;
  fields: FieldConfig[];

  // 현재 뷰
  currentView: 'list' | 'create' | 'edit';

  // 블록 목록 (뷰별)
  blocks: {
    list: Component[];
    create: Component[];
    edit: Component[];
  };

  // 선택된 블록
  selectedBlockId: string | null;

  // 템플릿 메타
  templateId: string | null;
  templateName: string;
  category: string;

  // 변경 상태
  isDirty: boolean;

  // 액션
  setTable: (tableName: string, fields: FieldConfig[]) => void;
  switchView: (view: 'list' | 'create' | 'edit') => void;
  addBlock: (type: ComponentType, position: Position) => void;
  removeBlock: (id: string) => void;
  updateBlock: (id: string, updates: Partial<Component>) => void;
  selectBlock: (id: string | null) => void;
  moveBlock: (id: string, x: number, y: number) => void;
  resizeBlock: (id: string, w: number, h: number) => void;
  toTemplate: () => Template;          // 현재 상태 → Template JSON
  fromTemplate: (tpl: Template) => void; // Template JSON → 상태 로드
}
```

---

## 7. 스타일

개발자 모드는 **코스믹 글래스모피즘이 아닌 IDE 스타일** (mockup의 `09-developer.css` 참조):
- 중성 다크 그레이 (#121218, #1a1a22)
- 라이트 모드도 지원 (#f5f5f8, #ededf2)
- 글로우/블러 없음, 깔끔한 보더
- 액센트: 블루 (#5b9ef5)
- 폰트: 0.42~0.62rem (컴팩트)

---

## 8. 참고 파일

| 파일 | 용도 |
|---|---|
| `notes/gbpark/2026-04-08-invyone-mockup/js/08-admin-builder.js` | **빌더 UX 진실의 원천** — 드래그, 리사이즈, 속성 패널, 3뷰 전부 구현됨 |
| `notes/gbpark/2026-04-08-invyone-mockup/css/09-developer.css` | **개발자 모드 스타일** |
| `notes/gbpark/2026-04-08-invyone-mockup/developer.html` | 개발자 모드 HTML 구조 |
| `notes/gbpark/2026-04-08-invyone-mockup/builder-v2.html` | 빌더 v2 standalone |
| `frontend/types/invyone-component.ts` | Template, Component, Config 타입 |
| `notes/gbpark/2026-04-09-invyone-architecture.md` | 팔레트 8종, DataPort, 3뷰 구조 |

---

## 9. Template fields 동기화 규칙 (★ 메타 드리프트 방지)

Template.fields는 저장 시점의 FieldConfig[] 스냅샷이다. DB 스키마(table_type_columns)가 나중에 바뀌면 드리프트가 발생한다.

**동기화 정책:**

1. **빌더에서 Template 열 때마다** Phase 1 API(`GET /api/meta/tables/{tableName}/fields`)를 호출하여 최신 메타와 비교
2. **비교 결과 표시**:
   - 새 컬럼 추가됨 → 빌더 상단에 "N개 컬럼 추가됨" 알림 + 필드 목록에 `(신규)` 표시
   - 기존 컬럼 삭제됨 → 해당 필드에 `(DB에서 삭제됨)` 경고 표시
   - 타입/옵션 변경됨 → 해당 필드에 `(변경됨)` 표시
3. **동기화 버튼** ("메타 동기화") 클릭 시:
   - 새 컬럼 → fields에 추가 (visible=false 기본)
   - 삭제된 컬럼 → fields에서 제거 (연관 사용자 오버라이드도 자동 무시됨)
   - 변경된 타입 → fields 업데이트
4. **자동 동기화는 안 함** — 개발자가 명시적으로 "동기화" 눌러야 적용 (의도치 않은 변경 방지)
5. **사용자 화면(Phase 4)에서는** Template.fields 스냅샷을 그대로 사용 (라이브 메타 조회 안 함 — 성능 + 안정성)

---

## 10. 완료 기준

1. `/admin/builder` 페이지에서 3패널 빌더가 동작한다
2. 드롭다운에서 테이블 선택 → FieldConfig[] 로드 → 팔레트 활성화
3. 팔레트에서 컴포넌트를 캔버스에 드래그앤드롭으로 추가할 수 있다
4. 캔버스에서 블록을 드래그(이동)/리사이즈할 수 있다
5. 블록 클릭 → 속성 패널에 해당 Config가 표시되고, 변경 시 캔버스에 반영된다
6. 필드 체크리스트에서 ON/OFF → 프리뷰에 실시간 반영
7. 3뷰 탭 (목록/등록/수정) 전환이 동작한다
8. 저장 → Template JSON이 DB에 저장된다
9. 기존 Template 로드 → 빌더에 상태 복원
10. 실제 테이블 (예: order_management_test)로 템플릿을 만들고 저장/로드 확인

---

## 10. 다음 단계 연결

Phase 4 (대시보드=메뉴)에서는:
- 사용자가 대시보드를 만들면 → 사이드바 메뉴에 등록됨
- 대시보드에 Template을 카드로 배치 → 카드 안에서 Phase 2의 FcTable/FcForm/FcSearch가 렌더됨
- 개발자가 만든 Template이 "사용 가능한 템플릿 라이브러리"에 나타남
