# Phase 4: 대시보드(=메뉴) — 사용자 화면 시스템

> **목적**: 사용자가 대시보드(=메뉴)를 만들고, 개발자가 만든 Template을 카드로 배치하여 업무 화면을 완성하는 시스템 구현
> **전제 조건**: Phase 1~3 완료 (FieldConfig API + 규격 컴포넌트 + 개발자 빌더에서 Template 생성 가능)
> **산출물**: 대시보드 CRUD + 사이드바 메뉴 자동 등록 + 템플릿 카드 배치 캔버스 + 사용자 오버라이드
> **다음 단계**: Phase 5에서 카드 간 비즈니스 룰/데이터 흐름 설정 (제어 모드)

---

## 1. 핵심 개념

**대시보드 = 메뉴 = 화면.** "대시보드"라는 이름이지만, 통계/차트 모아놓는 곳이 아니다.

```
사용자가 "수주관리" 대시보드 생성
  → 사이드바 메뉴에 "수주관리" 자동 등록
  → 캔버스에 수주관리 Template 카드 배치
  → 화면 완성!
  → 비즈니스 로직 필요하면 → Phase 5 (제어 모드)
```

---

## 2. 사용자 흐름 (mockup 기준)

mockup의 `index.html` + `js/05-state.js` + `js/01-shell.js` 참조.

### 2.1 대시보드 생성

1. 사이드바 하단의 **`+ 새 대시보드`** 클릭
2. 이름 입력 (예: "수주관리")
3. → 사이드바 메뉴에 자동 등록
4. → 빈 캔버스가 열림 ("아직 템플릿이 없습니다")

### 2.2 템플릿 카드 배치

1. 캔버스 상단의 **`+ 템플릿 추가`** 클릭
2. **템플릿 라이브러리 모달** 열림:
   - 좌측: 카테고리 (영업, 생산, 인사, 재고, 관리자...)
   - 우측: 해당 카테고리의 Template 카드 목록
   - 검색 가능
3. Template 카드 클릭 → 캔버스에 추가
4. 여러 Template을 추가하여 한 대시보드(=화면)에 여러 카드 배치 가능

### 2.3 카드 조작

- **편집 모드** (우상단 편집 버튼) 켜면:
  - 카드 드래그 (자유 배치, snap 없음)
  - 카드 리사이즈 (우하단 핸들)
  - 카드 삭제 (X 버튼)
- **카드 접기** (▼ 버튼) → 미니 KPI 뷰로 축소
- **카드 설정** (⚙ 버튼) → 사용자 오버라이드:
  - 컬럼 표시/숨김
  - 검색 필터 표시/숨김
  - 통계 카드 표시/숨김

### 2.4 저장/복원

- 대시보드 레이아웃(카드 위치/크기/설정) 자동 저장
- 다음 접속 시 그대로 복원

---

## 3. 데이터 모델

### 3.1 대시보드 (DB 테이블)

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

```sql
CREATE TABLE dashboards (
    dashboard_id    VARCHAR(50) PRIMARY KEY,
    name            VARCHAR(200) NOT NULL,
    icon            VARCHAR(10) DEFAULT '📋',
    display_order   INTEGER DEFAULT 0,
    company_code    VARCHAR(20) NOT NULL,
    user_id         VARCHAR(50),              -- NULL이면 회사 공통
    is_active       VARCHAR(1) DEFAULT 'Y',
    created_by      VARCHAR(50),
    created_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_by      VARCHAR(50),
    updated_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_dashboards_company ON dashboards(company_code);
CREATE INDEX idx_dashboards_user ON dashboards(user_id);
```

### 3.2 대시보드 카드 배치

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

```sql
CREATE TABLE dashboard_cards (
    card_id         VARCHAR(50) PRIMARY KEY,
    dashboard_id    VARCHAR(50) NOT NULL,    -- ★ DB FK 제약조건 안 걸음 (앱 레벨 관리)
    template_id     VARCHAR(50) NOT NULL,    -- ★ DB FK 제약조건 안 걸음
    position_x      INTEGER DEFAULT 0,        -- 캔버스 상 X 좌표 (px)
    position_y      INTEGER DEFAULT 0,        -- 캔버스 상 Y 좌표 (px)
    width           INTEGER DEFAULT 600,      -- 카드 너비 (px)
    height          INTEGER DEFAULT 400,      -- 카드 높이 (px)
    is_collapsed    BOOLEAN DEFAULT FALSE,    -- 접힌 상태 여부
    display_order   INTEGER DEFAULT 0,
    is_active       VARCHAR(1) DEFAULT 'Y',
    created_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_dcards_dashboard ON dashboard_cards(dashboard_id);
```

### 3.3 사용자 오버라이드

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

```sql
CREATE TABLE user_overrides (
    override_id     VARCHAR(50) PRIMARY KEY,
    user_id         VARCHAR(50) NOT NULL,
    card_id         VARCHAR(50) NOT NULL,    -- ★ DB FK 제약조건 안 걸음
    overrides       JSONB NOT NULL,           -- { fields: {...}, fieldOrder: [...], gridColumns: {...} }
    created_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_date    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, card_id)
);
-- ★ card_id 기준 — 같은 템플릿을 두 번 올려도 카드별로 다른 오버라이드 가능
```

**overrides JSON 구조** (로우코드 플랫폼 SPEC Section 3.2):

```json
{
  "fields": {
    "fax_number": { "visible": false },
    "customer_name": { "label": "고객사" }
  },
  "fieldOrder": ["order_date", "customer_name", "amount"],
  "gridColumns": {
    "order_id": { "width": 80 },
    "customer_name": { "width": 200 }
  }
}
```

**오버라이드 병합 규칙:**
1. 개발자 Template의 FieldConfig[]이 기본값
2. 사용자 override가 있으면 해당 필드만 덮어씀
3. 개발자가 필드를 삭제하면 해당 override는 자동 무시

---

## 4. 백엔드 API 설계

### 4.1 대시보드 CRUD

```
GET    /api/dashboards                        → 목록 (해당 유저 + 회사 공통)
POST   /api/dashboards                        → 생성 (이름, 아이콘)
PUT    /api/dashboards/{dashboard_id}           → 수정 (이름, 아이콘, 순서)
DELETE /api/dashboards/{dashboard_id}           → 삭제
```

### 4.2 대시보드 카드 CRUD

```
GET    /api/dashboards/{dashboard_id}/cards     → 해당 대시보드의 카드 목록 (+ Template 기본 정보)
POST   /api/dashboards/{dashboard_id}/cards     → 카드 추가 (templateId, 위치/크기)
PUT    /api/dashboards/{dashboard_id}/cards/{card_id}  → 카드 이동/리사이즈/접기
DELETE /api/dashboards/{dashboard_id}/cards/{card_id}  → 카드 제거
PUT    /api/dashboards/{dashboard_id}/cards/batch      → 다수 카드 일괄 업데이트 (위치/크기 저장)
```

### 4.3 사이드바 메뉴 (= 대시보드 목록)

```
GET    /api/sidebar/menu                       → 사이드바에 표시할 메뉴 목록
```

Response:
```json
{
  "data": [
    {
      "section": "내 대시보드",
      "items": [
        { "dashboardId": "dash-1", "name": "수주관리", "icon": "📦", "order": 1 },
        { "dashboardId": "dash-2", "name": "인사 대시보드", "icon": "👥", "order": 2 }
      ]
    }
  ]
}
```

### 4.4 템플릿 라이브러리 (카드 추가 시 모달)

```
GET    /api/templates/library                  → 게시된(published) 템플릿 목록 (카테고리별)
```

### 4.5 사용자 오버라이드

```
GET    /api/overrides?card_id=xxx                       → 해당 유저+카드의 오버라이드
PUT    /api/overrides                                   → 오버라이드 저장 (card_id 기준)
```

### 4.6 Java 파일

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

| 파일 | 역할 |
|---|---|
| `DashboardController.java` | 대시보드 + 카드 CRUD |
| `DashboardService.java` | `extends BaseService`, sqlSession으로 Map 기반 CRUD |
| `DashboardMapper.xml` | 대시보드/카드 SQL (`resultType="map"`, `parameterType="map"`) |
| `SidebarController.java` | 사이드바 메뉴 조회 |
| `UserOverrideController.java` | 사용자 오버라이드 CRUD |
| `UserOverrideService.java` | `extends BaseService`, 오버라이드 병합 로직 |

**파일명: `dashboard.xml` (namespace="dashboard"), `userOverride.xml` (namespace="userOverride")**

```java
// DashboardService.java (덕일 스타일)
@Service
@Slf4j
public class DashboardService extends BaseService {

    @Autowired
    private CommonService commonService;

    public List<Map<String, Object>> getDashboardList(Map<String, Object> params) {
        return sqlSession.selectList("dashboard.getDashboardList", params);
    }
    public Map<String, Object> getDashboardInfo(Map<String, Object> params) {
        return sqlSession.selectOne("dashboard.getDashboardInfo", params);
    }
    public List<Map<String, Object>> getDashboardCardList(Map<String, Object> params) {
        return sqlSession.selectList("dashboard.getDashboardCardList", params);
    }
    @Transactional
    public Map<String, Object> insertDashboard(Map<String, Object> params) {
        sqlSession.insert("dashboard.insertDashboard", params);
        return params;
    }
    @Transactional
    public void updateDashboard(Map<String, Object> params) {
        sqlSession.update("dashboard.updateDashboard", params);
    }
    @Transactional
    public int deleteDashboard(Map<String, Object> params) {
        return sqlSession.update("dashboard.deleteDashboard", params);
    }
    @Transactional
    public void updateCardPositions(Map<String, Object> params) {
        List<Map<String, Object>> cards = (List<Map<String, Object>>) params.get("cards");
        for (Map<String, Object> card : cards) {
            sqlSession.update("dashboard.updateCardPosition", card);
        }
    }
}
```

---

## 5. 프론트엔드 구현

### 5.1 파일 구조

```
frontend/components/dashboard/
  ├── DashboardCanvas.tsx          ← 캔버스 (카드 자유 배치)
  ├── DashboardCard.tsx            ← 개별 카드 (Template 렌더링)
  ├── DashboardToolbar.tsx         ← 상단 툴바 (편집/저장/+템플릿)
  ├── DashboardEmpty.tsx           ← 빈 대시보드 안내
  ├── TemplateLibraryModal.tsx     ← 템플릿 라이브러리 모달
  ├── CardSettingsPanel.tsx        ← 카드 설정 패널 (사용자 오버라이드)
  └── CardMiniView.tsx             ← 접힌 카드의 미니 KPI 뷰

frontend/components/layout/
  ├── AppLayout.tsx                ← (기존) 전체 레이아웃 — 수정 필요
  ├── Sidebar.tsx                  ← 사이드바 (대시보드 목록 = 메뉴)
  └── Header.tsx                   ← 헤더

frontend/stores/
  └── dashboardStore.ts            ← 대시보드 상태 (Zustand)

frontend/lib/api/
  ├── dashboard.ts                 ← 대시보드 CRUD API
  └── override.ts                  ← 사용자 오버라이드 API
```

### 5.2 DashboardCanvas — 핵심 컴포넌트

```typescript
/**
 * ★ 별도 인터페이스 정의 안 함 — Record<string, any> 사용
 * ★ 유일하게 타입이 있는 건 FieldConfig, Template 등 (invyone-component.ts 규격)
 *
 * 카드 데이터 구조 (Record<string, any>):
 *   card_id, template_id, template(로드된 Template), x, y, width, height, is_collapsed
 */
```

**카드 안에서 Template 렌더링:**
- Template.views.list의 components[]를 순서대로 렌더
- 각 Component는 Phase 2의 FcTable/FcForm/FcSearch를 사용
- Template.fields를 각 컴포넌트에 전달
- Template.connections로 컴포넌트 간 DataPort 연결

### 5.3 Sidebar — 대시보드 = 메뉴

```typescript
/**
 * ★ Props도 별도 인터페이스 불필요 — 인라인 또는 Record<string, any> 사용
 * 사이드바는 dashboards(Record<string, any>[]), active_dashboard_id, 콜백들을 props로 받음
 */
```

mockup 참조:
- `css/02-shell.css`의 `.side`, `.si`, `.side-add-btn`
- `js/05-state.js`의 `renderSidebar()`, `addDashboard()`, `switchDashboard()`
- 접힌 상태 (60px) + 툴팁
- hover 시 이름 변경/삭제 아이콘

### 5.4 TemplateLibraryModal

mockup 참조: `css/06-modals.css`의 `.lib-modal`, `js/01-shell.js`의 `openLib()/closeLib()`

```
┌──────────────────────────────────────────────┐
│ 템플릿 라이브러리          [검색...] [X]     │
├────────────┬─────────────────────────────────┤
│ 카테고리    │ 카드 그리드                     │
│            │                                 │
│ 전체       │ ┌───────┐ ┌───────┐ ┌───────┐  │
│ 영업/CRM   │ │ 📦     │ │ 📊     │ │ 👥     │  │
│ 생산/공정   │ │수주관리 │ │매출KPI │ │인사관리│  │
│ 인사/급여   │ │sales  │ │chart  │ │hr     │  │
│ 재고/물류   │ └───────┘ └───────┘ └───────┘  │
│ 관리자     │                                 │
└────────────┴─────────────────────────────────┘
```

---

## 6. 스타일

사용자 화면은 **v5 Cosmic Glassmorphism** (개발자 빌더의 IDE 스타일과 다름):
- 글래스모피즘: `backdrop-filter:blur(20px) saturate(1.4)` + `var(--glass)`
- 코스믹 배경: 별/성운/입자 (다크 모드), 구름형 (라이트 모드)
- 보라/시안/핑크 액센트
- 컴팩트 폰트 (0.55~0.85rem)

mockup 참조:
- `css/01-tokens.css` (토큰)
- `css/02-shell.css` (사이드바, 헤더)
- `css/03-canvas.css` (캔버스, 카드)
- `css/04-settings.css` (카드 설정)
- `css/06-modals.css` (라이브러리 모달)
- `frontend/styles/v5-layout.css` (React 포팅된 v5 토큰)

---

## 7. 참고 파일

| 파일 | 용도 |
|---|---|
| `notes/gbpark/2026-04-08-invyone-mockup/index.html` | **대시보드 전체 UI (진실의 원천)** |
| `notes/gbpark/2026-04-08-invyone-mockup/js/05-state.js` | 대시보드 상태 관리 로직 (SEED_STATE, renderCanvas, switchDashboard 등) |
| `notes/gbpark/2026-04-08-invyone-mockup/js/01-shell.js` | 테마/모드/사이드바/아바타/라이브러리 모달 |
| `notes/gbpark/2026-04-08-invyone-mockup/js/02-canvas.js` | 편집/드래그/리사이즈/접기/삭제 |
| `notes/gbpark/2026-04-08-invyone-mockup/js/04-templates.js` | templateRenderers, buildCardEl, addCardFromLib |
| `notes/gbpark/2026-04-08-invyone-mockup/css/01-tokens.css` ~ `06-modals.css` | v5 스타일 전체 |
| `notes/gbpark/2026-04-08-lowcode-platform-spec.md` (Section 3.2) | 사용자 오버라이드 레이어 설계 |
| `frontend/styles/v5-layout.css` | React 포팅된 v5 CSS |
| `frontend/components/layout/AppLayout.tsx` | 기존 레이아웃 (수정 대상) |

---

## 8. 완료 기준

1. 사이드바에 대시보드 목록이 표시되고, 클릭 시 해당 대시보드로 전환된다
2. `+ 새 대시보드` → 이름 입력 → 사이드바에 자동 등록된다
3. 대시보드 이름 변경/삭제가 동작한다
4. `+ 템플릿 추가` → 라이브러리 모달에서 Template 선택 → 캔버스에 카드 추가된다
5. 편집 모드에서 카드 드래그/리사이즈가 동작한다
6. 카드 접기/펴기 (미니 KPI 뷰)가 동작한다
7. 카드 안에서 실제 데이터가 렌더링된다 (FcTable/FcSearch 등)
8. 카드 설정(⚙)에서 컬럼 ON/OFF → 실시간 반영
9. 대시보드 레이아웃이 DB에 저장/복원된다
10. 사용자 오버라이드가 Template 위에 올바르게 적용된다

---

## 9. 다음 단계 연결

Phase 5 (제어 모드):
- 대시보드의 카드를 클릭하면 → 해당 카드의 데이터 흐름 시각화
- 카드 간 비즈니스 룰 설정 (카드 A의 데이터가 변경되면 카드 B 자동 갱신 등)
- 대시보드 캔버스 위에 제어 모드 오버레이
