# invyone 리팩토링 — 대시보드 + 템플릿 SPEC (v0.2)

> **상태**: DRAFT — 사용자 검토 대기
> **작성일**: 2026-04-08
> **작성자**: gbpark + Claude
> **이전 버전**: v0.1 (885줄) — 방향성(메타 모델 3계층, 인증 재활용, LLM 친화 컬럼 정책)은 맞았지만 톤이 "그린필드 / 새 라우트 격리" 같은 재빌드 쪽으로 새서 폐기. v0.2 는 **순수 리팩토링** 으로 다시 씀.
>
> **★ 핵심 입장**: 새 프로젝트 만드는 것 아님. 기존 invyone 코드(`frontend/app/(main)/dashboard/` 가 이미 있음, `DashboardElement` 가 이미 있음, v5 Cosmic 디자인이 이미 있음) 위에 시안의 UX 컨셉을 **얹는 것**.
>
> **v0.1 에서 흡수한 부분** (방향이 맞은 것):
> - Component / Template / Dashboard 3계층 메타 모델
> - 기존 인증/멀티테넌시/MyBatis 패턴 재활용
> - LLM 친화 컬럼 정책 (description / tags_json / semantic_keywords) — M3+
> - 비범위 정의 (BOM / 생산 / 출하 / 모바일 / i18n)
>
> **v0.1 에서 버린 부분** (방향이 틀린 것):
> - "재빌드" 라는 단어
> - 새 라우트 격리 (`(invyone)/builder`) — 기존 `/dashboard` 가 이미 있는데 별개 셸을 또 만드는 건 그린필드 발상
> - v5 Cosmic 디자인 폐기 — 사용자가 "디자인은 유지" 명시
> - Screen Designer 폐기 결정 — M1 범위 아님
> - "885줄" 의 광범위함 — 첫 마일스톤은 좁아야 함
>
> **입력 자료**:
> - 다운로드 시안 ZIP (`~/다운로드/INVYONE개발.zip`) — `index.html`, `developer.html`, `templates/hr-management.html`, `js/app.js`, `js/template-html.js` (디자이너 시안, vanilla HTML/CSS/JS — **참고만, 맹신 금지**)
> - 현재 invyone 코드 — `frontend/app/(main)/dashboard/page.tsx`, `frontend/app/(main)/dashboard/[dashboardId]/page.tsx`, `frontend/components/dashboard/DashboardViewer.tsx`, `frontend/components/admin/dashboard/types.ts`, `frontend/lib/api/dashboard.ts`, `frontend/styles/v5-layout.css`, `frontend/invion-layout-v5.html`, `backend-spring/src/main/resources/mapper/auth.xml`
> - **v5 톤 mockup HTML** (이번 SPEC 작업 중 만듦): `notes/gbpark/2026-04-08-invyone-mockup-dashboard.html` — 이게 SPEC 의 시각적 진실의 원천. SPEC 과 모순되면 mockup 이 정답
> - 사용자 인터뷰 (2026-04-08 채팅 세션)

---

## 0. 한 줄 요약

**기존 invyone 의 dashboard 시스템(이미 존재함)에 "Template" 단위 element 를 새로 추가해서, 사용자가 인사정보 같은 큰 화면을 카드로 잡아 캔버스에 배치할 수 있게 만든다. 디자인은 v5 Cosmic 그대로. 첫 마일스톤은 인사정보 템플릿 1개.**

> **이건 리팩토링이지 재빌드/그린필드 아님.** `frontend/app/(main)/dashboard/[dashboardId]/page.tsx` 에 이미 DashboardViewer 가 있고, `DashboardElement` 타입에 `position{x,y} + size{w,h}` 가 이미 있고, `lib/api/dashboard.ts` 가 이미 있다. M1 작업의 80% 는 **기존 코드에 type:'template' 분기 한 줄 추가 + 인사정보 React 컴포넌트 1개 신규** 가 전부다. 새 라우트 안 만든다. 새 셸 안 만든다.

## 0.1 mockup 시각 자료

`notes/gbpark/2026-04-08-invyone-mockup-dashboard.html` — 브라우저로 열면 됨. 이 SPEC 의 모든 UX 결정은 이 mockup 으로 시각화되어 있음. 보이는 것:

- v5 Cosmic 셸 (헤더 / 탭 / 사이드바 / 캔버스) — `invion-layout-v5.html` 베이스
- 사이드바 메뉴: **워크스페이스(인사 대시보드 / 매출 / 재고 / + 새 대시보드) + 도메인(ERP/MES/SCM) + 분석(리포트/AI)**
- 캔버스 위에 인사정보 템플릿 카드 1개 (통계 4개 + 필터 + 사원 테이블)
- 우측에 빈 자리 2개 (다음 카드 추가 위치)
- 우상단 "+ 템플릿 추가" 버튼 → 라이브러리 모달 (인사정보 활성, M2/M4 카드는 dim)
- 헤더의 톱니 버튼 → 사용자 ↔ 개발자 모드 토글 (시안의 mode-switch 와 같은 컨셉, 기존 v5 의 admin-mode 를 재사용)

이 mockup 이 SPEC 의 시각적 정답. SPEC 텍스트와 mockup 이 어긋나면 mockup 이 맞음.

---

## 1. 목적 / 방향

### 1.1 무엇을 하는가
- 기존 invyone 코드의 **리팩토링**. 새 프로젝트 X. 새 라우트 격리 X.
- 시안의 핵심 컨셉(= 사용자가 캔버스에 큰 템플릿 카드를 배치) 을 기존 dashboard 시스템 위에 얹는다.
- 기존 v5 Cosmic 디자인 그대로 사용. 시안의 `#4a6cf7` 파란/흰 톤은 가져오지 않는다.

### 1.2 무엇을 하지 않는가
- v5 Cosmic 디자인 폐기 X — `frontend/styles/v5-layout.css` 토큰 그대로 유지
- 시안의 80개 컴포넌트 라이브러리 통째 이식 X
- 시안의 iframe + srcdoc 렌더링 방식 채택 X — React 컴포넌트로 구현
- 시안의 옵션 패널(네비위치/테마컬러 등 화이트라벨링) 첫 마일스톤 범위 아님
- 사용자 모드 / 개발자 모드 분리 첫 마일스톤 범위 아님 (M2 이후)
- 기존 Screen Designer (`frontend/components/screen/` 147 파일) 폐기 X — 안 건드림

### 1.3 시안 (`INVYONE개발.zip`) 의 위치
- **참고 자료**. 맹신 금지.
- 가져올 것: 사용자가 캔버스 위에 큰 화면 카드를 배치하는 UX 흐름, 인사관리 화면의 정보 구조(통계카드 + 필터 + 테이블 + 상세)
- 버릴 것: 디자인 톤(파란/흰), iframe srcdoc, 컴포넌트 80개 카탈로그, 옵션 패널 세부사항

---

## 2. 기존 자산 인벤토리 (★ 리팩토링 SPEC 의 출발점)

### 2.1 이미 있는 것 — 그대로 활용

| 자산 | 위치 | 역할 |
|---|---|---|
| Dashboard 목록 페이지 | `frontend/app/(main)/dashboard/page.tsx` | 저장된 dashboard 카드 그리드. 검색, 새로 만들기 버튼 |
| Dashboard 뷰어 페이지 | `frontend/app/(main)/dashboard/[dashboardId]/page.tsx` | 특정 dashboard 를 읽기 전용으로 표시 |
| DashboardViewer 컴포넌트 | `frontend/components/dashboard/DashboardViewer.tsx` | elements 배열을 받아 절대 위치로 렌더링 |
| DashboardElement 타입 | `frontend/components/admin/dashboard/types.ts` | `{ id, type, position{x,y}, size{w,h}, ... }` |
| Dashboard API 클라이언트 | `frontend/lib/api/dashboard.ts` | `getDashboards()`, `getDashboard(id)` |
| 인증 / 멀티테넌시 | `backend-spring/.../security/`, `JwtAuthenticationFilter` | `company_code` 필터 그대로 |
| USER_INFO / DEPARTMENT / ROLE_INFO | DB + `auth.xml`, `department.xml`, `role.xml` 매퍼 | 인사정보 화면의 데이터 소스 |
| v5 Cosmic 디자인 토큰 | `frontend/styles/v5-layout.css` | `--v5-primary`, `--v5-glass`, `.v5-card`, `.v5-btn` 등 |
| MyBatis Map-based 패턴 | `BaseService` + XML mapper | 새 도메인은 동일 패턴 |

### 2.2 없는 것 — 새로 만들어야 함

| 자산 | 만들 위치 | 역할 |
|---|---|---|
| Template 메타 (이름/태그/카테고리/렌더러 키) | `frontend/lib/templates/registry.ts` | 사용 가능한 Template 목록을 정의하는 중앙 레지스트리 |
| Template 렌더러 컨벤션 | `frontend/components/templates/` | 각 Template 은 React 컴포넌트 1개. 위치/크기는 부모(DashboardItem) 가 줌 |
| 인사정보 Template (`hr-employee-list`) | `frontend/components/templates/HrEmployeeList.tsx` | 첫 마일스톤의 유일한 Template. v5 토큰으로 통계카드 + 필터 + 테이블 + 상세 |
| Template 라이브러리 모달 | `frontend/components/dashboard/TemplateLibraryModal.tsx` | 사용자가 캔버스에 새 카드를 추가할 때 뜨는 모달 |
| 인사정보 백엔드 API | `backend-spring/.../controller/HrEmployeeController.java` | `GET /api/hr/employees?page&size&search` 기존 USER_INFO/DEPARTMENT/ROLE_INFO 조인 |
| HR mapper | `backend-spring/src/main/resources/mapper/hrEmployee.xml` | 인사정보 조회 SQL |

### 2.3 다른 것 — 갭 분석

| 항목 | 기존 | 시안 | 결정 |
|---|---|---|---|
| Element 단위 | 차트 1개 (`type: 'chart', subtype: 'bar'`) | 큰 화면 1개 (`tpl-hr-mgmt`) | **DashboardElement 에 `type: 'template'` 추가**. 기존 `'chart'` 도 유지 |
| dataSource | 각 element 가 `dataSource: { query: "SELECT ..." }` 자체 보유 | 템플릿이 자체적으로 알아서 fetch | Template 컴포넌트 안에서 React Query/SWR 로 직접 호출. element 에는 templateId 만 |
| 편집 라우트 | `/admin/screenMng/dashboardList?load=...` (Screen Designer 계열) | 별도 개발자 모드 | 첫 마일스톤은 **편집 X, 미리 만든 dashboard 1개를 보여주기만**. 편집은 M2 |
| 사용자/개발자 모드 분리 | 없음 | 헤더의 mode-switch 토글 | M2 이후 |
| 화이트라벨링 옵션 패널 | 없음 | 네비위치/테마컬러/폰트 등 | M3 이후 |

---

## 3. 메타 모델

### 3.1 3계층

```
Component  ─ React 컴포넌트. 코드. 메타 모델 아님. (= 그냥 우리가 짜는 코드)
   │
   └─▶ Template ─ "큰 화면 한 덩이" 단위. 예: 인사정보, 매출 대시보드.
          │       Registry 에 { id, name, icon, category, tags, render } 로 등록.
          │       각각 하나의 React 컴포넌트.
          │
          └─▶ Dashboard ─ 사용자가 자기 작업 공간에 만든 캔버스.
                          DashboardElement[] 를 가짐. 각 element 는
                          { id, type:'template', templateId, position, size, ... }
```

**Component 는 메타 모델 아님** — 시안의 80개 컴포넌트 카탈로그처럼 DB 에 박지 않는다. 그냥 코드. Template 을 만들 때 React 코드 안에서 자유롭게 import 해서 쓴다.

### 3.2 Template Registry (TS 코드)

```ts
// frontend/lib/templates/registry.ts
import { HrEmployeeList } from "@/components/templates/HrEmployeeList";

export type TemplateRenderer = React.ComponentType<{
  config?: Record<string, unknown>;
}>;

export interface TemplateMeta {
  id: string;                // 'hr-employee-list'
  name: string;              // '인사정보'
  description: string;       // '사원 목록, 부서/역할 조회'
  icon: string;              // '👥'
  category: 'erp' | 'mes' | 'scm' | 'admin' | 'dashboard';
  tags: string[];            // ['ERP', '인사/급여']
  defaultSize: { w: number; h: number };  // 픽셀
  render: TemplateRenderer;
}

export const TEMPLATE_REGISTRY: Record<string, TemplateMeta> = {
  'hr-employee-list': {
    id: 'hr-employee-list',
    name: '인사정보',
    description: '사원 목록, 부서/역할/상태 조회',
    icon: '👥',
    category: 'erp',
    tags: ['ERP', '인사/급여'],
    defaultSize: { w: 960, h: 640 },
    render: HrEmployeeList,
  },
};
```

첫 마일스톤에서 Registry 의 엔트리는 **딱 1개**. 이후 마일스톤마다 1~2개씩 추가.

### 3.3 DashboardElement 타입 변경

```ts
// frontend/components/admin/dashboard/types.ts (수정)
export type DashboardElement =
  | DashboardChartElement   // 기존 — 그대로 유지, 안 건드림
  | DashboardTemplateElement;  // ★ NEW

export interface DashboardTemplateElement {
  id: string;
  type: 'template';
  templateId: string;          // TEMPLATE_REGISTRY 의 키
  position: { x: number; y: number };
  size: { width: number; height: number };
  title?: string;              // 카드 헤더 표시용 (없으면 TEMPLATE_REGISTRY[].name)
  collapsed?: boolean;
  config?: Record<string, unknown>;  // 템플릿별 옵션 (예: { defaultDept: 'IT' })
}
```

기존 chart element 는 그대로 둔다. DashboardViewer 는 `element.type` 으로 분기.

---

## 4. 첫 마일스톤 (M1) — 인사정보 카드 1개

### 4.1 정의
사용자가 invyone 에 로그인 → 사이드바의 "대시보드" 클릭 → "인사 대시보드" 진입 → 화면에 **인사정보 템플릿 카드 1개가 v5 Cosmic 디자인으로 그려져 있고, 그 안에 실제 USER_INFO/DEPARTMENT/ROLE_INFO 조인 데이터가 표 형태로 보임.**

### 4.2 완료 조건 (Definition of Done)
1. `/dashboard` 진입 시 "인사 대시보드" 라는 dashboard 카드가 1개 보임
2. 그걸 클릭하면 `/dashboard/{id}` 로 이동
3. 그 페이지에 인사정보 template 카드가 그려짐 (절대 위치, defaultSize)
4. 카드 안에 통계카드 4개(전체/재직/휴직/퇴직) + 검색 필터 + 사원 테이블이 v5 Cosmic 톤으로 표시됨
5. 테이블 데이터는 백엔드 API (`GET /api/hr/employees`) 에서 진짜 옴
6. 검색/페이지네이션 동작
7. 다크/라이트 모드 둘 다 깨지지 않음
8. 빌드 통과 (`npm run build`, `./gradlew bootJar`)

### 4.3 작업 리스트 (구체적)

**Frontend**

1. `frontend/lib/templates/registry.ts` 생성 — `TEMPLATE_REGISTRY` 정의
2. `frontend/components/templates/HrEmployeeList.tsx` 생성
   - v5 토큰 사용 (`v5-card`, `var(--v5-glass)`, `var(--v5-primary)`)
   - 통계카드 4개 (전체/재직/휴직/퇴직 — STATUS 컬럼 기준)
   - 검색 필터바 (이름/사번/부서)
   - 사원 테이블 (USER_NAME, USER_ID, 부서, 역할, STATUS, 입사일)
   - 페이지네이션
   - React Query 또는 SWR 로 `/api/hr/employees` 호출
3. `frontend/components/admin/dashboard/types.ts` 수정 — `DashboardTemplateElement` 추가, union 타입 갱신
4. `frontend/components/dashboard/DashboardViewer.tsx` 수정 — `element.type === 'template'` 분기 추가, `TEMPLATE_REGISTRY[templateId].render` 호출
5. `frontend/lib/api/dashboard.ts` — 기존 API 가 죽어있으면 임시로 `getDashboards()` 가 "인사 대시보드" 1개를 하드코딩 반환하게 (M1 한정. M2 에서 진짜 백엔드 연결)
6. `frontend/lib/api/hrEmployee.ts` 신규 — `getEmployees({ page, size, search })`

**Backend**

7. `backend-spring/src/main/java/com/erp/controller/HrEmployeeController.java` 신규 — `GET /api/hr/employees`
8. `backend-spring/src/main/java/com/erp/service/HrEmployeeService.java` 신규 — BaseService 패턴
9. `backend-spring/src/main/resources/mapper/hrEmployee.xml` 신규 — USER_INFO + DEPARTMENT + ROLE_INFO 조인 SELECT, `common.companyCodeFilter` include
10. `SecurityConfig.java` — `/api/hr/**` 인증 경로 등록 (이미 패턴 있으면 추가만)

**검증**

11. `cd frontend && npm run build` 통과
12. `cd backend-spring && ./gradlew bootJar` 통과
13. 도커 컨테이너 (`docker-compose.invyone.yml`) 재기동 후 브라우저로 동작 확인

### 4.4 비범위 (M1 안 함)
- Dashboard 편집 UX (드래그/리사이즈/추가/삭제) — M2
- Template 라이브러리 모달 — M2
- 사용자/개발자 모드 분리 — M3
- 화이트라벨링 옵션 패널 — M3
- 인사정보 외 다른 Template — M2 부터 점진 추가
- 사원 등록/수정/삭제 — M2 (M1 은 조회만)
- AI 어시스턴트 연결 — M5

---

## 5. 디자인 — v5 Cosmic (★ 변경 없음)

### 5.1 절대 준수
- **모든 색상 / 간격 / 글래스 / 그림자는 `frontend/styles/v5-layout.css` 의 토큰만 사용**
- 새 hex/rgb 즉흥 사용 금지
- 시안의 `#4a6cf7`, `#27ae60`, `#e74c3c` 등 가져오지 말 것 — 같은 의미는 v5 토큰으로 매핑

### 5.2 시안 → v5 토큰 매핑 (인사정보 화면용)

| 시안 (hr-management.html) | v5 토큰 |
|---|---|
| `#4a6cf7` (주 액센트) | `var(--v5-primary)` (#6c5ce7) |
| `#27ae60` (성공/재직) | `var(--v5-success)` 또는 v5-bdg-success 클래스 |
| `#f39c12` (경고/휴직) | `var(--v5-warn)` |
| `#e74c3c` (위험/퇴직) | `var(--v5-danger)` |
| `#fff` 카드 배경 | `var(--v5-glass)` + `backdrop-filter: blur(20px) saturate(1.4)` |
| `#e8e8e8` 보더 | `var(--v5-glass-border)` |
| `box-shadow: 0 2px 12px rgba(0,0,0,0.06)` | `var(--v5-glow-sm)` |
| 폰트 사이즈 13px | v5 컴팩트 스케일 (0.6~0.85rem) |

### 5.3 새 클래스가 필요하면
- 일회성 inline `<style>` 보다 `frontend/styles/v5-layout.css` 에 `.v5-tpl-hr-*` 형태로 추가
- 기존 `.v5-card`, `.v5-btn`, `.v5-bdg` 가 충분하면 그걸 우선 재사용

---

## 6. DB 변경

### 6.1 M1 에서 변경 없음
- `USER_INFO`, `DEPARTMENT`, `ROLE_INFO` 는 이미 있음. 인사정보 조회는 SELECT 만 함
- Dashboard 자체도 M1 은 하드코딩 반환 → DB 변경 불필요

### 6.2 M2 에서 추가 예정 (이번 SPEC 범위 아님, 미리보기)
```
DASHBOARD            (id, company_code, owner_user_id, name, is_default, created_date)
DASHBOARD_ELEMENT    (id, dashboard_id, element_type, template_id, position_x, position_y, size_w, size_h, config_json, created_date)
TEMPLATE_REGISTRY    (id, name, description, icon, category, tags_json, default_size_json)
                     ※ TS 코드 registry 와 1:1. DB 는 M3 에 LLM 친화적으로 옮길 수 있음
```

### 6.3 M3+ — LLM 친화 컬럼 정책 (개념만)
- 모든 새 테이블에 `description TEXT`, `tags_json JSONB`, `semantic_keywords JSONB` 컬럼을 시드부터 박는다
- 자연어 → 워크플로우 자동 생성 시 LLM 이 검색할 길

---

## 7. 라우트 / 메뉴

### 7.1 라우트 변경 없음 (M1)
- `/dashboard` — 기존 그대로
- `/dashboard/[dashboardId]` — 기존 그대로 (DashboardViewer 가 template 분기 처리하면 끝)

### 7.2 사이드바 메뉴
- 기존 사이드바에 "대시보드" 항목이 이미 있으면 그대로 사용
- 없으면 `frontend/components/layout/AppLayout.tsx` 의 사이드바 구성에 추가

---

## 8. AI 어시스턴트

- M1 범위 아님
- 자리만 둔다는 의미: Template 컴포넌트는 props 로 `config: Record<string, unknown>` 를 받게 설계 → M5 에서 LLM 이 자연어 입력으로 이 config 를 만들어 넣음

---

## 9. 후속 마일스톤 (개념만, 본 SPEC 범위 아님)

| M | 목표 | 주요 작업 |
|---|---|---|
| M1 | 인사정보 카드 1개를 dashboard 에 띄움 | 본 SPEC |
| M2 | Dashboard 편집 UX + Template 라이브러리 모달 + DB 마이그레이션 | DASHBOARD/DASHBOARD_ELEMENT 테이블 신설, 드래그/리사이즈 |
| M3 | 사용자/개발자 모드 분리 + 화이트라벨링 옵션 패널 | mode-switch, 옵션 패널, 회사별 테마 저장 |
| M4 | Template 5~10개 추가 (회계, 영업, 재고 등) | Registry 확장, 도메인별 컨트롤러 |
| M5 | AI 어시스턴트 자연어 → Template/config 자동 생성 | 로컬 LLM 어댑터, semantic search |

---

## 10. 작업 시작 전 사용자 확인 사항 (★)

1. **인사정보의 데이터 출처 — `USER_INFO` 가 맞나?**
   기존 `auth.xml` 의 `USER_INFO` 는 `LICENSE_NUMBER`, `VEHICLE_NUMBER` 컬럼이 있는 걸 보면 운송업/공차중계 도메인 흔적이 있음. invyone 의 진짜 사원 정보가 다른 테이블에 있으면 알려줘 (예: `EMPLOYEE`, `STAFF` 등)

2. **첫 인사정보 화면이 보여줄 컬럼**
   - 후보: `USER_ID`, `USER_NAME`, `CELL_PHONE`, 부서명(DEPARTMENT.NAME), 역할(ROLE_INFO.NAME), `STATUS`, `CREATED_DATE`
   - 빠진 것 / 더 보고 싶은 것 있으면 알려줘

3. **dashboard 라우트가 이미 다른 용도로 쓰이고 있는데 갈아엎어도 되나?**
   현재 `/dashboard/[dashboardId]` 의 element 는 차트(`type:'chart'`) 만 가정함. `type:'template'` 추가는 비파괴적이지만, 기존 차트 dashboard 가 살아있는 게 있으면 둘 다 그려지는 게 맞나?

4. **편집 UX 는 정말 M2 로 미뤄도 되나?**
   M1 은 "이미 만들어진 dashboard 를 보여주기만" 인데, 그러면 데모 가치가 약할 수 있음. 만약 M1 에 "Template 라이브러리 모달에서 인사정보 드래그해서 캔버스에 놓기" 까지 넣으면 작업량 ~1.5배. 어떻게 할지 결정 필요

이 4개 답 받으면 M1 작업 들어감.
