중간 세이브
This commit is contained in:
@@ -0,0 +1,634 @@
|
||||
# Phase 1: DB 메타 읽기 → FieldConfig 변환
|
||||
|
||||
> **목적**: PostgreSQL 테이블의 스키마(컬럼명, 타입, PK/FK, nullable 등)를 읽어서 INVYONE의 FieldConfig[] 배열로 변환하는 파이프라인 구축
|
||||
> **전제 조건**: 없음 (최초 단계)
|
||||
> **산출물**: 테이블명을 주면 FieldConfig[]을 반환하는 백엔드 API + 프론트엔드 타입/API 클라이언트
|
||||
> **다음 단계**: Phase 2가 이 FieldConfig[]을 받아서 테이블/폼/검색 컴포넌트로 렌더링
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 개념
|
||||
|
||||
INVYONE의 모든 UI는 **FieldConfig**라는 단일 규격으로 동작한다. FieldConfig는 DB 테이블의 컬럼 메타데이터를 UI가 소비할 수 있는 형태로 변환한 것이다.
|
||||
|
||||
```
|
||||
PostgreSQL information_schema + table_type_columns (기존)
|
||||
→ Java Service에서 변환
|
||||
→ FieldConfig[] JSON 반환
|
||||
→ 프론트엔드가 이걸 받아서 렌더링
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 현재 존재하는 것
|
||||
|
||||
### 2.1 백엔드 (backend-spring)
|
||||
|
||||
| 파일 | 역할 | 활용 |
|
||||
|---|---|---|
|
||||
| `TableManagementController.java` | 테이블/컬럼 CRUD API | **확장 대상** — FieldConfig 변환 API 추가 |
|
||||
| `TableManagementService.java` | 테이블 메타 조회 서비스 | **확장 대상** — 변환 로직 추가 |
|
||||
| `mapper/TableManagementMapper.xml` | MyBatis SQL | **확장 대상** — 조회 쿼리 추가 |
|
||||
| `DataController.java` | 범용 데이터 CRUD | 참고용 — 나중에 Phase 2에서 활용 |
|
||||
|
||||
### 2.2 DB 테이블 (이미 존재)
|
||||
|
||||
**`table_type_columns`** — VEX에서 가져온 컬럼 메타데이터 테이블:
|
||||
- `table_name`, `column_name`, `input_type`, `detail_settings` (JSONB)
|
||||
- `is_nullable`, `display_order`, `company_code`
|
||||
- 회사별 오버라이드 지원 (`company_code = '*'`이면 글로벌)
|
||||
|
||||
**PostgreSQL `information_schema.columns`** — DB 원본 스키마:
|
||||
- `table_name`, `column_name`, `data_type`, `is_nullable`, `column_default`
|
||||
- `character_maximum_length`, `ordinal_position`
|
||||
|
||||
**PostgreSQL `information_schema.table_constraints` + `key_column_usage`** — PK/FK 정보
|
||||
|
||||
### 2.3 프론트엔드
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `frontend/types/invyone-component.ts` | **FieldConfig 타입 정의 (이미 완성)** — 이 파일의 `FieldConfig` 인터페이스가 진실의 원천 |
|
||||
| `frontend/lib/api/tableSchema.ts` | 테이블 스키마 API 클라이언트 (기존 VEX용, 참고) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 상세 설계
|
||||
|
||||
### 3.1 FieldConfig 타입 (이미 정의됨 — frontend/types/invyone-component.ts)
|
||||
|
||||
```typescript
|
||||
interface FieldConfig {
|
||||
// 식별
|
||||
column: string; // DB 컬럼명
|
||||
label: string; // 화면 표시 라벨
|
||||
|
||||
// 타입
|
||||
type: FieldType; // 'text' | 'number' | 'date' | 'datetime' | 'select' | 'entity' | 'checkbox' | 'textarea' | 'file' | 'code'
|
||||
|
||||
// 표시
|
||||
visible: boolean;
|
||||
order: number;
|
||||
width?: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
|
||||
// 입력
|
||||
required: boolean;
|
||||
editable: boolean;
|
||||
defaultValue?: unknown;
|
||||
placeholder?: string;
|
||||
|
||||
// 타입별 확장
|
||||
options?: string[]; // select용
|
||||
ref?: FieldRef; // entity FK 참조
|
||||
format?: string; // 포맷 문자열
|
||||
computed?: string; // 자동 계산 수식
|
||||
|
||||
// 메타
|
||||
pk?: boolean;
|
||||
system?: boolean; // company_code 같은 시스템 필드
|
||||
searchable?: boolean;
|
||||
sortable?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 PostgreSQL → FieldType 매핑 규칙
|
||||
|
||||
DB의 `data_type`을 FieldType으로 변환하는 규칙:
|
||||
|
||||
```
|
||||
PostgreSQL data_type → FieldType
|
||||
─────────────────────────────────────────
|
||||
character varying, varchar → 'text'
|
||||
text → 'textarea'
|
||||
integer, bigint, smallint → 'number'
|
||||
numeric, decimal, real, double→ 'number'
|
||||
boolean → 'checkbox'
|
||||
date → 'date'
|
||||
timestamp, timestamptz → 'datetime'
|
||||
jsonb, json → 'textarea'
|
||||
bytea → 'file'
|
||||
(그 외) → 'text' (기본값)
|
||||
```
|
||||
|
||||
**단, `table_type_columns.input_type`이 있으면 그것이 우선한다.**
|
||||
|
||||
`table_type_columns.input_type` → FieldType 매핑:
|
||||
|
||||
```
|
||||
input_type → FieldType
|
||||
────────────────────────────
|
||||
text → 'text'
|
||||
number → 'number'
|
||||
date → 'date'
|
||||
datetime → 'datetime'
|
||||
select → 'select'
|
||||
entity → 'entity'
|
||||
checkbox → 'checkbox'
|
||||
boolean → 'checkbox'
|
||||
textarea → 'textarea'
|
||||
text_area → 'textarea'
|
||||
file → 'file'
|
||||
code → 'code'
|
||||
numbering → 'code'
|
||||
category → 'select'
|
||||
decimal → 'number'
|
||||
email → 'text'
|
||||
password → 'text'
|
||||
tel → 'text'
|
||||
(그 외) → 'text'
|
||||
```
|
||||
|
||||
### 3.3 변환 로직 상세
|
||||
|
||||
입력: `table_name` (String), `company_code` (String)
|
||||
|
||||
```
|
||||
Step 1: information_schema.columns에서 해당 테이블의 모든 컬럼 조회
|
||||
Step 2: information_schema.table_constraints + key_column_usage에서 PK 컬럼 목록 조회
|
||||
Step 3: table_type_columns에서 해당 테이블의 커스텀 메타 조회 (company_code 우선순위: 해당 회사 > '*')
|
||||
→ input_type='entity'인 컬럼의 detail_settings JSON에서 FK 참조 정보 추출
|
||||
→ referenceTable, referenceColumn, displayColumn
|
||||
Step 4: Step 1~3 병합하여 FieldConfig[] 생성
|
||||
```
|
||||
|
||||
**★ FK는 DB 제약조건(FOREIGN KEY)이 아닌 `table_type_columns.detail_settings` JSON으로 관리.**
|
||||
VEX는 비즈니스 테이블에 DB 레벨 FK 제약조건을 걸지 않는다. 관계 정보는 전부 메타데이터 테이블에서 관리한다.
|
||||
이유: 유연성 (마이그레이션 없이 관계 변경), 멀티테넌시 (회사별 관계 설정), 데이터 이관 시 제약조건 우회 가능.
|
||||
|
||||
#### Step 5 병합 규칙:
|
||||
|
||||
```
|
||||
FieldConfig.column = information_schema.column_name
|
||||
FieldConfig.label = table_type_columns.column_label (있으면) || column_name을 한글화 시도 (없으면 그대로)
|
||||
FieldConfig.type = table_type_columns.input_type 매핑 (있으면) || data_type 매핑
|
||||
FieldConfig.visible = true (기본값, table_type_columns에서 오버라이드 가능)
|
||||
FieldConfig.order = table_type_columns.display_order (있으면) || information_schema.ordinal_position
|
||||
FieldConfig.required = (is_nullable = 'NO') 이고 (column_default가 없으면) true
|
||||
FieldConfig.editable = PK가 아니고 system이 아니면 true
|
||||
FieldConfig.pk = PK 컬럼이면 true
|
||||
FieldConfig.system = column_name이 'company_code', 'created_by', 'created_date', 'updated_by', 'updated_date', 'is_active' 중 하나면 true
|
||||
FieldConfig.searchable = table_type_columns에서 지정 (없으면 text/select/entity/date만 true)
|
||||
FieldConfig.sortable = true (기본값)
|
||||
FieldConfig.ref = table_type_columns.input_type='entity'이면 detail_settings에서 { table, value_column, display_column } 추출
|
||||
FieldConfig.options = table_type_columns.detail_settings에서 options 추출 (select 타입)
|
||||
FieldConfig.format = number이면 '#,##0', date이면 'YYYY-MM-DD', datetime이면 'YYYY-MM-DD HH:mm'
|
||||
FieldConfig.width = type에 따라 기본값: text=150, number=100, date=120, select=130, entity=180
|
||||
FieldConfig.align = number이면 'right', 나머지 'left'
|
||||
```
|
||||
|
||||
### 3.4 백엔드 API 설계
|
||||
|
||||
#### 3.4.1 신규 API 엔드포인트
|
||||
|
||||
**`GET /api/meta/tables`** — 접근 가능한 테이블 목록
|
||||
|
||||
```
|
||||
Request: (없음, JWT에서 company_code 추출)
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"table_name": "order_management_test",
|
||||
"table_label": "수주관리",
|
||||
"column_count": 13,
|
||||
"has_custom_meta": true
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**`GET /api/meta/tables/{tableName}/fields`** — 특정 테이블의 FieldConfig[] 반환
|
||||
|
||||
```
|
||||
Request: GET /api/meta/tables/order_management_test/fields
|
||||
Headers: Authorization: Bearer {jwt}
|
||||
|
||||
Response (★ 백엔드 응답은 전부 snake_case, 프론트에서 camelCase 변환):
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"table_name": "order_management_test",
|
||||
"table_label": "수주관리",
|
||||
"primary_key": "order_no",
|
||||
"fields": [
|
||||
{
|
||||
"column": "order_no",
|
||||
"label": "수주번호",
|
||||
"type": "code",
|
||||
"visible": true,
|
||||
"order": 1,
|
||||
"width": 120,
|
||||
"align": "left",
|
||||
"required": true,
|
||||
"editable": false,
|
||||
"pk": true,
|
||||
"system": false,
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"format": null,
|
||||
"options": null,
|
||||
"ref": null,
|
||||
"computed": null
|
||||
},
|
||||
{
|
||||
"column": "order_date",
|
||||
"label": "수주일",
|
||||
"type": "date",
|
||||
"visible": true,
|
||||
"order": 2,
|
||||
"width": 120,
|
||||
"align": "left",
|
||||
"required": true,
|
||||
"editable": true,
|
||||
"pk": false,
|
||||
"system": false,
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"format": "YYYY-MM-DD",
|
||||
"options": null,
|
||||
"ref": null,
|
||||
"computed": null
|
||||
},
|
||||
{
|
||||
"column": "customer",
|
||||
"label": "거래처",
|
||||
"type": "entity",
|
||||
"visible": true,
|
||||
"order": 3,
|
||||
"width": 180,
|
||||
"align": "left",
|
||||
"required": true,
|
||||
"editable": true,
|
||||
"pk": false,
|
||||
"system": false,
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"format": null,
|
||||
"options": null,
|
||||
"ref": {
|
||||
"table": "customer_mng",
|
||||
"value_column": "customer_code",
|
||||
"display_column": "customer_name",
|
||||
"search_columns": ["customer_name", "biz_number"]
|
||||
},
|
||||
"computed": null
|
||||
},
|
||||
{
|
||||
"column": "amount",
|
||||
"label": "금액",
|
||||
"type": "number",
|
||||
"visible": true,
|
||||
"order": 7,
|
||||
"width": 100,
|
||||
"align": "right",
|
||||
"required": false,
|
||||
"editable": false,
|
||||
"pk": false,
|
||||
"system": false,
|
||||
"searchable": false,
|
||||
"sortable": true,
|
||||
"format": "#,##0",
|
||||
"options": null,
|
||||
"ref": null,
|
||||
"computed": "quantity * unit_price"
|
||||
},
|
||||
{
|
||||
"column": "status",
|
||||
"label": "상태",
|
||||
"type": "select",
|
||||
"visible": true,
|
||||
"order": 9,
|
||||
"width": 100,
|
||||
"align": "center",
|
||||
"required": false,
|
||||
"editable": true,
|
||||
"pk": false,
|
||||
"system": false,
|
||||
"searchable": true,
|
||||
"sortable": true,
|
||||
"format": null,
|
||||
"options": ["임시저장", "확정", "완료", "취소"],
|
||||
"ref": null,
|
||||
"computed": null
|
||||
},
|
||||
{
|
||||
"column": "company_code",
|
||||
"label": "회사코드",
|
||||
"type": "text",
|
||||
"visible": false,
|
||||
"order": 99,
|
||||
"required": false,
|
||||
"editable": false,
|
||||
"pk": false,
|
||||
"system": true,
|
||||
"searchable": false,
|
||||
"sortable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**★ 관계 정보 2소스 책임 분리 (B안 확정):**
|
||||
|
||||
| 소스 | 용도 | 사용 Phase |
|
||||
|---|---|---|
|
||||
| `table_type_columns.detail_settings` | **필드 레벨 참조** — FieldConfig.ref, entity picker, 폼/검색/테이블 렌더링 | Phase 1 (`getTableFields`) |
|
||||
| `table_relationships` | **테이블 간 관계** — 제어 모드, 데이터플로우, 업무적 연결 (one-to-many 등) | Phase 5 (`getMetaRelations`) |
|
||||
|
||||
- Phase 1의 `getTableFields()`는 `table_type_columns`만 읽는다 (필드 참조 원천)
|
||||
- Phase 5의 관계 그래프는 `table_relationships`를 읽는다 (업무 관계 원천)
|
||||
- 제어 모드에서 두 소스가 필요하면 **서비스 레이어에서 읽기 시점에만 합친다** (저장 구조 통합 안 함)
|
||||
|
||||
따라서 **이 Phase 1에서는 relations API를 만들지 않는다.** `getTableFields()`가 `table_type_columns`에서 entity 참조(ref)를 추출하는 것까지만 책임진다. `table_relationships` 기반 관계 API는 Phase 5에서 구현한다.
|
||||
|
||||
#### 3.4.2 구현할 Java 파일 (★ 파이프라인 규칙 준수)
|
||||
|
||||
**★ 덕일 스타일 3레이어. Mapper Interface 금지. 파일명 1:1 매칭.**
|
||||
|
||||
| 파일 | 경로 | 비고 |
|
||||
|---|---|---|
|
||||
| `MetaController.java` | `controller/` | `/api/meta/*` |
|
||||
| `MetaService.java` | `service/` | `extends BaseService` |
|
||||
| `meta.xml` | `resources/mapper/` | `namespace="meta"` (소문자, Mapper 안 붙임) |
|
||||
|
||||
**MetaController.java:**
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/meta")
|
||||
@Slf4j
|
||||
public class MetaController {
|
||||
|
||||
@Autowired
|
||||
private MetaService metaService;
|
||||
|
||||
@GetMapping("/tables")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getMetaTableList(
|
||||
@RequestAttribute("companyCode") String companyCode) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("company_code", companyCode);
|
||||
return ResponseEntity.ok(ApiResponse.success(metaService.getMetaTableList(params)));
|
||||
}
|
||||
|
||||
@GetMapping("/tables/{tableName}/fields")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getMetaFields(
|
||||
@PathVariable String tableName,
|
||||
@RequestAttribute("companyCode") String companyCode) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("table_name", tableName);
|
||||
params.put("company_code", companyCode);
|
||||
return ResponseEntity.ok(ApiResponse.success(metaService.getMetaFields(params)));
|
||||
}
|
||||
|
||||
// ★ relations API는 Phase 1에서 안 만듦
|
||||
// table_relationships 기반 관계 조회는 Phase 5 (제어 모드)에서 구현
|
||||
// Phase 1은 getTableFields()의 FieldConfig.ref (entity 참조)까지만 책임
|
||||
}
|
||||
```
|
||||
|
||||
**MetaService.java:**
|
||||
|
||||
```java
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MetaService extends BaseService {
|
||||
|
||||
@Autowired
|
||||
private CommonService commonService;
|
||||
|
||||
/** 테이블 목록 */
|
||||
public List<Map<String, Object>> getMetaTableList(Map<String, Object> params) {
|
||||
return sqlSession.selectList("meta.getMetaTableList", params);
|
||||
}
|
||||
|
||||
/** 특정 테이블의 FieldConfig[] 반환 */
|
||||
public Map<String, Object> getMetaFields(Map<String, Object> params) {
|
||||
List<Map<String, Object>> schemaCols = sqlSession.selectList("meta.getSchemaColumns", params);
|
||||
List<String> pks = sqlSession.selectList("meta.getPrimaryKeys", params);
|
||||
List<Map<String, Object>> customMeta = sqlSession.selectList("meta.getCustomMeta", params);
|
||||
// ★ FK는 DB 제약조건이 아닌 customMeta(table_type_columns)의 detail_settings에서 추출
|
||||
// input_type='entity'인 컬럼의 detail_settings JSON → referenceTable/referenceColumn/displayColumn
|
||||
|
||||
List<Map<String, Object>> fields = buildFieldConfigs(schemaCols, pks, customMeta);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("table_name", params.get("table_name"));
|
||||
result.put("fields", fields);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ★ relations 조회는 Phase 5에서 table_relationships 기반으로 구현
|
||||
// Phase 1은 getMetaFields()의 FieldConfig.ref (entity 참조)까지만 책임
|
||||
|
||||
/** 병합하여 FieldConfig Map 리스트 생성 (상세 로직은 Section 3.3 참고) */
|
||||
private List<Map<String, Object>> buildFieldConfigs(
|
||||
List<Map<String, Object>> schemaCols,
|
||||
List<String> pks,
|
||||
List<Map<String, Object>> customMeta
|
||||
) {
|
||||
// column, label, type, visible, order, required, editable, pk, system 등
|
||||
// customMeta가 있으면 우선 적용
|
||||
// ★ input_type='entity'이면 detail_settings JSON 파싱 → ref Map 생성
|
||||
// ObjectMapper로 detail_settings 파싱 → referenceTable, referenceColumn, displayColumn 추출
|
||||
// system 컬럼은 visible=false, system=true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4.3 MyBatis SQL — `meta.xml` (★ 덕일 스타일)
|
||||
|
||||
**★ 파일명: `meta.xml` (소문자, Mapper 안 붙임)**
|
||||
**★ namespace: `meta`**
|
||||
**★ SQL 키워드/테이블/컬럼: UPPER_SNAKE, #{파라미터}: snake_case**
|
||||
**★ SELECT 쉼표 앞에, OGNL test 바깥 작은따옴표**
|
||||
|
||||
```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="meta">
|
||||
|
||||
<!-- ═══ 테이블 목록 (public 스키마, 시스템 테이블 제외) ═══ -->
|
||||
<select id="getMetaTableList" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
T.TABLE_NAME
|
||||
, (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS C
|
||||
WHERE C.TABLE_SCHEMA = 'public' AND C.TABLE_NAME = T.TABLE_NAME) AS COLUMN_COUNT
|
||||
, CASE WHEN EXISTS(
|
||||
SELECT 1 FROM TABLE_TYPE_COLUMNS TTC
|
||||
WHERE TTC.TABLE_NAME = T.TABLE_NAME
|
||||
<if test='company_code != null and company_code != "*"'>
|
||||
AND (TTC.COMPANY_CODE = #{company_code} OR TTC.COMPANY_CODE = '*')
|
||||
</if>
|
||||
) THEN true ELSE false END AS HAS_CUSTOM_META
|
||||
FROM INFORMATION_SCHEMA.TABLES T
|
||||
WHERE T.TABLE_SCHEMA = 'public'
|
||||
AND T.TABLE_TYPE = 'BASE TABLE'
|
||||
AND T.TABLE_NAME NOT LIKE 'pg_%'
|
||||
AND T.TABLE_NAME NOT IN ('spatial_ref_sys')
|
||||
ORDER BY T.TABLE_NAME
|
||||
</select>
|
||||
|
||||
<!-- ═══ 특정 테이블의 컬럼 정보 ═══ -->
|
||||
<select id="getSchemaColumns" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
COLUMN_NAME
|
||||
, DATA_TYPE
|
||||
, IS_NULLABLE
|
||||
, COLUMN_DEFAULT
|
||||
, CHARACTER_MAXIMUM_LENGTH
|
||||
, ORDINAL_POSITION
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'public'
|
||||
AND TABLE_NAME = #{table_name}
|
||||
ORDER BY ORDINAL_POSITION
|
||||
</select>
|
||||
|
||||
<!-- ═══ PK 컬럼 목록 ═══ -->
|
||||
<select id="getPrimaryKeys" parameterType="map" resultType="string">
|
||||
SELECT KCU.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
|
||||
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU
|
||||
ON TC.CONSTRAINT_NAME = KCU.CONSTRAINT_NAME
|
||||
AND TC.TABLE_SCHEMA = KCU.TABLE_SCHEMA
|
||||
WHERE TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
AND TC.TABLE_SCHEMA = 'public'
|
||||
AND TC.TABLE_NAME = #{table_name}
|
||||
</select>
|
||||
|
||||
<!-- ═══ 관계 조회 (★ DB FK 제약조건이 아닌 table_type_columns 메타 기반) ═══ -->
|
||||
<select id="getMetaRelations" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
TABLE_NAME AS FROM_TABLE
|
||||
, COLUMN_NAME AS FROM_COLUMN
|
||||
, INPUT_TYPE
|
||||
, DETAIL_SETTINGS
|
||||
FROM TABLE_TYPE_COLUMNS
|
||||
WHERE TABLE_NAME = #{table_name}
|
||||
AND INPUT_TYPE IN ('entity', 'category')
|
||||
AND DETAIL_SETTINGS IS NOT NULL
|
||||
<if test='company_code != null and company_code != "*"'>
|
||||
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
|
||||
</if>
|
||||
</select>
|
||||
<!--
|
||||
★ detail_settings JSON에서 참조 정보 추출 (Java Service에서 파싱):
|
||||
{
|
||||
"referenceTable": "partner_info",
|
||||
"referenceColumn": "id",
|
||||
"displayColumn": "partner_name"
|
||||
}
|
||||
→ FieldConfig.ref = { table, value_column, display_column } 로 변환
|
||||
-->
|
||||
|
||||
<!-- ═══ TABLE_TYPE_COLUMNS 커스텀 메타 (회사 우선, '*' 폴백) ═══ -->
|
||||
<select id="getCustomMeta" parameterType="map" resultType="map">
|
||||
SELECT DISTINCT ON (COLUMN_NAME)
|
||||
COLUMN_NAME
|
||||
, INPUT_TYPE
|
||||
, DETAIL_SETTINGS
|
||||
, DISPLAY_ORDER
|
||||
, IS_NULLABLE
|
||||
, COMPANY_CODE
|
||||
FROM TABLE_TYPE_COLUMNS
|
||||
WHERE TABLE_NAME = #{table_name}
|
||||
<if test='company_code != null and company_code != "*"'>
|
||||
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
|
||||
</if>
|
||||
ORDER BY COLUMN_NAME
|
||||
, CASE WHEN COMPANY_CODE = #{company_code} THEN 0 ELSE 1 END
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
```
|
||||
|
||||
### 3.5 프론트엔드 API 클라이언트
|
||||
|
||||
**신규 파일: `frontend/lib/api/meta.ts`**
|
||||
|
||||
```typescript
|
||||
import { client } from './client';
|
||||
|
||||
import type { FieldConfig } from '@/types/invyone-component';
|
||||
|
||||
/**
|
||||
* ★ 일반 API는 Record<string, any> — 별도 인터페이스 정의 안 함
|
||||
* ★ 단, FieldConfig만은 invyone-component.ts에 규격 확정된 예외 타입
|
||||
* → getTableFields()의 fields 배열은 FieldConfig[]로 캐스팅
|
||||
*/
|
||||
|
||||
/** 접근 가능한 테이블 목록 */
|
||||
export async function getTableList(): Promise<Record<string, any>[]> {
|
||||
const res = await client.get('/api/meta/tables');
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
/** 특정 테이블의 FieldConfig[] 반환 (★ fields만 FieldConfig[] 타입) */
|
||||
export async function getTableFields(tableName: string): Promise<{
|
||||
table_name: string;
|
||||
fields: FieldConfig[];
|
||||
[key: string]: any;
|
||||
}> {
|
||||
const res = await client.get(`/api/meta/tables/${tableName}/fields`);
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
// ★ relations API는 Phase 5에서 구현 (table_relationships 기반)
|
||||
// Phase 1은 getTableFields()의 FieldConfig.ref까지만 책임
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 시스템 필드 판별 규칙
|
||||
|
||||
다음 컬럼명은 자동으로 `system: true`로 설정하고, `visible: false`, `editable: false`로 만든다:
|
||||
|
||||
```
|
||||
company_code, created_by, created_date, updated_by, updated_date,
|
||||
is_active, deleted_date, deleted_by, writer, write_date
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 참고 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|---|---|
|
||||
| `frontend/types/invyone-component.ts` | **FieldConfig 타입 정의 (진실의 원천)** — 이 파일의 인터페이스와 정확히 일치하는 JSON을 반환해야 함 |
|
||||
| `notes/gbpark/2026-04-09-invyone-architecture.md` | 아키텍처 결정 문서 — 변환 규칙의 근거 |
|
||||
| `notes/gbpark/2026-04-08-invyone-component-spec.md` | 컴포넌트 규격 — FieldType별 렌더링 계약 |
|
||||
| `backend-spring/src/main/java/com/erp/service/TableManagementService.java` | 기존 테이블 관리 서비스 — 참고용 (패턴) |
|
||||
| `backend-spring/src/main/resources/mapper/TableManagementMapper.xml` | 기존 MyBatis 매퍼 — SQL 패턴 참고 |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/js/08-admin-builder.js` | mockup의 테이블 메타 데이터 — `AB_TABLE_FIELDS` 객체가 FieldConfig의 mockup 버전 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 완료 기준
|
||||
|
||||
1. **`GET /api/meta/tables`** 호출 시 DB의 public 스키마 테이블 목록이 반환된다
|
||||
2. **`GET /api/meta/tables/{tableName}/fields`** 호출 시:
|
||||
- `FieldConfig[]` 형태의 JSON이 반환된다
|
||||
- `frontend/types/invyone-component.ts`의 `FieldConfig` 인터페이스와 필드명이 정확히 일치한다
|
||||
- PK 컬럼에 `pk: true`가 설정된다
|
||||
- system 컬럼(company_code 등)에 `system: true`, `visible: false`가 설정된다
|
||||
- `table_type_columns`에 커스텀 메타가 있으면 그것이 우선 적용된다
|
||||
- select 타입 필드에 `options[]`가 포함된다
|
||||
- entity 타입 필드에 `ref` 객체가 포함된다
|
||||
3. ~~relations API는 Phase 1에서 안 만듦~~ (Phase 5에서 `table_relationships` 기반으로 구현)
|
||||
4. 프론트엔드 `lib/api/meta.ts`에서 위 API들을 호출하는 함수가 있다
|
||||
5. 실제 DB 테이블 (예: `order_management_test`, `user_info`)로 테스트하여 합리적인 FieldConfig가 생성됨을 확인
|
||||
|
||||
---
|
||||
|
||||
## 7. 다음 단계 연결
|
||||
|
||||
Phase 2는 이 API가 반환하는 `FieldConfig[]`을 받아서:
|
||||
- **테이블 컴포넌트**: FieldConfig[]을 컬럼으로 렌더링
|
||||
- **폼 컴포넌트**: FieldConfig[]을 입력 필드로 렌더링
|
||||
- **검색 컴포넌트**: FieldConfig[]을 검색 조건으로 렌더링
|
||||
|
||||
Phase 2의 컴포넌트들은 `getTableFields(tableName)`을 호출하여 FieldConfig[]을 가져온 뒤 렌더링한다.
|
||||
@@ -0,0 +1,298 @@
|
||||
# Phase 1 구현 작업기록 — DB 메타 → FieldConfig 변환
|
||||
|
||||
> **작업일**: 2026-04-10
|
||||
> **설계서**: `notes/gbpark/2026-04-10-phase1-db-meta-to-fieldconfig.md`
|
||||
> **상태**: 구현 완료 + DB 실테스트 통과
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성된 파일 (4개)
|
||||
|
||||
### 1.1 `backend-spring/src/main/resources/mapper/meta.xml`
|
||||
|
||||
MyBatis XML 매퍼. namespace=`meta`, 쿼리 5개:
|
||||
|
||||
| 쿼리 ID | 용도 | 반환 |
|
||||
|---|---|---|
|
||||
| `getMetaTableList` | public 스키마 테이블 목록 | table_name, table_label, column_count, has_custom_meta |
|
||||
| `getSchemaColumns` | information_schema.columns 전체 컬럼 | column_name, data_type, is_nullable, column_default, ordinal_position |
|
||||
| `getPrimaryKeys` | PK 컬럼명 목록 | string (column_name) |
|
||||
| `getCustomMeta` | table_type_columns 커스텀 메타 (회사 우선순위) | column_name, column_label, input_type, detail_settings, reference_table 등 |
|
||||
| `getTableLabel` | TABLE_LABELS에서 라벨 단건 | string (table_label) |
|
||||
|
||||
**회사 우선순위 처리**: `getCustomMeta`에서 `DISTINCT ON (COLUMN_NAME)` + `CASE WHEN COMPANY_CODE = #{company_code} THEN 0 ELSE 1 END` 정렬로 회사별 메타가 '*'(글로벌)보다 우선.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 `backend-spring/src/main/java/com/erp/service/MetaService.java`
|
||||
|
||||
핵심 변환 로직. 덕일 스타일 준수 — extends BaseService, @Autowired CommonService, sqlSession 직접 호출.
|
||||
|
||||
#### 주요 메서드
|
||||
|
||||
| 메서드 | 역할 |
|
||||
|---|---|
|
||||
| `getMetaTableList(params)` | 테이블 목록 pass-through |
|
||||
| `getMetaFields(params)` | 4개 쿼리 오케스트레이션 → buildFieldConfigs 호출 → 결과 조립 |
|
||||
| `buildFieldConfigs(schemaCols, pks, customMeta)` | **핵심** — 3소스 병합하여 FieldConfig Map 리스트 생성 |
|
||||
| `mapDataTypeToFieldType(dataType)` | PostgreSQL data_type → FieldType 변환 (12개 매핑) |
|
||||
| `mapInputTypeToFieldType(inputType)` | table_type_columns.input_type → FieldType 변환 (15개 매핑) |
|
||||
| `buildFieldRef(meta, detailSettings)` | entity 타입의 ref 객체 빌드 (top-level 컬럼 우선 → detail_settings 폴백) |
|
||||
| `extractOptions(detailSettings)` | select 타입의 options 추출 (`string[]` 및 `[{value,label}]` 둘 다 처리) |
|
||||
| `parseDetailSettings(meta)` | detail_settings JSONB → Map 파싱 (ObjectMapper) |
|
||||
|
||||
#### buildFieldConfigs 병합 규칙 (필드별)
|
||||
|
||||
```
|
||||
column = information_schema.column_name
|
||||
label = table_type_columns.column_label (있으면) || column_name
|
||||
type = table_type_columns.input_type 매핑 (있으면) || data_type 매핑
|
||||
visible = system이면 무조건 false, 아니면 is_visible
|
||||
order = display_order > 0이면 우선 || ordinal_position
|
||||
required = ★ 앱 레벨 메타(table_type_columns.is_nullable) 우선, 없으면 DB 스키마 폴백
|
||||
editable = !PK && !system && type!='code'
|
||||
pk = information_schema PK 제약조건
|
||||
system = SYSTEM_FIELDS 목록에 포함 여부
|
||||
searchable = !system && (text|select|entity|date|code)
|
||||
sortable = !system
|
||||
format = number→'#,##0', date→'YYYY-MM-DD', datetime→'YYYY-MM-DD HH:mm'
|
||||
width = type별 기본값 (number:100, date:120, entity:180 등)
|
||||
align = number→'right', 나머지→'left'
|
||||
options = detail_settings.options (string[] 또는 [{value,label}] → label 추출)
|
||||
ref = entity일 때 reference_table/column/display_column (top-level 우선 → detail_settings 폴백)
|
||||
computed = detail_settings.computed (있으면 editable=false 강제)
|
||||
```
|
||||
|
||||
#### required 판정 로직 (★ 수동 수정 반영)
|
||||
|
||||
```java
|
||||
// ★ 앱 레벨 메타 우선, DB 스키마 폴백
|
||||
if (!isSystem && meta != null && meta.get("is_nullable") != null) {
|
||||
// table_type_columns.IS_NULLABLE가 있으면 앱 레벨 메타가 진실
|
||||
required = "NO".equalsIgnoreCase(metaNullable);
|
||||
} else {
|
||||
// 없으면 information_schema 폴백
|
||||
required = "NO".equalsIgnoreCase(isNullable) && columnDefault == null && !isSystem;
|
||||
}
|
||||
```
|
||||
|
||||
원래 구현: information_schema.is_nullable + column_default 기반.
|
||||
수정 이유: VEX에서 앱 레벨 메타(table_type_columns.IS_NULLABLE)가 DB 스키마보다 우선하는 패턴. 관리자가 설정한 필수 여부가 DB 제약조건보다 비즈니스적으로 정확함.
|
||||
|
||||
#### system 필드 목록
|
||||
|
||||
```
|
||||
company_code, created_by, created_date, updated_by, updated_date,
|
||||
is_active, deleted_date, deleted_by, writer, write_date
|
||||
```
|
||||
|
||||
→ 자동으로 `visible: false`, `editable: false`, `required: false`, `searchable: false`, `sortable: false`
|
||||
|
||||
#### 타입 매핑 테이블
|
||||
|
||||
**PostgreSQL data_type → FieldType:**
|
||||
|
||||
| data_type | FieldType |
|
||||
|---|---|
|
||||
| character varying, varchar | text |
|
||||
| text | textarea |
|
||||
| integer, bigint, smallint | number |
|
||||
| numeric, decimal, real, double precision | number |
|
||||
| boolean | checkbox |
|
||||
| date | date |
|
||||
| timestamp (with/without tz) | datetime |
|
||||
| jsonb, json | textarea |
|
||||
| bytea | file |
|
||||
| 기타 | text |
|
||||
|
||||
**input_type → FieldType (custom meta 우선):**
|
||||
|
||||
| input_type | FieldType |
|
||||
|---|---|
|
||||
| text, email, password, tel | text |
|
||||
| number, decimal | number |
|
||||
| date | date |
|
||||
| datetime | datetime |
|
||||
| select, category | select |
|
||||
| entity | entity |
|
||||
| checkbox, boolean | checkbox |
|
||||
| textarea, text_area | textarea |
|
||||
| file | file |
|
||||
| code, numbering | code |
|
||||
| 기타 | text |
|
||||
|
||||
#### entity ref 빌드 로직
|
||||
|
||||
```
|
||||
1차: top-level 컬럼 (REFERENCE_TABLE, REFERENCE_COLUMN, DISPLAY_COLUMN)
|
||||
2차: detail_settings JSON 폴백 (referenceTable, referenceColumn, displayColumn)
|
||||
→ table이 없으면 ref=null
|
||||
→ value_column 기본값 "id", display_column 기본값 = value_column
|
||||
→ search_columns는 detail_settings에서만 추출
|
||||
```
|
||||
|
||||
#### select options 추출 로직
|
||||
|
||||
```
|
||||
detail_settings.options 필드에서 추출
|
||||
- string[] 형식: ["대기", "확정", ...] → 그대로 반환
|
||||
- [{value, label}] 형식: [{value:"PENDING", label:"대기"}, ...] → label만 추출
|
||||
- options 없으면 null (category 타입은 common code 연동 필요 — Phase 1 범위 밖)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 `backend-spring/src/main/java/com/erp/controller/MetaController.java`
|
||||
|
||||
REST 컨트롤러. 2개 엔드포인트:
|
||||
|
||||
| 엔드포인트 | 메서드 | 설명 |
|
||||
|---|---|---|
|
||||
| `GET /api/meta/tables` | `getMetaTableList` | 접근 가능한 테이블 목록 |
|
||||
| `GET /api/meta/tables/{tableName}/fields` | `getMetaFields` | 특정 테이블의 FieldConfig[] 반환 |
|
||||
|
||||
- `@RequestAttribute("company_code")` — JWT 필터에서 주입 (snake_case, 기존 패턴 따름)
|
||||
- `@RequiredArgsConstructor` + `private final MetaService` — 기존 컨트롤러 패턴 따름
|
||||
- relations API는 Phase 1에서 안 만듦 (Phase 5 담당)
|
||||
|
||||
---
|
||||
|
||||
### 1.4 `frontend/lib/api/meta.ts`
|
||||
|
||||
프론트엔드 API 클라이언트. 2개 함수:
|
||||
|
||||
| 함수 | 반환 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| `getMetaTableList()` | `Record<string, any>[]` | 테이블 목록 (별도 인터페이스 안 만듦) |
|
||||
| `getMetaFields(tableName)` | `{table_name, table_label, primary_key, fields: FieldConfig[]}` | FieldConfig 규격 타입 사용 (유일한 예외) |
|
||||
|
||||
**snake_case → camelCase 변환 처리:**
|
||||
- `toFieldConfig(raw)` — 대부분 단일 단어라 변환 불필요, `default_value` → `defaultValue`만 처리
|
||||
- `toFieldRef(raw)` — `value_column` → `valueColumn`, `display_column` → `displayColumn`, `search_columns` → `searchColumns`
|
||||
- `apiClient` 사용 (baseURL에 `/api` 이미 포함 → `/meta/tables`로 호출)
|
||||
|
||||
---
|
||||
|
||||
## 2. 덕일 스타일 준수 체크리스트
|
||||
|
||||
| 규칙 | 준수 |
|
||||
|---|---|
|
||||
| 3레이어 (Controller → Service → XML) | O |
|
||||
| Mapper Interface 금지 | O — sqlSession 직접 호출 |
|
||||
| Map<String, Object> 사용, DTO 금지 | O — ApiResponse만 예외 |
|
||||
| BaseService 상속 | O |
|
||||
| @Autowired CommonService | O |
|
||||
| XML 파일명: 소문자, Mapper 안 붙임 | O — `meta.xml` |
|
||||
| XML namespace: 파일명과 동일 | O — `namespace="meta"` |
|
||||
| SQL: UPPER_SNAKE | O |
|
||||
| SELECT 쉼표: 앞에 | O |
|
||||
| #{파라미터}: snake_case | O |
|
||||
| OGNL test: 바깥 작은따옴표 | O |
|
||||
| 프론트 타입: Record<string, any> (FieldConfig만 예외) | O |
|
||||
|
||||
---
|
||||
|
||||
## 3. 실제 DB 테스트 결과
|
||||
|
||||
**테스트 환경**: `test_dev` DB (211.115.91.141:11134)
|
||||
**테스트 계정**: shkim (SUPER_ADMIN)
|
||||
**테스트 테이블**: `sales_order_mng` (entity/select/category/number/date 타입 다수 보유)
|
||||
|
||||
### 3.1 GET /api/meta/tables
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{"table_name": "approval_definitions", "table_label": "approval_definitions", "column_count": 15, "has_custom_meta": false},
|
||||
{"table_name": "sales_order_mng", "table_label": "수주관리", "column_count": 44, "has_custom_meta": true},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 GET /api/meta/tables/sales_order_mng/fields
|
||||
|
||||
**검증 결과:**
|
||||
|
||||
| 검증 항목 | 결과 | 상세 |
|
||||
|---|---|---|
|
||||
| table_label | OK | "수주관리" (TABLE_LABELS에서) |
|
||||
| primary_key | OK | "id" |
|
||||
| PK 감지 | OK | id: `pk:true, editable:false` |
|
||||
| system 필드 | OK | company_code, created_by/date, updated_by/date, writer → `visible:false, system:true` |
|
||||
| entity + ref | OK | partner_id: `ref:{table:"customer_mng", value_column:"customer_code", display_column:"customer_name"}` |
|
||||
| entity (top-level 컬럼) | OK | delivery_partner_id: `ref:{table:"delivery_destination", ...}` |
|
||||
| select + options ({value,label}) | OK | shipping_method: `options:["직접배송","택배","화물","퀵서비스"]` |
|
||||
| select + options (문자열) | OK | status: `options:["대기","확정","출하","완료","취소"]` |
|
||||
| custom meta 우선 | OK | id(integer) → table_type_columns에 input_type='text' 설정 → type:"text" |
|
||||
| date 포맷 | OK | `format:"YYYY-MM-DD"` |
|
||||
| number 포맷/정렬 | OK | `format:"#,##0", align:"right"` |
|
||||
| 컴파일 | OK | `./gradlew compileJava` + `./gradlew bootJar` 성공 |
|
||||
| 프론트 타입체크 | OK | `npx tsc --noEmit` — meta.ts 에러 0개 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 1 범위 밖 (나중에 해야 할 것)
|
||||
|
||||
| 항목 | 담당 Phase | 비고 |
|
||||
|---|---|---|
|
||||
| relations API (table_relationships) | Phase 5 | 제어 모드에서 사용 |
|
||||
| category 타입의 common code 옵션 로딩 | Phase 2+ | CODE_CATEGORY → 공통코드 테이블 조회 |
|
||||
| entity ref가 없는 entity 필드 처리 | Phase 2+ | part_code, manager_id 등 — reference_table 미설정 |
|
||||
| FieldConfig.defaultValue, placeholder 세팅 | Phase 2+ | detail_settings에서 추출 가능하나 현재 미구현 |
|
||||
| select options의 value/label 분리 저장 | Phase 2+ | 현재 label만 string[]로 반환, value 매핑 필요 시 타입 확장 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 파일별 코드 요약
|
||||
|
||||
### meta.xml (89줄)
|
||||
|
||||
```
|
||||
5개 쿼리:
|
||||
- getMetaTableList: INFORMATION_SCHEMA.TABLES + TABLE_LABELS + TABLE_TYPE_COLUMNS EXISTS
|
||||
- getSchemaColumns: INFORMATION_SCHEMA.COLUMNS
|
||||
- getPrimaryKeys: TABLE_CONSTRAINTS + KEY_COLUMN_USAGE
|
||||
- getCustomMeta: TABLE_TYPE_COLUMNS (DISTINCT ON + 회사 우선순위)
|
||||
- getTableLabel: TABLE_LABELS 단건
|
||||
```
|
||||
|
||||
### MetaService.java (375줄)
|
||||
|
||||
```
|
||||
퍼블릭 메서드 2개:
|
||||
- getMetaTableList(params) → List<Map>
|
||||
- getMetaFields(params) → Map (table_name, table_label, primary_key, fields[])
|
||||
|
||||
프라이빗 메서드 10개:
|
||||
- buildFieldConfigs: 3소스 병합 (핵심, ~120줄)
|
||||
- mapDataTypeToFieldType: PG타입→FieldType (12매핑)
|
||||
- mapInputTypeToFieldType: input_type→FieldType (15매핑)
|
||||
- buildFieldRef: entity ref 조립 (top-level 우선 → detail_settings 폴백)
|
||||
- extractOptions: select options 추출 (string[] + {value,label} 대응)
|
||||
- parseDetailSettings: JSONB→Map (ObjectMapper)
|
||||
- getDefaultWidth: 타입별 기본 너비
|
||||
- getDefaultFormat: 타입별 기본 포맷
|
||||
- str, strFromMap, num: Map 유틸
|
||||
```
|
||||
|
||||
### MetaController.java (45줄)
|
||||
|
||||
```
|
||||
2개 엔드포인트:
|
||||
- GET /api/meta/tables → getMetaTableList
|
||||
- GET /api/meta/tables/{tableName}/fields → getMetaFields
|
||||
```
|
||||
|
||||
### meta.ts (86줄)
|
||||
|
||||
```
|
||||
2개 API 함수:
|
||||
- getMetaTableList() → Record<string, any>[]
|
||||
- getMetaFields(tableName) → {fields: FieldConfig[], ...}
|
||||
|
||||
2개 변환 헬퍼:
|
||||
- toFieldConfig(raw) → FieldConfig
|
||||
- toFieldRef(raw) → FieldRef (snake→camel)
|
||||
```
|
||||
@@ -0,0 +1,386 @@
|
||||
# Phase 2: 규격 기반 컴포넌트 — FieldConfig로 테이블/폼/검색 렌더링
|
||||
|
||||
> **목적**: FieldConfig[]을 입력으로 받아 테이블/폼/검색 UI를 렌더링하는 React 컴포넌트 구현
|
||||
> **전제 조건**: Phase 1 완료 (GET /api/meta/tables/{tableName}/fields API가 FieldConfig[]을 반환)
|
||||
> **산출물**: FieldConfig 기반 3대 핵심 컴포넌트 (FcTable, FcForm, FcSearch) + 보조 컴포넌트 (FcButton, FcButtonBar, FcPagination)
|
||||
> **다음 단계**: Phase 3에서 이 컴포넌트들을 개발자 빌더 캔버스에 배치
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 원칙
|
||||
|
||||
**"같은 FieldConfig가 컴포넌트에 따라 다르게 렌더된다"**
|
||||
|
||||
```
|
||||
FieldConfig { column: 'order_date', label: '수주일', type: 'date' }
|
||||
|
||||
→ FcTable에서: <td>2026-04-08</td> (format 적용된 텍스트)
|
||||
→ FcForm에서: <DatePicker label="수주일" /> (입력 위젯)
|
||||
→ FcSearch에서: <DateRangePicker /> (범위 검색)
|
||||
```
|
||||
|
||||
이 렌더링 규칙은 `notes/gbpark/2026-04-08-invyone-component-spec.md` Section 2.2에 명시되어 있다. **반드시 이 매핑 테이블을 따를 것.**
|
||||
|
||||
---
|
||||
|
||||
## 2. 현재 존재하는 것
|
||||
|
||||
### 2.1 타입 정의 (이미 완성)
|
||||
|
||||
`frontend/types/invyone-component.ts` — FieldConfig, Component, DataPort, Template, 모든 Config 타입이 정의됨. **이 파일의 타입을 그대로 사용한다. 새로 만들지 말 것.**
|
||||
|
||||
### 2.2 기존 컴포넌트 (참고용, 직접 사용 금지)
|
||||
|
||||
기존 `frontend/lib/registry/components/`에 85개+ 컴포넌트가 있지만, 이것들은 VEX 규격(ColumnConfig 354줄 등)에 맞춰 만들어진 것이므로 **FieldConfig 기반 신규 컴포넌트를 만든다.** 기존 컴포넌트는 코드 패턴 참고용으로만 사용.
|
||||
|
||||
### 2.3 UI 라이브러리 (활용)
|
||||
|
||||
| 라이브러리 | 용도 |
|
||||
|---|---|
|
||||
| Radix UI | 기본 UI 프리미티브 (Dialog, Select, Checkbox 등) |
|
||||
| TanStack Table v8 | 테이블 렌더링 |
|
||||
| React Hook Form v7 | 폼 상태 관리 |
|
||||
| Zod v4 | 폼 유효성 검증 |
|
||||
| date-fns v4 | 날짜 포맷팅 |
|
||||
| Lucide React | 아이콘 |
|
||||
|
||||
### 2.4 디자인 시스템
|
||||
|
||||
v5 Cosmic Glassmorphism (`frontend/styles/v5-layout.css`, `frontend/app/globals.css`). 모든 컴포넌트는 v5 토큰을 따른다. 즉흥 hex/rgb 금지.
|
||||
|
||||
---
|
||||
|
||||
## 3. 파일 구조
|
||||
|
||||
```
|
||||
frontend/components/fc/ ← FieldConfig 기반 컴포넌트 (fc = FieldConfig)
|
||||
├── FcTable.tsx ← 데이터 테이블
|
||||
├── FcForm.tsx ← 입력 폼
|
||||
├── FcSearch.tsx ← 검색 필터
|
||||
├── FcButton.tsx ← 단일 버튼
|
||||
├── FcButtonBar.tsx ← 버튼 그룹
|
||||
├── FcPagination.tsx ← 페이지네이션
|
||||
├── fields/ ← FieldType별 렌더러
|
||||
│ ├── FieldRenderer.tsx ← FieldType→위젯 디스패처
|
||||
│ ├── TextField.tsx
|
||||
│ ├── NumberField.tsx
|
||||
│ ├── DateField.tsx
|
||||
│ ├── DateTimeField.tsx
|
||||
│ ├── SelectField.tsx
|
||||
│ ├── EntityField.tsx
|
||||
│ ├── CheckboxField.tsx
|
||||
│ ├── TextareaField.tsx
|
||||
│ ├── FileField.tsx
|
||||
│ └── CodeField.tsx
|
||||
├── table/ ← 테이블 전용 셀 렌더러
|
||||
│ └── CellRenderer.tsx ← FieldType→셀 포맷 디스패처
|
||||
└── index.ts ← 공개 exports
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 상세 설계
|
||||
|
||||
### 4.1 FieldRenderer — FieldType→위젯 디스패처
|
||||
|
||||
모든 폼/검색 입력 필드를 렌더링하는 허브 컴포넌트.
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/fields/FieldRenderer.tsx
|
||||
|
||||
interface FieldRendererProps {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
mode: 'form' | 'search'; // 폼인지 검색인지에 따라 위젯이 달라짐
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FieldConfig.type을 보고 적절한 입력 위젯을 렌더한다.
|
||||
*
|
||||
* mode='form' 이면 폼 렌더링 규칙 적용:
|
||||
* date → DatePicker(단일), select → Select(단일)
|
||||
*
|
||||
* mode='search' 이면 검색 렌더링 규칙 적용:
|
||||
* date → DateRangePicker(범위), select → MultiSelect(다중)
|
||||
*/
|
||||
function FieldRenderer({ field, value, onChange, mode, disabled, error }: FieldRendererProps) {
|
||||
// field.type에 따라 switch/case로 분기
|
||||
// 각 케이스에서 fields/ 폴더의 개별 컴포넌트를 렌더
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 렌더링 계약 (★ 반드시 이 테이블대로 구현)
|
||||
|
||||
| FieldType | FcTable (셀) | FcForm (입력) | FcSearch (검색) |
|
||||
|---|---|---|---|
|
||||
| `text` | 텍스트 그대로 | `<input type="text">` | `<input>` (부분 일치) |
|
||||
| `number` | format 적용 (#,##0) | `<input type="number">` | min~max 범위 입력 2개 |
|
||||
| `date` | format 적용 (YYYY-MM-DD) | DatePicker (단일) | **DateRangePicker (시작~종료)** |
|
||||
| `datetime` | format 적용 | DateTimePicker | DateTimeRangePicker |
|
||||
| `select` | 텍스트 그대로 | `<Select>` (단일) | **MultiSelect (다중, 체크박스)** |
|
||||
| `entity` | ref.displayColumn 값 표시 | 팝업 검색 버튼 + 입력 | 팝업 검색 (단일) |
|
||||
| `checkbox` | ✓/✗ 아이콘 | `<Checkbox>` | `<Select>` (전체/✓/✗) |
|
||||
| `textarea` | 말줄임 (...) 40자 | `<textarea>` rows=3 | `<input>` (부분 일치) |
|
||||
| `file` | 파일명 링크 | 파일 업로드 | — (검색 불가, 렌더 안 함) |
|
||||
| `code` | 텍스트 그대로 | readonly 표시 | `<input>` (완전 일치) |
|
||||
|
||||
### 4.3 FcTable — 데이터 테이블
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/FcTable.tsx
|
||||
|
||||
interface FcTableProps {
|
||||
fields: FieldConfig[]; // 컬럼 정의
|
||||
data: Record<string, any>[]; // 행 데이터
|
||||
config?: TableConfig; // 테이블 설정 (invyone-component.ts)
|
||||
loading?: boolean;
|
||||
|
||||
// ─── DataPort 출력 (콜백) ───
|
||||
onRowSelect?: (row: Record<string, any>) => void; // selectedRow(row)
|
||||
onRowsSelect?: (rows: Record<string, any>[]) => void; // selectedRows(rows)
|
||||
|
||||
// ─── DataPort 입력 ───
|
||||
searchParams?: Record<string, any>; // 검색 조건 (적용 시 필터링/API 재호출)
|
||||
}
|
||||
```
|
||||
|
||||
**렌더링 규칙:**
|
||||
1. `fields`에서 `visible: true`인 것만 컬럼으로 표시
|
||||
2. `order` 순으로 정렬
|
||||
3. `width` 적용 (없으면 타입별 기본값)
|
||||
4. `align` 적용
|
||||
5. `sortable: true`인 컬럼은 헤더 클릭 시 정렬
|
||||
6. 셀 값은 `CellRenderer`가 FieldType에 따라 포맷팅
|
||||
7. 행 클릭 시 `onRowSelect` 호출
|
||||
8. 체크박스 선택 시 `onRowsSelect` 호출
|
||||
|
||||
**데이터 조회:**
|
||||
- `config.autoLoad: true`이면 마운트 시 자동으로 `GET /api/data/{tableName}` 호출
|
||||
- `searchParams`가 변경되면 재조회
|
||||
- 페이지네이션은 FcPagination과 연동
|
||||
|
||||
### 4.4 FcForm — 입력 폼
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/FcForm.tsx
|
||||
|
||||
interface FcFormProps {
|
||||
fields: FieldConfig[]; // 필드 정의
|
||||
config?: FormConfig; // 폼 설정
|
||||
initialData?: Record<string, any>; // 수정 모드 시 초기값
|
||||
|
||||
// ─── DataPort 출력 ───
|
||||
onSubmit?: (data: Record<string, any>) => void; // formData(row)
|
||||
onSaved?: (data: Record<string, any>) => void; // savedRow(row)
|
||||
|
||||
// ─── DataPort 입력 ───
|
||||
loadRow?: Record<string, any>; // 외부에서 행 데이터 로드
|
||||
}
|
||||
```
|
||||
|
||||
**렌더링 규칙:**
|
||||
1. `fields`에서 `system: true`인 것은 숨김
|
||||
2. `visible: true`인 것만 표시
|
||||
3. `order` 순으로 배치
|
||||
4. `config.columns` (1/2/3)에 따라 그리드 레이아웃
|
||||
5. 각 필드는 `FieldRenderer(mode='form')`로 렌더
|
||||
6. `required: true`인 필드에 * 표시
|
||||
7. `editable: false`인 필드는 disabled
|
||||
8. `pk: true`이고 `type: 'code'`이면 자동채번 표시 (readonly)
|
||||
9. `config.sections`가 있으면 섹션별로 그룹핑
|
||||
|
||||
**유효성 검증:**
|
||||
- 제출 시 `required: true` + 값이 비어있으면 에러 표시
|
||||
- empty 판단: `value === null || value === undefined || value === ''` (0, false는 유효)
|
||||
|
||||
### 4.5 FcSearch — 검색 필터
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/FcSearch.tsx
|
||||
|
||||
interface FcSearchProps {
|
||||
fields: FieldConfig[]; // 전체 필드 (searchable: true인 것만 렌더)
|
||||
config?: SearchConfig;
|
||||
|
||||
// ─── DataPort 출력 ───
|
||||
onSearch?: (params: Record<string, any>) => void; // searchParams(params)
|
||||
}
|
||||
```
|
||||
|
||||
**렌더링 규칙:**
|
||||
1. `fields`에서 `searchable: true`인 것만 표시
|
||||
2. 각 필드는 `FieldRenderer(mode='search')`로 렌더
|
||||
3. `config.layout: 'inline'`이면 한 줄에 나열, `'stacked'`이면 세로
|
||||
4. "검색" 버튼 + "초기화" 버튼 (showResetButton)
|
||||
5. `config.autoSearch: true`이면 입력 시 300ms 디바운스 후 자동 검색
|
||||
6. 검색 버튼 클릭 또는 자동 검색 시 `onSearch(params)` 호출
|
||||
|
||||
**검색 파라미터 형식:**
|
||||
```typescript
|
||||
// date 범위: { order_date_from: '2026-01-01', order_date_to: '2026-12-31' }
|
||||
// select 다중: { status: ['확정', '완료'] }
|
||||
// text 부분 일치: { customer_name: '삼성' }
|
||||
// number 범위: { amount_min: 1000, amount_max: 9999 }
|
||||
```
|
||||
|
||||
### 4.6 FcButton / FcButtonBar
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/FcButton.tsx
|
||||
interface FcButtonProps {
|
||||
config: ButtonConfig;
|
||||
onClick?: () => void; // clicked(value)
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// frontend/components/fc/FcButtonBar.tsx
|
||||
interface FcButtonBarProps {
|
||||
config: ButtonBarConfig;
|
||||
onAction?: (actionType: ActionType) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**ActionType 12종 (invyone-component.ts에 정의됨):**
|
||||
save, edit, delete, add, cancel, close, navigate, popup, search, reset, submit, approval
|
||||
|
||||
### 4.7 FcPagination
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/FcPagination.tsx
|
||||
interface FcPaginationProps {
|
||||
config?: PaginationConfig;
|
||||
total: number;
|
||||
page: number;
|
||||
onPageChange?: (params: { page: number; size: number }) => void; // pageChange(params)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.8 CellRenderer — 테이블 셀 포맷터
|
||||
|
||||
```typescript
|
||||
// frontend/components/fc/table/CellRenderer.tsx
|
||||
interface CellRendererProps {
|
||||
field: FieldConfig;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* FieldType에 따라 셀 내용을 포맷팅한다:
|
||||
* - number: format 적용 (#,##0 → 1,234,567)
|
||||
* - date: format 적용 (YYYY-MM-DD)
|
||||
* - checkbox: ✓/✗ 아이콘
|
||||
* - entity: ref.displayColumn 값 표시 (별도 조회 필요 시 캐시)
|
||||
* - textarea: 40자 말줄임 (...)
|
||||
* - file: 파일명 링크
|
||||
* - 나머지: 텍스트 그대로
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 연동 (CRUD API)
|
||||
|
||||
FcTable이 데이터를 조회하고, FcForm이 데이터를 저장할 때 사용하는 API.
|
||||
|
||||
기존 `DataController.java`가 범용 CRUD를 제공하지만, FieldConfig 기반으로 파라미터를 맞춰주는 래퍼가 필요.
|
||||
|
||||
**프론트엔드 API: `frontend/lib/api/fcData.ts`**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* ★ 별도 인터페이스 정의 안 함 — 백엔드가 Map<String, Object>이므로 프론트도 Record<string, any>
|
||||
* ★ 유일하게 타입이 있는 건 FieldConfig (invyone-component.ts에 규격 확정된 것)
|
||||
*/
|
||||
|
||||
/** FieldConfig 기반 목록 조회 */
|
||||
export async function fcList(params: Record<string, any>): Promise<Record<string, any>>;
|
||||
|
||||
/** FieldConfig 기반 단건 조회 */
|
||||
export async function fcGet(tableName: string, id: string): Promise<Record<string, any>>;
|
||||
|
||||
/** FieldConfig 기반 등록 */
|
||||
export async function fcInsert(tableName: string, data: Record<string, any>): Promise<any>;
|
||||
|
||||
/** FieldConfig 기반 수정 */
|
||||
export async function fcUpdate(tableName: string, id: string, data: Record<string, any>): Promise<any>;
|
||||
|
||||
/** FieldConfig 기반 삭제 (soft delete) */
|
||||
export async function fcDelete(tableName: string, ids: string[]): Promise<any>;
|
||||
```
|
||||
|
||||
**백엔드**: 기존 `DataController` 또는 `DataAdvancedController`의 엔드포인트를 활용. 필요하면 FieldConfig의 searchable/computed 등을 반영하는 동적 쿼리 빌더 추가.
|
||||
|
||||
---
|
||||
|
||||
## 6. 테스트 페이지
|
||||
|
||||
구현 확인용 테스트 페이지를 하나 만든다:
|
||||
|
||||
**`frontend/app/(main)/test-fc/page.tsx`**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* FieldConfig 컴포넌트 테스트 페이지
|
||||
*
|
||||
* 1. 드롭다운에서 테이블 선택
|
||||
* 2. getTableFields(tableName) 호출 → FieldConfig[] 수신
|
||||
* 3. FcSearch + FcTable + FcForm을 동시에 렌더
|
||||
* 4. FcSearch → searchParams → FcTable (검색 연동)
|
||||
* 5. FcTable 행 클릭 → selectedRow → FcForm (데이터 로드)
|
||||
* 6. FcForm 저장 → DB에 실제 저장 → FcTable 새로고침
|
||||
*/
|
||||
```
|
||||
|
||||
이 페이지가 동작하면 Phase 2 완료.
|
||||
|
||||
---
|
||||
|
||||
## 7. 스타일링 규칙
|
||||
|
||||
1. v5 CSS 토큰 사용 (`var(--v5-primary)`, `var(--glass)`, `var(--glow-sm)` 등)
|
||||
2. 컴팩트 폰트 (0.55~0.85rem)
|
||||
3. 글래스모피즘: `backdrop-filter:blur(20px) saturate(1.4)` + `var(--glass)` + `var(--glass-border)`
|
||||
4. 다크/라이트 모드 둘 다 지원
|
||||
5. CSS 파일: `frontend/styles/fc-components.css` (필요 시) 또는 Tailwind 클래스 활용
|
||||
6. 기존 `v5-layout.css`와 `globals.css`의 토큰 먼저 확인 후 재사용
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|---|---|
|
||||
| `notes/gbpark/2026-04-10-phase1-implementation-log.md` | **Phase 1 구현 결과** — API 응답 형태, 병합 규칙, 실 DB 테스트 결과, 범위 밖 항목 |
|
||||
| `frontend/types/invyone-component.ts` | **모든 타입 정의 (진실의 원천)** |
|
||||
| `notes/gbpark/2026-04-08-invyone-component-spec.md` (Section 2.2) | **렌더링 계약 테이블** — 반드시 이대로 구현 |
|
||||
| `notes/gbpark/2026-04-09-invyone-architecture.md` (Section 4) | 렌더링 계약 요약 |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/js/08-admin-builder.js` | mockup의 프리뷰 렌더 함수 (pvTable, pvForm, pvSearch) — UI 감각 참고 |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/css/05-widgets.css` | HR 테이블 스타일 — 테이블 CSS 참고 |
|
||||
| `frontend/styles/v5-layout.css` | v5 디자인 토큰 |
|
||||
| `frontend/app/globals.css` | Tailwind/shadcn 토큰 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 완료 기준
|
||||
|
||||
1. **FcTable**: FieldConfig[]과 데이터를 받아 테이블을 렌더링한다. 정렬, 행 선택, 페이지네이션이 동작한다.
|
||||
2. **FcForm**: FieldConfig[]을 받아 입력 폼을 렌더링한다. 10종 FieldType 전부 동작한다. required 검증이 동작한다.
|
||||
3. **FcSearch**: FieldConfig[]에서 searchable 필드만 추출하여 검색 UI를 렌더한다. 검색 시 params 객체를 반환한다.
|
||||
4. **FcSearch → FcTable 연동**: 검색 실행 시 테이블이 필터링된 데이터를 표시한다.
|
||||
5. **FcTable → FcForm 연동**: 행 클릭 시 폼에 해당 행 데이터가 로드된다.
|
||||
6. **FcForm 저장**: 폼 제출 시 DB에 INSERT/UPDATE가 실행되고 테이블이 새로고침된다.
|
||||
7. **테스트 페이지** (`/test-fc`)에서 위 전체 흐름이 실제 DB 데이터로 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 다음 단계 연결
|
||||
|
||||
Phase 3 (개발자 빌더)는 이 컴포넌트들을:
|
||||
- 팔레트에서 선택하여 캔버스에 배치
|
||||
- 속성 패널에서 config(TableConfig, FormConfig 등) 조정
|
||||
- Template JSON으로 저장/로드
|
||||
|
||||
Phase 3은 FcTable/FcForm/FcSearch를 "배치하고 설정하는 UI"이고, Phase 2는 그 컴포넌트 자체다.
|
||||
@@ -0,0 +1,305 @@
|
||||
# Phase 2 구현 작업기록 — FieldConfig 기반 컴포넌트 (FcTable/FcForm/FcSearch)
|
||||
|
||||
> **작업일**: 2026-04-10
|
||||
> **설계서**: `notes/gbpark/2026-04-10-phase2-fieldconfig-components.md`
|
||||
> **상태**: 구현 완료 + TypeScript 타입체크 통과 (tsc --noEmit 에러 0개)
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성된 파일 (17개)
|
||||
|
||||
### 1.1 파일 구조
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── components/fc/ ← FieldConfig 기반 컴포넌트 (신규)
|
||||
│ ├── index.ts ← 공개 exports
|
||||
│ ├── FcTable.tsx ← 데이터 테이블 (TanStack Table v8)
|
||||
│ ├── FcForm.tsx ← 입력 폼 (자체 상태 관리)
|
||||
│ ├── FcSearch.tsx ← 검색 필터
|
||||
│ ├── FcButton.tsx ← 단일 버튼 (confirm 팝업 포함)
|
||||
│ ├── FcButtonBar.tsx ← 버튼 그룹
|
||||
│ ├── FcPagination.tsx ← 페이지네이션
|
||||
│ ├── fields/ ← FieldType별 렌더러 (10종)
|
||||
│ │ ├── FieldRenderer.tsx ← FieldType→위젯 디스패처
|
||||
│ │ ├── TextField.tsx
|
||||
│ │ ├── NumberField.tsx
|
||||
│ │ ├── DateField.tsx
|
||||
│ │ ├── DateTimeField.tsx
|
||||
│ │ ├── SelectField.tsx
|
||||
│ │ ├── EntityField.tsx
|
||||
│ │ ├── CheckboxField.tsx
|
||||
│ │ ├── TextareaField.tsx
|
||||
│ │ ├── FileField.tsx
|
||||
│ │ └── CodeField.tsx
|
||||
│ └── table/ ← 테이블 전용
|
||||
│ └── CellRenderer.tsx ← 셀 포맷 디스패처
|
||||
├── lib/api/
|
||||
│ └── fcData.ts ← FieldConfig 기반 CRUD API 래퍼 (신규)
|
||||
└── app/(main)/test-fc/
|
||||
└── page.tsx ← 테스트 페이지 (신규)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 핵심 컴포넌트 상세
|
||||
|
||||
### 2.1 FieldRenderer — FieldType→위젯 디스패처
|
||||
|
||||
`frontend/components/fc/fields/FieldRenderer.tsx`
|
||||
|
||||
- `mode='form'` / `mode='search'` 분기로 같은 FieldConfig가 다른 위젯으로 렌더
|
||||
- switch/case로 10종 FieldType을 개별 컴포넌트에 위임
|
||||
- 모든 필드 공통 인터페이스: `{ field, value, onChange, mode, disabled?, error? }`
|
||||
|
||||
### 2.2 렌더링 계약 구현 (spec Section 2.2 전부 준수)
|
||||
|
||||
| FieldType | FcTable 셀 | FcForm 입력 | FcSearch 검색 |
|
||||
|---|---|---|---|
|
||||
| `text` | 텍스트 그대로 | `<Input type="text">` | `<Input>` (부분 일치) |
|
||||
| `number` | `#,##0` 포맷 (toLocaleString) | `<Input type="number">` | min~max 범위 2개 |
|
||||
| `date` | YYYY-MM-DD 포맷 | `<Input type="date">` | from~to 범위 2개 |
|
||||
| `datetime` | YYYY-MM-DD HH:mm 포맷 | `<Input type="datetime-local">` | from~to 범위 2개 |
|
||||
| `select` | 텍스트 그대로 | Radix `<Select>` 단일 | MultiSelect (Checkbox 드롭다운) |
|
||||
| `entity` | ref.displayColumn 그대로 | `<Input>` + 검색 버튼 | `<Input>` + 검색 버튼 |
|
||||
| `checkbox` | ✓(green) / ✗(muted) 아이콘 | Radix `<Checkbox>` | `<Select>` 전체/✓/✗ |
|
||||
| `textarea` | 40자 말줄임 (...) | `<textarea rows={3}>` | `<Input>` (부분 일치) |
|
||||
| `file` | 파일명 + FileText 아이콘 | 파일 업로드 버튼 | — (렌더 안 함) |
|
||||
| `code` | 텍스트 그대로 | readonly + Lock 아이콘 | `<Input>` (완전 일치) |
|
||||
|
||||
### 2.3 FcTable — 데이터 테이블
|
||||
|
||||
`frontend/components/fc/FcTable.tsx` (~190줄)
|
||||
|
||||
**기술 스택**: TanStack Table v8 (`@tanstack/react-table`)
|
||||
|
||||
**기능:**
|
||||
- `fields`에서 `visible: true`인 것만 컬럼으로 표시, `order` 순 정렬
|
||||
- `width`, `align` 적용 (없으면 타입별 기본값)
|
||||
- `sortable: true`인 컬럼은 헤더 클릭 시 정렬 토글 (asc→desc→none)
|
||||
- 행 클릭 시 `onRowSelect(row)` 호출 + 선택 행 하이라이트
|
||||
- `showCheckbox: true` + `selectionMode: 'multiple'`일 때 체크박스 전체선택/개별선택
|
||||
- 로딩 스피너, 빈 데이터 표시
|
||||
- `CellRenderer`가 FieldType에 따라 셀 포맷팅
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface FcTableProps {
|
||||
fields: FieldConfig[];
|
||||
data: Record<string, any>[];
|
||||
config?: Partial<TableConfig>;
|
||||
loading?: boolean;
|
||||
onRowSelect?: (row: Record<string, any>) => void;
|
||||
onRowsSelect?: (rows: Record<string, any>[]) => void;
|
||||
selectedRowIndex?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 FcForm — 입력 폼
|
||||
|
||||
`frontend/components/fc/FcForm.tsx` (~130줄)
|
||||
|
||||
**상태 관리**: `useState` 직접 사용 (React Hook Form 의존성 제거 — 폼이 단순하고 FieldConfig 동적 필드라 자체 관리가 적합)
|
||||
|
||||
**기능:**
|
||||
- `system: true` 필드 숨김, `visible: true`만 표시, `order` 순
|
||||
- `config.columns` (1/2/3)에 따라 CSS Grid 레이아웃
|
||||
- `config.sections`가 있으면 섹션별로 그룹핑 (라벨 + 구분선)
|
||||
- `required: true` 필드에 * 표시
|
||||
- `editable: false` 필드 disabled
|
||||
- `pk: true` + `type: 'code'`이면 readonly (자동채번)
|
||||
- `loadRow` 변경 시 폼 데이터 자동 갱신
|
||||
- 제출 시 `required` 검증 (null/undefined/'' 만 empty, 0/false는 유효)
|
||||
- 초기화 버튼 (loadRow/initialData로 복원)
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface FcFormProps {
|
||||
fields: FieldConfig[];
|
||||
config?: Partial<FormConfig>;
|
||||
initialData?: Record<string, any>;
|
||||
onSubmit?: (data: Record<string, any>) => void;
|
||||
loadRow?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 FcSearch — 검색 필터
|
||||
|
||||
`frontend/components/fc/FcSearch.tsx` (~110줄)
|
||||
|
||||
**기능:**
|
||||
- `searchable: true` + `!system` 필드만 추출, `order` 순
|
||||
- `FieldRenderer(mode='search')`로 각 필드 렌더
|
||||
- `config.layout: 'inline'` (한 줄 나열) / `'stacked'` (세로)
|
||||
- `config.autoSearch: true`이면 300ms 디바운스 후 자동 `onSearch`
|
||||
- 검색/초기화 버튼
|
||||
- `buildSearchParams()` 변환 로직:
|
||||
|
||||
```
|
||||
date/datetime 범위 → { column_from: '2026-01-01', column_to: '2026-12-31' }
|
||||
number 범위 → { column_min: 1000, column_max: 9999 }
|
||||
select 다중 → { column: ['확정', '완료'] }
|
||||
text 부분 일치 → { column: '삼성' }
|
||||
```
|
||||
|
||||
### 2.6 CellRenderer — 테이블 셀 포맷터
|
||||
|
||||
`frontend/components/fc/table/CellRenderer.tsx` (~85줄)
|
||||
|
||||
| 타입 | 포맷 |
|
||||
|---|---|
|
||||
| number | `#,##0` → `toLocaleString('ko-KR')`, 소수점 포맷도 지원 |
|
||||
| date | `YYYY-MM-DD` (Date 파싱 후 수동 포맷) |
|
||||
| datetime | `YYYY-MM-DD HH:mm` |
|
||||
| checkbox | ✓ (green Check 아이콘) / ✗ (muted X 아이콘) |
|
||||
| textarea | 40자 초과 시 `...` 말줄임 + title 툴팁 |
|
||||
| file | FileText 아이콘 + 파일명 링크 스타일 |
|
||||
| null/undefined | `-` (muted) |
|
||||
| 기타 | `String(value)` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 보조 컴포넌트
|
||||
|
||||
### 3.1 FcButton
|
||||
|
||||
- `ButtonConfig.variant` 5종 (primary/default/destructive/outline/ghost) → v5 토큰 매핑
|
||||
- `confirm` 속성 있으면 클릭 시 확인/취소 인라인 팝업
|
||||
|
||||
### 3.2 FcButtonBar
|
||||
|
||||
- `ButtonBarConfig.buttons[]` 순회하며 `FcButton` 렌더
|
||||
- `onAction(actionType)` 콜백으로 12종 ActionType 전달
|
||||
|
||||
### 3.3 FcPagination
|
||||
|
||||
- 총 건수, 페이지 크기 선택기 (10/20/50/100)
|
||||
- 5페이지 범위 번호 표시 + 처음/이전/다음/끝 버튼
|
||||
- `onPageChange({ page, size })` 콜백
|
||||
|
||||
---
|
||||
|
||||
## 4. API 래퍼
|
||||
|
||||
### 4.1 fcData.ts
|
||||
|
||||
`frontend/lib/api/fcData.ts` (~35줄)
|
||||
|
||||
기존 `dataApi` (frontend/lib/api/data.ts)를 감싸는 얇은 래퍼.
|
||||
|
||||
| 함수 | 용도 | 내부 호출 |
|
||||
|---|---|---|
|
||||
| `fcList(params)` | 목록 조회 (검색+페이징) | `dataApi.getTableData()` |
|
||||
| `fcGet(tableName, id)` | 단건 조회 | `dataApi.getRecordDetail()` |
|
||||
| `fcInsert(tableName, data)` | 등록 | `dataApi.createRecord()` |
|
||||
| `fcUpdate(tableName, id, data)` | 수정 | `dataApi.updateRecord()` |
|
||||
| `fcDelete(tableName, ids)` | 삭제 (복수) | `dataApi.deleteRecord()` × N |
|
||||
|
||||
★ 별도 인터페이스 정의 안 함 — 전부 `Record<string, any>`
|
||||
|
||||
---
|
||||
|
||||
## 5. 테스트 페이지
|
||||
|
||||
### 5.1 `/test-fc` (frontend/app/(main)/test-fc/page.tsx)
|
||||
|
||||
**레이아웃**: 좌(검색+테이블+페이지네이션) / 우(폼) 2컬럼
|
||||
|
||||
**흐름:**
|
||||
```
|
||||
1. 드롭다운에서 테이블 선택
|
||||
2. getMetaFields(tableName) → FieldConfig[] 수신
|
||||
3. FcSearch + FcTable + FcForm 동시 렌더
|
||||
4. FcSearch 검색 → searchParams → fcList() 재조회 → FcTable 갱신
|
||||
5. FcTable 행 클릭 → selectedRow → FcForm 데이터 로드
|
||||
6. FcForm 저장 → fcInsert/fcUpdate → 성공 메시지 + FcTable 새로고침
|
||||
```
|
||||
|
||||
**기능:**
|
||||
- 테이블 목록에 `has_custom_meta` 표시 (★)
|
||||
- 테이블 선택 시 필드 수, PK 표시
|
||||
- 수정/신규 모드 자동 전환 (selectedRow 유무)
|
||||
- 성공/실패 알림 (3초 자동 소멸)
|
||||
|
||||
---
|
||||
|
||||
## 6. v5 디자인 토큰 사용 목록
|
||||
|
||||
모든 컴포넌트에서 즉흥 hex/rgb 사용 안 함. v5-layout.css 토큰만 사용:
|
||||
|
||||
| 용도 | 토큰 |
|
||||
|---|---|
|
||||
| 배경 | `var(--v5-glass)`, `var(--v5-surface)`, `var(--v5-bg-subtle)` |
|
||||
| 테두리 | `var(--v5-glass-border)`, `var(--v5-border)`, `var(--v5-border-subtle)` |
|
||||
| 텍스트 | `var(--v5-text)`, `var(--v5-text-sec)`, `var(--v5-text-muted)` |
|
||||
| 강조 | `var(--v5-primary)`, `var(--v5-primary-glow)` |
|
||||
| 상태 | `var(--v5-green)` (✓), `var(--v5-red)` (* 필수, 에러) |
|
||||
| 호버 | `var(--v5-surface-hover)` |
|
||||
| 글로우 | `var(--v5-glow-sm)` |
|
||||
| 블러 | `backdrop-blur-[20px]` (v5 글래스 패턴) |
|
||||
|
||||
폰트 크기: 0.65rem(라벨) ~ 0.75rem(본문) — v5 컴팩트 스케일 준수
|
||||
|
||||
---
|
||||
|
||||
## 7. 덕일 스타일 / 프로젝트 컨벤션 준수
|
||||
|
||||
| 규칙 | 준수 |
|
||||
|---|---|
|
||||
| 프론트 타입: `Record<string, any>` (FieldConfig만 예외) | ✅ |
|
||||
| 별도 인터페이스 정의 금지 | ✅ — Props만 인라인 정의 |
|
||||
| FieldConfig/TableConfig 등은 invyone-component.ts 타입 사용 | ✅ |
|
||||
| v5 CSS 토큰 사용, 즉흥 값 금지 | ✅ |
|
||||
| shadcn UI 컴포넌트 재활용 (Input, Select, Checkbox) | ✅ |
|
||||
| 기존 dataApi 활용 (중복 API 안 만듦) | ✅ |
|
||||
| 컴팩트 폰트 사이즈 (0.55~0.85rem) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 8. TypeScript 검증 결과
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep -E "(components/fc/|test-fc/|lib/api/fcData)"
|
||||
# 출력 없음 — 에러 0개
|
||||
```
|
||||
|
||||
기존 코드(admin 페이지)의 타입 에러는 Phase 2와 무관.
|
||||
|
||||
---
|
||||
|
||||
## 9. Phase 2 범위 밖 (다음에 해야 할 것)
|
||||
|
||||
| 항목 | 담당 Phase | 비고 |
|
||||
|---|---|---|
|
||||
| entity 팝업 검색 (ref 테이블 조회) | Phase 3+ | 현재는 텍스트 입력 + 검색 버튼 UI만 |
|
||||
| category 공통코드 옵션 로딩 | Phase 2+ | CODE_CATEGORY → 공통코드 테이블 조회 |
|
||||
| 실제 파일 업로드 구현 | Phase 3+ | 현재는 파일 선택 UI만 |
|
||||
| computed 수식 파서 | Phase 5 | AST 기반 안전한 파서 필요 |
|
||||
| DataPort 이벤트 버스 | Phase 3 | 빌더에서 컴포넌트 간 연결 시 |
|
||||
| inlineEdit (테이블 인라인 편집) | Phase 3+ | TableConfig.inlineEdit는 정의만 |
|
||||
| 엑셀 내보내기 | Phase 3+ | toolbar.showExcel UI 미구현 |
|
||||
| select options의 value/label 분리 | Phase 2+ | 현재 label만 string[]으로 반환 |
|
||||
| defaultValue, placeholder 자동 세팅 | Phase 2+ | Phase 1에서 미구현 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 사용법 (다음 Phase에서 참고)
|
||||
|
||||
```tsx
|
||||
import { FcTable, FcForm, FcSearch, FcPagination } from '@/components/fc';
|
||||
import { getMetaFields } from '@/lib/api/meta';
|
||||
import { fcList } from '@/lib/api/fcData';
|
||||
|
||||
// 1. FieldConfig 가져오기
|
||||
const meta = await getMetaFields('sales_order_mng');
|
||||
const fields = meta.fields;
|
||||
|
||||
// 2. 데이터 조회
|
||||
const result = await fcList({ tableName: 'sales_order_mng', page: 1, size: 20 });
|
||||
|
||||
// 3. 컴포넌트 렌더
|
||||
<FcSearch fields={fields} onSearch={handleSearch} />
|
||||
<FcTable fields={fields} data={result.data} onRowSelect={handleSelect} />
|
||||
<FcForm fields={fields} loadRow={selectedRow} onSubmit={handleSave} />
|
||||
<FcPagination total={result.total} page={1} onPageChange={handlePage} />
|
||||
```
|
||||
@@ -0,0 +1,533 @@
|
||||
# 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이 "사용 가능한 템플릿 라이브러리"에 나타남
|
||||
@@ -0,0 +1,480 @@
|
||||
# Phase 3 구현 작업기록 — 개발자 빌더 (수동 템플릿 구성)
|
||||
|
||||
> **작업일**: 2026-04-10
|
||||
> **설계서**: `notes/gbpark/2026-04-10-phase3-developer-builder.md`
|
||||
> **상태**: 구현 완료 + TypeScript 타입체크 통과 + DB 테이블 생성 완료
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성된 파일 (18개)
|
||||
|
||||
### 1.1 파일 구조
|
||||
|
||||
```
|
||||
backend-spring/
|
||||
├── src/main/java/com/erp/
|
||||
│ ├── controller/TemplateController.java ← CRUD + publish 엔드포인트 (신규)
|
||||
│ └── service/TemplateService.java ← Template CRUD 비즈니스 로직 (신규)
|
||||
└── src/main/resources/mapper/
|
||||
└── template.xml ← MyBatis SQL (신규)
|
||||
|
||||
frontend/
|
||||
├── lib/api/
|
||||
│ └── template.ts ← Template CRUD API 클라이언트 (신규)
|
||||
├── styles/
|
||||
│ └── developer.css ← IDE 스타일 개발자 테마 (신규)
|
||||
├── components/builder/ ← 빌더 컴포넌트 디렉토리 (전체 신규)
|
||||
│ ├── BuilderLayout.tsx ← 3패널 셸 + 키보드 단축키 + 상태바
|
||||
│ ├── BuilderToolbar.tsx ← 헤더 + 도구모음 (테이블 선택, 뷰 탭, 저장)
|
||||
│ ├── BuilderPalette.tsx ← 좌측 팔레트 (8종 컴포넌트)
|
||||
│ ├── BuilderCanvas.tsx ← 중앙 캔버스 (드롭 영역, 팝업 뷰)
|
||||
│ ├── BuilderBlock.tsx ← 개별 블록 (드래그/리사이즈 + 타입별 프리뷰)
|
||||
│ ├── BuilderProps.tsx ← 우측 속성 패널 (공통 + 타입별 분기)
|
||||
│ ├── hooks/
|
||||
│ │ ├── useBuilderState.ts ← Zustand 빌더 상태관리
|
||||
│ │ └── useBlockDrag.ts ← 블록 드래그/리사이즈 훅
|
||||
│ └── props/
|
||||
│ ├── FieldListEditor.tsx ← 필드 체크리스트 (공통)
|
||||
│ ├── TableProps.tsx ← 테이블 속성 패널
|
||||
│ ├── FormProps.tsx ← 폼 속성 패널
|
||||
│ ├── SearchProps.tsx ← 검색 속성 패널
|
||||
│ ├── ButtonProps.tsx ← 버튼/버튼바 속성 패널
|
||||
│ └── TitleProps.tsx ← 제목 속성 패널
|
||||
└── app/(main)/admin/builder/
|
||||
└── page.tsx ← 빌더 페이지 진입점
|
||||
```
|
||||
|
||||
### 1.2 수정된 파일 (2개)
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|---|---|
|
||||
| `backend-spring/src/main/resources/application.yml` | DB 접속 정보 변경: `39.117.244.52:11132/testvex` → `211.115.91.141:11134/test_dev` |
|
||||
| `CLAUDE.local.md` | DB 접속 정보 업데이트 기록 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 백엔드 상세
|
||||
|
||||
### 2.1 TemplateController.java (~100줄)
|
||||
|
||||
`/api/templates` REST 컨트롤러. 6개 엔드포인트:
|
||||
|
||||
| 메서드 | 엔드포인트 | 설명 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/templates` | 템플릿 목록 (keyword, status, category 필터 + 페이지네이션) |
|
||||
| `GET` | `/api/templates/{templateId}` | 템플릿 상세 (JSONB 파싱된 Template 전체) |
|
||||
| `POST` | `/api/templates` | 템플릿 생성 (template_id 자동 생성: `tpl_` + UUID 12자) |
|
||||
| `PUT` | `/api/templates/{templateId}` | 템플릿 수정 (version 자동 증가) |
|
||||
| `PUT` | `/api/templates/{templateId}/publish` | 템플릿 게시 (draft → published) |
|
||||
| `DELETE` | `/api/templates/{templateId}` | 템플릿 삭제 (소프트: IS_ACTIVE='D') |
|
||||
|
||||
**패턴**: MetaController와 동일 — `@RequestAttribute("company_code")`, `@RequestAttribute("user_id")`, `ApiResponse.success/error`
|
||||
|
||||
### 2.2 TemplateService.java (~120줄)
|
||||
|
||||
덕일 스타일 준수 — `extends BaseService`, `@Autowired CommonService`, `sqlSession` 직접 호출.
|
||||
|
||||
| 메서드 | 역할 |
|
||||
|---|---|
|
||||
| `getTemplateList(params)` | CommonService.applyPagination → selectList + selectOne(Cnt) → buildListResponse |
|
||||
| `getTemplateInfo(params)` | selectOne + JSONB 파싱 (fields, views, connections) |
|
||||
| `insertTemplate(params)` | UUID 생성 + JSON 직렬화 + insert |
|
||||
| `updateTemplate(params)` | JSON 직렬화 + update (VERSION + 1) |
|
||||
| `publishTemplate(params)` | status='published' 업데이트 |
|
||||
| `deleteTemplate(params)` | IS_ACTIVE='D' 소프트 삭제 |
|
||||
|
||||
**JSONB 처리 유틸 (private):**
|
||||
- `parseJsonField(row, key)` — PostgreSQL JSONB 문자열 → Java Object (ObjectMapper)
|
||||
- `stringifyJsonField(params, key)` — Java Object → JSON 문자열 (INSERT/UPDATE 전)
|
||||
|
||||
### 2.3 template.xml (~120줄)
|
||||
|
||||
MyBatis 매퍼. namespace=`template`, 쿼리 6개:
|
||||
|
||||
| 쿼리 ID | 용도 |
|
||||
|---|---|
|
||||
| `getTemplateList` | 목록 (keyword LIKE, status/category 필터, companyCodeFilter, dynamicOrderBy, pagination) |
|
||||
| `getTemplateListCnt` | 목록 카운트 |
|
||||
| `getTemplateInfo` | 단건 (FIELDS, VIEWS, CONNECTIONS JSONB 포함) |
|
||||
| `insertTemplate` | 등록 (`#{fields}::jsonb` 캐스팅) |
|
||||
| `updateTemplate` | 수정 (VERSION + 1) |
|
||||
| `deleteTemplate` | 소프트 삭제 |
|
||||
| `publishTemplate` | 게시 상태 변경 |
|
||||
|
||||
**common include 사용**: `companyCodeFilter`, `dynamicOrderBy`, `pagination`
|
||||
**OGNL test**: 바깥 작은따옴표 규칙 준수 (`test='keyword != null and keyword != ""'`)
|
||||
|
||||
---
|
||||
|
||||
## 3. 프론트엔드 상세
|
||||
|
||||
### 3.1 template.ts — API 클라이언트 (~40줄)
|
||||
|
||||
| 함수 | 반환 | 설명 |
|
||||
|---|---|---|
|
||||
| `getTemplateList(params?)` | `Record<string, any>` | 목록 (list + total_count) |
|
||||
| `getTemplateInfo(templateId)` | `Record<string, any> \| null` | 상세 (Template JSON 전체) |
|
||||
| `insertTemplate(data)` | `Record<string, any>` | 생성 → `{ template_id }` 반환 |
|
||||
| `updateTemplate(templateId, data)` | `void` | 수정 |
|
||||
| `publishTemplate(templateId)` | `void` | 게시 |
|
||||
| `deleteTemplate(templateId)` | `void` | 삭제 |
|
||||
|
||||
★ 별도 인터페이스 정의 안 함 — 전부 `Record<string, any>` (덕일 스타일)
|
||||
|
||||
### 3.2 developer.css — IDE 스타일 테마 (~350줄)
|
||||
|
||||
mockup `09-developer.css` 기반 React 포팅. v5 코스믹이 아닌 IDE/Figma 스타일.
|
||||
|
||||
**색상 체계 (CSS 변수 `--d-*`):**
|
||||
|
||||
| 변수 | 다크 | 라이트 | 용도 |
|
||||
|---|---|---|---|
|
||||
| `--d-bg` | #121218 | #f5f5f8 | 기본 배경 |
|
||||
| `--d-bg2` | #1a1a22 | #ededf2 | 패널 배경 |
|
||||
| `--d-bg3` | #22222c | #e4e4ec | 입력 배경 |
|
||||
| `--d-surface` | #2a2a36 | #fff | 호버 배경 |
|
||||
| `--d-border` | #3a3a48 | #d8d8e2 | 기본 보더 |
|
||||
| `--d-text` | #e8e8ee | #1a1a24 | 기본 텍스트 |
|
||||
| `--d-accent` | #5b9ef5 | #3b7dd8 | 액센트 (블루) |
|
||||
| `--d-red` | #f87171 | #dc2626 | 위험/필수 |
|
||||
| `--d-green` | #4ade80 | #16a34a | 성공/검색 |
|
||||
|
||||
**주요 클래스:**
|
||||
- `.dev-shell` — 전체 셸 (flex column, 100vh)
|
||||
- `.dev-hdr` — 헤더 (42px)
|
||||
- `.dev-toolbar` — 도구모음 (34px)
|
||||
- `.dev-palette` — 좌측 팔레트 (180px)
|
||||
- `.dev-canvas` — 중앙 캔버스 (도트 그리드 배경)
|
||||
- `.dev-props` — 우측 속성 패널 (260px)
|
||||
- `.dev-block` — 캔버스 위 블록 (점선 보더, 선택 시 solid + 글로우)
|
||||
- `.dev-status` — 하단 상태바 (22px)
|
||||
- `.dev-popup-overlay/frame` — 등록/수정 팝업 편집 프레임
|
||||
|
||||
**폰트 사이즈**: 0.36rem(배지) ~ 0.72rem(로고) — mockup 컴팩트 스케일 그대로
|
||||
|
||||
### 3.3 useBuilderState.ts — Zustand 상태관리 (~280줄)
|
||||
|
||||
`create<BuilderState>()(devtools(...))` 패턴 (기존 tabStore와 동일).
|
||||
|
||||
**상태:**
|
||||
|
||||
| 키 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| `tableName` | `string \| null` | 선택된 테이블 |
|
||||
| `fields` | `FieldConfig[]` | 현재 테이블의 필드 목록 |
|
||||
| `currentView` | `'list' \| 'create' \| 'edit'` | 현재 뷰 탭 |
|
||||
| `blocks` | `Record<BuilderView, Component[]>` | 뷰별 블록 목록 |
|
||||
| `selectedBlockId` | `string \| null` | 선택된 블록 |
|
||||
| `connections` | `Connection[]` | DataPort 연결 목록 |
|
||||
| `templateId` | `string \| null` | 저장된 템플릿 ID |
|
||||
| `templateName` | `string` | 템플릿 이름 |
|
||||
| `category` | `string` | 분류 |
|
||||
| `description` | `string` | 설명 |
|
||||
| `isDirty` | `boolean` | 변경 여부 |
|
||||
|
||||
**액션 (17개):**
|
||||
|
||||
| 액션 | 설명 |
|
||||
|---|---|
|
||||
| `setTable(name, fields)` | 테이블 선택 → fields 로드 |
|
||||
| `switchView(view)` | 뷰 탭 전환 (선택 해제) |
|
||||
| `addBlock(type, position)` | 블록 추가 (기본 config/size/label 자동 설정) |
|
||||
| `removeBlock(id)` | 블록 삭제 (연결도 함께 제거) |
|
||||
| `updateBlock(id, updates)` | 블록 업데이트 |
|
||||
| `selectBlock(id)` | 블록 선택 |
|
||||
| `moveBlock(id, x, y)` | 블록 이동 (min 0) |
|
||||
| `resizeBlock(id, w, h)` | 블록 리사이즈 (min 40x20) |
|
||||
| `updateBlockConfig(id, config)` | 타입별 config 업데이트 |
|
||||
| `updateField(column, updates)` | FieldConfig 속성 변경 |
|
||||
| `setTemplateMeta(meta)` | 템플릿 이름/분류/설명 변경 |
|
||||
| `addConnection(conn)` | DataPort 연결 추가 |
|
||||
| `removeConnection(connId)` | 연결 제거 |
|
||||
| `toTemplate()` | 현재 상태 → Template JSON (저장용) |
|
||||
| `fromTemplate(tpl)` | Template JSON → 상태 복원 (로드용) |
|
||||
| `resetBuilder()` | 초기화 |
|
||||
| `markClean()` | isDirty=false |
|
||||
|
||||
**셀렉터 훅:**
|
||||
- `useCurrentViewBlocks()` — 현재 뷰의 블록 목록
|
||||
- `useSelectedBlock()` — 선택된 블록 객체
|
||||
|
||||
**컴포넌트 기본 설정 (`defaultConfig`):**
|
||||
|
||||
| ComponentType | 기본 config |
|
||||
|---|---|
|
||||
| `table` | pageSize:20, selectionMode:'single', autoLoad:true, style:'default' |
|
||||
| `form` | columns:2, saveAction:{method:'UPSERT', refreshAfterSave:true} |
|
||||
| `search` | dateRangeEnabled:true, showResetButton:true, autoSearch:false, layout:'inline' |
|
||||
| `button` | text:'버튼', actionType:'save', variant:'default' |
|
||||
| `button-bar` | buttons:[{등록/primary}, {삭제/destructive}] |
|
||||
| `title` | text:'제목', fontSize:'0.75rem', fontWeight:'700', align:'left' |
|
||||
| `stats` | items:[] |
|
||||
| `divider` | style:'solid' |
|
||||
| `pagination` | pageSize:20, showSizeSelector:true, sizeOptions:[10,20,50,100] |
|
||||
|
||||
**컴포넌트 기본 크기 (`defaultSize`):**
|
||||
|
||||
| ComponentType | W × H |
|
||||
|---|---|
|
||||
| `table` | 854 × 380 |
|
||||
| `form` | 440 × 300 |
|
||||
| `search` | 854 × 42 |
|
||||
| `button` | 100 × 36 |
|
||||
| `button-bar` | 370 × 36 |
|
||||
| `title` | 300 × 36 |
|
||||
| `pagination` | 854 × 24 |
|
||||
|
||||
### 3.4 useBlockDrag.ts — 드래그/리사이즈 훅 (~90줄)
|
||||
|
||||
mousedown → document.mousemove → mouseup 패턴.
|
||||
|
||||
- `startDrag(e, id, origX, origY, origW, origH)` — 블록 이동 시작
|
||||
- `startResize(e, id, origX, origY, origW, origH)` — 리사이즈 시작
|
||||
- **Shift 키**: 8px 스냅 (mockup의 `Math.round(n/8)*8` 그대로)
|
||||
- mouseup 시 리스너 자동 정리
|
||||
- `document.body.style.cursor/userSelect` 드래그 중 설정/해제
|
||||
|
||||
### 3.5 BuilderLayout.tsx — 3패널 셸 (~55줄)
|
||||
|
||||
```
|
||||
┌─ BuilderToolbar (헤더 + 도구모음) ─────────────────────┐
|
||||
├──────────┬────────────────────────────┬──────────────────┤
|
||||
│ Palette │ Canvas │ Props │
|
||||
│ (180px) │ (flex:1) │ (260px) │
|
||||
├──────────┴────────────────────────────┴──────────────────┤
|
||||
│ 상태바: 블록 N개 · 테이블명 · 연결 N개 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**키보드 단축키:**
|
||||
- `Delete` / `Backspace` — 선택된 블록 삭제
|
||||
- `Escape` — 블록 선택 해제
|
||||
|
||||
### 3.6 BuilderToolbar.tsx — 헤더 + 도구모음 (~130줄)
|
||||
|
||||
**헤더 (dev-hdr):**
|
||||
- INVYONE 로고 + DEV 배지
|
||||
- 템플릿 이름 입력
|
||||
- 저장 버튼
|
||||
|
||||
**도구모음 (dev-toolbar):**
|
||||
- 테이블 드롭다운 (getMetaTableList → has_custom_meta ★ 표시)
|
||||
- 뷰 탭 (목록 / 등록 / 수정)
|
||||
- 수정됨 표시 (isDirty)
|
||||
|
||||
**저장 흐름:**
|
||||
1. `toTemplate()` → Template JSON 생성
|
||||
2. `templateId` 있으면 `updateTemplate()`, 없으면 `insertTemplate()`
|
||||
3. 성공 시 `markClean()` + templateId 저장
|
||||
|
||||
### 3.7 BuilderPalette.tsx — 좌측 팔레트 (~75줄)
|
||||
|
||||
8종 컴포넌트를 4개 섹션으로 분류:
|
||||
|
||||
| 섹션 | 컴포넌트 |
|
||||
|---|---|
|
||||
| 데이터 | 📊 데이터 테이블, 🔍 검색 필터 |
|
||||
| 입력 | 📝 입력 폼 |
|
||||
| 액션 | 🔘 버튼, ⬜ 버튼 바 |
|
||||
| 표시 | 📌 제목/텍스트, 📈 통계 카드, ── 구분선, 📄 페이지네이션 |
|
||||
|
||||
- **드래그앤드롭**: `onDragStart` → `component-type` 데이터 전달 → 캔버스에서 `onDrop`
|
||||
- **클릭 추가**: 테이블 미선택 시 data 컴포넌트 비활성화 (opacity 0.4)
|
||||
|
||||
### 3.8 BuilderCanvas.tsx — 중앙 캔버스 (~80줄)
|
||||
|
||||
- **목록 뷰**: 전체 캔버스에 블록 자유 배치 (min 1200×800, 도트 그리드 배경)
|
||||
- **등록/수정 뷰**: `dev-popup-overlay` + `dev-popup-frame` (500px 너비) 안에 블록 배치
|
||||
- **드롭 처리**: `onDrop` → 마우스 좌표 계산 → `addBlock(type, {x, y})`
|
||||
- **빈 캔버스**: 안내 메시지 표시 ("팔레트에서 컴포넌트를 드래그하거나 클릭하여 추가하세요")
|
||||
- **선택 해제**: 캔버스 빈 공간 클릭 시 `selectBlock(null)`
|
||||
|
||||
### 3.9 BuilderBlock.tsx — 개별 블록 (~160줄)
|
||||
|
||||
**구조:**
|
||||
```
|
||||
.dev-block (position:absolute, 점선 보더)
|
||||
├── .dev-block-label (블록 이름)
|
||||
├── .dev-block-content (타입별 프리뷰)
|
||||
└── .dev-resize-handle (우하단 리사이즈 핸들)
|
||||
```
|
||||
|
||||
**타입별 프리뷰 (BlockPreview 내부 컴포넌트):**
|
||||
|
||||
| 타입 | 프리뷰 내용 |
|
||||
|---|---|
|
||||
| `table` | `<table>` 헤더 + 3행 더미 (visible 필드 최대 8개) |
|
||||
| `form` | CSS Grid (columns 수) + 필드 라벨/입력 (최대 10개, required * 표시) |
|
||||
| `search` | 가로 나열 필드 라벨/입력 (searchable 필드 최대 5개) + 검색 버튼 |
|
||||
| `title` | fontSize/fontWeight/align 적용된 텍스트 |
|
||||
| `button` | variant 스타일 적용된 단일 버튼 |
|
||||
| `button-bar` | 버튼 목록 가로 나열 |
|
||||
| `pagination` | 총 건수 / 페이지 번호 / 건수 선택기 |
|
||||
| `divider` | 수평선 |
|
||||
| `stats` | "통계 카드 프리뷰" 텍스트 |
|
||||
|
||||
**FieldOption 렌더링**: `string | {value, label}` 유니온 타입 처리 (tsc 에러 수정)
|
||||
|
||||
### 3.10 BuilderProps.tsx — 우측 속성 패널 (~90줄)
|
||||
|
||||
**공통 속성 (모든 블록):**
|
||||
- 컴포넌트 종류 (아이콘 + 한글 라벨)
|
||||
- 이름 (input, 캔버스 라벨과 양방향 동기화)
|
||||
- 위치·크기 (X/Y/W/H 4칸 그리드, 캔버스와 양방향)
|
||||
|
||||
**타입별 분기:**
|
||||
- `table` → TableProps
|
||||
- `form` → FormProps
|
||||
- `search` → SearchProps
|
||||
- `button` → SingleButtonProps
|
||||
- `button-bar` → ButtonBarProps
|
||||
- `title` → TitleProps
|
||||
|
||||
**공통 삭제 버튼**: 하단 빨간 테두리 버튼
|
||||
|
||||
### 3.11 FieldListEditor.tsx — 필드 체크리스트 (~110줄)
|
||||
|
||||
table/form/search 속성 패널에서 공통으로 사용하는 필드 목록.
|
||||
|
||||
**기능:**
|
||||
- 체크박스 토글 (`visible` 또는 `searchable` 속성)
|
||||
- 필드 배지 표시 (PK, 필수, 검색, SYS, 계산)
|
||||
- 타입 배지 (text, number, date 등)
|
||||
- **클릭하면 상세 펼침** (FieldDetail 패널):
|
||||
- 표시 이름, 너비 편집
|
||||
- 토글: 필수, 편집, 검색, 정렬 (dev-toggle 스타일)
|
||||
|
||||
### 3.12 타입별 속성 패널
|
||||
|
||||
**TableProps.tsx** (~70줄):
|
||||
- 페이지 크기 (10/20/50/100)
|
||||
- 선택 방식 (없음/단일/다중)
|
||||
- 자동 로드, 인라인 편집, 체크박스 토글
|
||||
- 스타일 (기본/줄무늬/테두리/컴팩트)
|
||||
- FieldListEditor (visible 토글)
|
||||
|
||||
**FormProps.tsx** (~40줄):
|
||||
- 컬럼 수 (1/2/3칸)
|
||||
- 저장 방식 (등록/수정/등록+수정)
|
||||
- FieldListEditor (visible 토글)
|
||||
|
||||
**SearchProps.tsx** (~50줄):
|
||||
- 날짜 범위 검색, 초기화 버튼, 자동 검색 토글
|
||||
- 레이아웃 (인라인/세로)
|
||||
- FieldListEditor (searchable 토글)
|
||||
|
||||
**ButtonProps.tsx** (~140줄):
|
||||
- `SingleButtonProps`: 텍스트, 액션 종류(12종), 스타일(5종), 확인 메시지
|
||||
- `ButtonBarProps`: 버튼 목록 CRUD (추가/삭제/편집), 각 버튼 액션+스타일 설정
|
||||
|
||||
**TitleProps.tsx** (~40줄):
|
||||
- 텍스트, 크기(4단계), 굵기(4단계), 정렬(3종)
|
||||
|
||||
---
|
||||
|
||||
## 4. DB 변경
|
||||
|
||||
### 4.1 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,
|
||||
views JSONB NOT NULL,
|
||||
connections JSONB DEFAULT '[]',
|
||||
company_code VARCHAR(20) NOT NULL DEFAULT '*',
|
||||
version INTEGER DEFAULT 1,
|
||||
status VARCHAR(20) DEFAULT 'draft',
|
||||
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'
|
||||
);
|
||||
```
|
||||
|
||||
**인덱스 3개:**
|
||||
- `idx_templates_company` (company_code)
|
||||
- `idx_templates_table` (primary_table)
|
||||
- `idx_templates_status` (status)
|
||||
|
||||
**실행 대상**: `211.115.91.141:11134/test_dev` (2026-04-10 실행 완료)
|
||||
|
||||
### 4.2 application.yml DB 접속 변경
|
||||
|
||||
```
|
||||
변경 전: jdbc:postgresql://39.117.244.52:11132/testvex (pw: ph0909!!)
|
||||
변경 후: jdbc:postgresql://211.115.91.141:11134/test_dev (pw: vexplor0909!!)
|
||||
```
|
||||
|
||||
39.117 서버 폐기에 따른 변경.
|
||||
|
||||
---
|
||||
|
||||
## 5. 덕일 스타일 / 프로젝트 컨벤션 준수
|
||||
|
||||
| 규칙 | 준수 |
|
||||
|---|---|
|
||||
| 3레이어 (Controller → Service → XML) | ✅ |
|
||||
| Mapper Interface 금지 | ✅ — sqlSession 직접 호출 |
|
||||
| Map<String, Object> 사용, DTO 금지 | ✅ — ApiResponse만 예외 |
|
||||
| BaseService 상속 | ✅ |
|
||||
| @Autowired CommonService | ✅ |
|
||||
| XML 파일명: 소문자, Mapper 안 붙임 | ✅ — `template.xml` |
|
||||
| XML namespace: 파일명과 동일 | ✅ — `namespace="template"` |
|
||||
| SQL: UPPER_SNAKE | ✅ |
|
||||
| SELECT 쉼표: 앞에 | ✅ |
|
||||
| #{파라미터}: snake_case | ✅ |
|
||||
| OGNL test: 바깥 작은따옴표 | ✅ |
|
||||
| 프론트 타입: Record<string, any> | ✅ — FieldConfig/Component만 예외 |
|
||||
| 개발자 모드 CSS: IDE 스타일 (v5 코스믹 아님) | ✅ — `--d-*` 변수 체계 |
|
||||
| Zustand devtools 미들웨어 | ✅ |
|
||||
| 기존 apiClient 사용 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 결과
|
||||
|
||||
| 검증 항목 | 결과 |
|
||||
|---|---|
|
||||
| `./gradlew compileJava` | BUILD SUCCESSFUL |
|
||||
| `./gradlew bootJar` | BUILD SUCCESSFUL |
|
||||
| `npx tsc --noEmit` (빌더 관련) | 에러 0개 |
|
||||
| DB 테이블 생성 (test_dev) | CREATE TABLE + INDEX 3개 성공 |
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 3 범위 밖 (다음에 해야 할 것)
|
||||
|
||||
| 항목 | 담당 Phase | 비고 |
|
||||
|---|---|---|
|
||||
| 메타 드리프트 감지 (빌더에서 Template 열 때 최신 메타 비교) | Phase 3+ | 설계서 Section 9에 정의됨 |
|
||||
| DataPort 연결 UI (속성 패널에서 연결 추가/삭제) | Phase 3+ | mockup의 `propsConnections()` 참고 |
|
||||
| 기존 Template 목록/로드 UI | Phase 3+ | 현재는 새 템플릿 생성만 |
|
||||
| 자동생성 + 프리셋 (⚡ 버튼) | Phase 6 | 설계서 명시 |
|
||||
| stats/chart 타입 속성 패널 | Phase 3+ | 현재 빈 구조만 |
|
||||
| 필드 순서 드래그 재정렬 | Phase 3+ | 현재 order 기준 정렬만 |
|
||||
| Template 게시 워크플로우 UI | Phase 4 | 대시보드에서 게시된 Template 사용 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 빌더 사용 흐름
|
||||
|
||||
```
|
||||
1. /admin/builder 접속
|
||||
2. 도구모음에서 테이블 선택 (예: sales_order_mng)
|
||||
→ Phase 1 API로 FieldConfig[] 자동 로드
|
||||
3. 팔레트에서 컴포넌트 선택 → 캔버스에 드래그 또는 클릭
|
||||
(예: 제목 → 검색 필터 → 데이터 테이블 → 페이지네이션)
|
||||
4. 캔버스에서 블록 드래그(이동) / 우하단 핸들로 리사이즈
|
||||
5. 블록 클릭 → 우측 속성 패널에서 설정 조정
|
||||
(필드 ON/OFF, 페이지 크기, 선택 모드 등)
|
||||
6. 뷰 탭으로 목록/등록/수정 전환
|
||||
(등록/수정은 팝업 프레임 안에서 편집)
|
||||
7. 💾 저장 → Template JSON이 DB에 저장
|
||||
8. 기존 Template 로드 → fromTemplate()으로 상태 복원
|
||||
```
|
||||
@@ -0,0 +1,400 @@
|
||||
# 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 자동 갱신 등)
|
||||
- 대시보드 캔버스 위에 제어 모드 오버레이
|
||||
@@ -0,0 +1,297 @@
|
||||
# Phase 4 구현 작업기록 — 대시보드(=메뉴) 사용자 화면 시스템
|
||||
|
||||
> **작업일**: 2026-04-10
|
||||
> **설계서**: `notes/gbpark/2026-04-10-phase4-dashboard-menu.md`
|
||||
> **상태**: 구현 완료 + 빌드 검증 통과 + DB 테이블 생성 완료
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성/수정된 파일 (19개)
|
||||
|
||||
### 1.1 백엔드 (6개)
|
||||
|
||||
| 파일 | 상태 | 역할 |
|
||||
|---|---|---|
|
||||
| `backend-spring/src/main/resources/mapper/dashboard.xml` | 수정 (VEX 레거시 교체) | 대시보드/카드/사이드바 SQL 12쿼리 |
|
||||
| `backend-spring/src/main/resources/mapper/userOverride.xml` | 신규 | 사용자 오버라이드 UPSERT/조회/삭제 3쿼리 |
|
||||
| `backend-spring/src/main/java/com/erp/service/DashboardService.java` | 수정 (VEX 레거시 교체) | BaseService + sqlSession 덕일 스타일 |
|
||||
| `backend-spring/src/main/java/com/erp/service/UserOverrideService.java` | 신규 | JSONB 파싱/직렬화 + UPSERT |
|
||||
| `backend-spring/src/main/java/com/erp/controller/DashboardController.java` | 수정 (VEX 레거시 교체) | `/api/dashboards` 엔드포인트 |
|
||||
| `backend-spring/src/main/java/com/erp/controller/UserOverrideController.java` | 신규 | `/api/overrides` 엔드포인트 |
|
||||
|
||||
### 1.2 프론트엔드 (13개)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `frontend/lib/api/dashMenu.ts` | 대시보드/카드/사이드바 API 래퍼 |
|
||||
| `frontend/lib/api/override.ts` | 사용자 오버라이드 API 래퍼 |
|
||||
| `frontend/stores/dashboardStore.ts` | Zustand 대시보드 상태관리 |
|
||||
| `frontend/styles/dashboard.css` | v5 Cosmic Glassmorphism 대시보드 CSS |
|
||||
| `frontend/components/dash/DashboardLayout.tsx` | 전체 레이아웃 (사이드바+캔버스+모달) |
|
||||
| `frontend/components/dash/DashboardSidebar.tsx` | 대시보드 목록 사이드바 |
|
||||
| `frontend/components/dash/DashboardToolbar.tsx` | 상단 툴바 (편집/저장/+템플릿) |
|
||||
| `frontend/components/dash/DashboardCanvas.tsx` | 자유 배치 캔버스 + 드래그/리사이즈 |
|
||||
| `frontend/components/dash/DashboardCard.tsx` | 카드 (Template→FcTable/FcSearch 렌더) |
|
||||
| `frontend/components/dash/DashboardEmpty.tsx` | 빈 대시보드 안내 |
|
||||
| `frontend/components/dash/TemplateLibraryModal.tsx` | 템플릿 라이브러리 모달 |
|
||||
| `frontend/components/dash/CardSettingsPanel.tsx` | 카드 설정 패널 (컬럼 ON/OFF) |
|
||||
| `frontend/components/dash/CardMiniView.tsx` | 접힌 카드 미니 뷰 |
|
||||
| `frontend/app/(main)/dash/page.tsx` | `/dash` 라우트 페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 2. VEX 레거시 교체 사항
|
||||
|
||||
### 2.1 기존 DashboardService/Controller (VEX)
|
||||
|
||||
**교체 전**: JdbcTemplate 직접 사용, `dashboards`+`dashboard_elements` VEX 테이블, `/api/dashboard` (단수)
|
||||
**교체 후**: BaseService + sqlSession 덕일 스타일, `DASHBOARDS`+`DASHBOARD_CARDS` 테이블, `/api/dashboards` (복수)
|
||||
|
||||
기존 VEX 대시보드의 유틸리티 엔드포인트 (`execute-query`, `execute-dml`, `table-schema`, `fetch-external-api`)는 제거됨. 필요 시 별도 유틸리티 컨트롤러로 분리 가능.
|
||||
|
||||
### 2.2 프론트엔드 경로 분리
|
||||
|
||||
| 구분 | VEX 레거시 | Phase 4 (INVYONE) |
|
||||
|---|---|---|
|
||||
| API 파일 | `lib/api/dashboard.ts` (유지) | `lib/api/dashMenu.ts` (신규) |
|
||||
| 컴포넌트 | `components/dashboard/` (유지) | `components/dash/` (신규) |
|
||||
| 페이지 | `app/(main)/dashboard/` (유지) | `app/(main)/dash/` (신규) |
|
||||
|
||||
VEX 레거시 파일은 건드리지 않음. 나중에 VEX 완전 폐기 시 삭제.
|
||||
|
||||
---
|
||||
|
||||
## 3. 백엔드 상세
|
||||
|
||||
### 3.1 dashboard.xml (namespace="dashboard", 12쿼리)
|
||||
|
||||
| 쿼리 ID | 용도 |
|
||||
|---|---|
|
||||
| `getDashboardList` | 대시보드 목록 (유저+회사공통, 페이지네이션) |
|
||||
| `getDashboardListCnt` | 목록 카운트 |
|
||||
| `getDashboardInfo` | 대시보드 단건 |
|
||||
| `insertDashboard` | 대시보드 생성 |
|
||||
| `updateDashboard` | 대시보드 수정 (이름/아이콘/순서) |
|
||||
| `deleteDashboard` | 대시보드 소프트 삭제 (IS_ACTIVE='D') |
|
||||
| `getDashboardCardList` | 카드 목록 (TEMPLATES JOIN으로 기본 정보 포함) |
|
||||
| `insertDashboardCard` | 카드 추가 |
|
||||
| `updateDashboardCard` | 카드 업데이트 (위치/크기/접기) |
|
||||
| `updateCardPosition` | 카드 일괄 위치 업데이트 (단건, for loop용) |
|
||||
| `deleteDashboardCard` | 카드 소프트 삭제 |
|
||||
| `getSidebarMenu` | 사이드바 메뉴 (간략 대시보드 목록) |
|
||||
|
||||
### 3.2 userOverride.xml (namespace="userOverride", 3쿼리)
|
||||
|
||||
| 쿼리 ID | 용도 |
|
||||
|---|---|
|
||||
| `getUserOverride` | 유저+카드 기준 오버라이드 조회 |
|
||||
| `upsertUserOverride` | ON CONFLICT UPSERT (UNIQUE(user_id, card_id)) |
|
||||
| `deleteUserOverride` | 오버라이드 삭제 |
|
||||
|
||||
### 3.3 DashboardService.java (~100줄)
|
||||
|
||||
덕일 스타일 준수: `extends BaseService`, `@Autowired CommonService`, `sqlSession` 직접 호출.
|
||||
- ID 생성: `dash_` + UUID 12자 / `card_` + UUID 12자
|
||||
- 기본값: icon=📋, position_x=50, width=600, height=400
|
||||
|
||||
### 3.4 DashboardController.java (~120줄)
|
||||
|
||||
| 메서드 | 엔드포인트 | 설명 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/dashboards` | 목록 (keyword, page, limit) |
|
||||
| `GET` | `/api/dashboards/{id}` | 단건 |
|
||||
| `POST` | `/api/dashboards` | 생성 |
|
||||
| `PUT` | `/api/dashboards/{id}` | 수정 |
|
||||
| `DELETE` | `/api/dashboards/{id}` | 삭제 |
|
||||
| `GET` | `/api/dashboards/{id}/cards` | 카드 목록 (Template JOIN) |
|
||||
| `POST` | `/api/dashboards/{id}/cards` | 카드 추가 |
|
||||
| `PUT` | `/api/dashboards/{id}/cards/{cardId}` | 카드 수정 |
|
||||
| `DELETE` | `/api/dashboards/{id}/cards/{cardId}` | 카드 삭제 |
|
||||
| `PUT` | `/api/dashboards/{id}/cards/batch` | 카드 일괄 위치 업데이트 |
|
||||
| `GET` | `/api/dashboards/sidebar/menu` | 사이드바 메뉴 |
|
||||
|
||||
### 3.5 UserOverrideController.java (~40줄)
|
||||
|
||||
| 메서드 | 엔드포인트 | 설명 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/overrides?card_id=xxx` | 조회 |
|
||||
| `PUT` | `/api/overrides` | UPSERT |
|
||||
| `DELETE` | `/api/overrides?card_id=xxx` | 삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 프론트엔드 상세
|
||||
|
||||
### 4.1 dashMenu.ts — API 래퍼 (~55줄)
|
||||
|
||||
```
|
||||
getDashboardList, getDashboardInfo, insertDashboard, updateDashboard, deleteDashboard
|
||||
getDashboardCards, insertDashboardCard, updateDashboardCard, deleteDashboardCard
|
||||
updateCardPositionsBatch, getSidebarMenu
|
||||
```
|
||||
|
||||
★ 전부 `Record<string, any>` — 별도 인터페이스 정의 안 함
|
||||
|
||||
### 4.2 dashboardStore.ts — Zustand (~80줄)
|
||||
|
||||
상태: `dashboards[]`, `activeDashboardId`, `cards[]`, `editMode`, `loading`
|
||||
액션: `setDashboards`, `setActiveDashboard`, `setCards`, `addCard`, `updateCard`, `removeCard`, `toggleEditMode`, `addDashboard`, `updateDashboardInList`, `removeDashboard`
|
||||
|
||||
### 4.3 DashboardLayout.tsx — 전체 오케스트레이터 (~180줄)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ DashboardLayout │
|
||||
├────────────┬─────────────────────────────────────┤
|
||||
│ Sidebar │ Toolbar │
|
||||
│ (220px) ├─────────────────────────────────────┤
|
||||
│ - 목록 │ Canvas │
|
||||
│ - +추가 │ - DashboardCard × N │
|
||||
│ │ - 드래그/리사이즈 │
|
||||
│ │ + CardSettingsPanel (조건부) │
|
||||
├────────────┴─────────────────────────────────────┤
|
||||
│ TemplateLibraryModal (조건부) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 DashboardCanvas.tsx — 드래그/리사이즈 (~130줄)
|
||||
|
||||
mockup의 `02-canvas.js` 로직 포팅:
|
||||
- mousedown → 드래그 또는 리사이즈 모드 판별
|
||||
- mousemove → 캔버스 경계 clamp 적용
|
||||
- mouseup → store에 최종 위치 반영
|
||||
- snap 없음 (px 단위), 캔버스 밖 방지
|
||||
|
||||
### 4.5 DashboardCard.tsx — Template 렌더 (~160줄)
|
||||
|
||||
카드 안에서 Phase 2 컴포넌트가 실제 데이터로 동작:
|
||||
1. `primaryTable`로 `getMetaFields()` → FieldConfig[] 로드
|
||||
2. `fcList()` → 실제 데이터 조회
|
||||
3. `FcSearch` + `FcTable` + `FcPagination` 렌더
|
||||
4. 검색 → 재조회, 페이지네이션 → 재조회
|
||||
|
||||
### 4.6 TemplateLibraryModal.tsx (~130줄)
|
||||
|
||||
mockup의 `06-modals.css` 스타일 포팅:
|
||||
- 좌측 카테고리 (7종: 전체/영업/생산/인사/재고/재무/관리자)
|
||||
- 우측 템플릿 카드 그리드 (auto-fill, minmax 180px)
|
||||
- 검색 필터
|
||||
- 클릭 → `onSelectTemplate` 콜백
|
||||
|
||||
### 4.7 CardSettingsPanel.tsx (~100줄)
|
||||
|
||||
- 필드 visible 토글 (ON/OFF)
|
||||
- `getUserOverride` + `upsertUserOverride` 실시간 저장
|
||||
- JSONB overrides 구조: `{ fields: { column: { visible: bool } } }`
|
||||
|
||||
### 4.8 dashboard.css — v5 토큰 전부 사용 (~350줄)
|
||||
|
||||
| 용도 | 토큰 |
|
||||
|---|---|
|
||||
| 배경 | `var(--v5-glass)`, `var(--v5-surface)` |
|
||||
| 테두리 | `var(--v5-glass-border)`, `var(--v5-border-subtle)` |
|
||||
| 텍스트 | `var(--v5-text)`, `var(--v5-text-sec)`, `var(--v5-text-muted)` |
|
||||
| 강조 | `var(--v5-primary)`, `var(--v5-primary-light)` |
|
||||
| 글로우 | `var(--v5-glow-sm)`, `var(--v5-glow-md)` |
|
||||
| 유리 | `backdrop-filter: blur(20px) saturate(1.4)` |
|
||||
|
||||
다크/라이트 모드 변형: `.dark` 선택자 사용 (mockup 패턴 동일)
|
||||
|
||||
---
|
||||
|
||||
## 5. DB 테이블 (✅ 생성 완료)
|
||||
|
||||
**실행 대상**: `211.115.91.141:11134/test_dev` (2026-04-10 실행 완료)
|
||||
|
||||
### 5.1 VEX 레거시 처리
|
||||
|
||||
기존 `dashboards` 테이블(VEX 스키마: id, title, description, tags, settings 등)이 있어서 **`dashboards_vex_backup`으로 rename** 후 Phase 4 테이블 신규 생성.
|
||||
|
||||
### 5.2 생성된 테이블 3개 + 인덱스 3개
|
||||
|
||||
| 테이블 | PK | 용도 |
|
||||
|---|---|---|
|
||||
| `DASHBOARDS` | DASHBOARD_ID (VARCHAR 50) | 대시보드 (=메뉴 항목) |
|
||||
| `DASHBOARD_CARDS` | CARD_ID (VARCHAR 50) | 대시보드 위 카드 배치 (위치/크기) |
|
||||
| `USER_OVERRIDES` | OVERRIDE_ID (VARCHAR 50) | 사용자별 카드 오버라이드 (JSONB) |
|
||||
|
||||
**DASHBOARDS 컬럼**: DASHBOARD_ID, NAME, ICON, DISPLAY_ORDER, COMPANY_CODE, USER_ID, IS_ACTIVE, CREATED_BY, CREATED_DATE, UPDATED_BY, UPDATED_DATE
|
||||
|
||||
**DASHBOARD_CARDS 컬럼**: CARD_ID, DASHBOARD_ID, TEMPLATE_ID, POSITION_X, POSITION_Y, WIDTH, HEIGHT, IS_COLLAPSED, DISPLAY_ORDER, IS_ACTIVE, CREATED_DATE, UPDATED_DATE
|
||||
|
||||
**USER_OVERRIDES 컬럼**: OVERRIDE_ID, USER_ID, CARD_ID, OVERRIDES(JSONB), CREATED_DATE, UPDATED_DATE + `UNIQUE(USER_ID, CARD_ID)`
|
||||
|
||||
**인덱스**: `idx_dashboards_company`, `idx_dashboards_user`, `idx_dcards_dashboard`
|
||||
|
||||
★ DB FK 제약조건 안 걸음 (앱 레벨 관리, Phase 4 설계서 Section 3 참조)
|
||||
|
||||
---
|
||||
|
||||
## 6. 덕일 스타일 / 프로젝트 컨벤션 준수
|
||||
|
||||
| 규칙 | 준수 |
|
||||
|---|---|
|
||||
| 3레이어 (Controller → Service → XML) | ✅ |
|
||||
| Mapper Interface 금지 | ✅ — sqlSession 직접 호출 |
|
||||
| Map<String, Object> 사용, DTO 금지 | ✅ — ApiResponse만 예외 |
|
||||
| BaseService 상속 | ✅ |
|
||||
| @Autowired CommonService | ✅ |
|
||||
| XML 파일명: 소문자, Mapper 안 붙임 | ✅ — `dashboard.xml`, `userOverride.xml` |
|
||||
| XML namespace: 파일명과 동일 | ✅ |
|
||||
| SQL: UPPER_SNAKE | ✅ |
|
||||
| SELECT 쉼표: 앞에 | ✅ |
|
||||
| #{파라미터}: snake_case | ✅ |
|
||||
| OGNL test: 바깥 작은따옴표 | ✅ |
|
||||
| 프론트 타입: Record<string, any> | ✅ — FieldConfig만 예외 |
|
||||
| v5 CSS 토큰 사용, 즉흥 값 금지 | ✅ |
|
||||
| 컴팩트 폰트 사이즈 (0.55~0.85rem) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 7. 검증 결과
|
||||
|
||||
| 검증 항목 | 결과 |
|
||||
|---|---|
|
||||
| `./gradlew compileJava` | BUILD SUCCESSFUL |
|
||||
| `npx tsc --noEmit` (Phase 4 파일) | 에러 0개 |
|
||||
| 기존 VEX 코드 에러 | Phase 4와 무관 (기존 camelCase/snake_case 불일치 에러) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 접속 경로
|
||||
|
||||
- 대시보드 메뉴 시스템: `/dash`
|
||||
- 개발자 빌더: `/admin/builder` (Phase 3)
|
||||
- 테스트 페이지: `/test-fc` (Phase 2)
|
||||
|
||||
---
|
||||
|
||||
## 9. Phase 4 범위 밖 (다음에 해야 할 것)
|
||||
|
||||
| 항목 | 담당 Phase | 비고 |
|
||||
|---|---|---|
|
||||
| AppLayout 사이드바 통합 (대시보드 목록을 기존 사이드바에 표시) | Phase 4+ | 현재는 `/dash` 페이지에 자체 사이드바 |
|
||||
| 대시보드 순서 드래그 재정렬 | Phase 4+ | 현재 display_order는 수동 |
|
||||
| Template 게시 워크플로우 (빌더에서 publish → 라이브러리에 표시) | Phase 3+ | 현재 빌더 publish API는 구현됨 |
|
||||
| 카드 간 DataPort 연결 (제어 모드) | Phase 5 | 대시보드 캔버스 위에 오버레이 |
|
||||
| 사용자 오버라이드 실시간 반영 (FcTable에 override 적용) | Phase 4+ | 현재 settings에서 toggle만 저장 |
|
||||
| 카드 미니 KPI 뷰 (접었을 때 실제 데이터 집계) | Phase 4+ | 현재 템플릿 이름/분류만 표시 |
|
||||
| 대시보드 아이콘 선택 UI | Phase 4+ | 현재 기본 📋 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 사용 흐름
|
||||
|
||||
```
|
||||
1. /dash 접속
|
||||
2. 좌측 사이드바: 대시보드 목록 표시
|
||||
3. "+ 새 대시보드" → 이름 입력 → 사이드바에 자동 등록
|
||||
4. 대시보드 클릭 → 캔버스 표시
|
||||
5. "편집" 버튼 → 편집 모드 (드래그/리사이즈)
|
||||
6. "+ 템플릿 추가" → 라이브러리 모달 → 게시된 Template 선택
|
||||
7. → 캔버스에 카드 추가 (FcTable/FcSearch가 실제 DB 데이터 표시)
|
||||
8. 카드 ⚙ → 컬럼 ON/OFF (사용자 오버라이드)
|
||||
9. 카드 ▼ → 접기 (미니 뷰)
|
||||
10. "저장" → DB에 카드 위치/크기 일괄 저장
|
||||
11. 다른 대시보드 클릭 → 전환 (편집 모드 자동 해제)
|
||||
```
|
||||
@@ -0,0 +1,357 @@
|
||||
# Phase 5: 제어 모드 — 비즈니스 룰 / 데이터 흐름
|
||||
|
||||
> **목적**: 대시보드의 카드(Template) 간 비즈니스 룰과 데이터 흐름을 시각적으로 정의하는 제어 모드 구현
|
||||
> **전제 조건**: Phase 1~4 완료 (DB 메타 + 컴포넌트 + 빌더 + 대시보드에 카드 배치까지 동작)
|
||||
> **산출물**: 제어 모드 UI (SVG 연결선 + 테이블 노드 + 규칙 빌더) + 비즈니스 룰 엔진
|
||||
> **다음 단계**: Phase 6 (자동생성/프리셋 — 편의 기능, 맨 마지막)
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 개념
|
||||
|
||||
**제어 모드 = 같은 캔버스에서 "데이터가 어떻게 흐르는지" 보고 편집하는 모드**
|
||||
|
||||
일반 모드: 카드(화면)를 사용
|
||||
제어 모드: 카드 간 관계/비즈니스 룰을 설정
|
||||
|
||||
```
|
||||
[수주관리 카드] ──수주 확정──→ [발주관리 카드] ──금액>1000만──→ [프로젝트 카드]
|
||||
(자동 등록) (조건분기)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 두 가지 기능
|
||||
|
||||
### 2.1 읽기 모드 — 흐름 시각화 (카드 클릭)
|
||||
|
||||
카드 클릭 → 해당 카드의 데이터 소스(테이블) + 관련 테이블 + 비즈니스 룰이 트리 형태로 표시
|
||||
|
||||
mockup 참조: `js/06-control-mode.js`의 `showCardFlow()`, `enterControlMode()`
|
||||
|
||||
### 2.2 편집 모드 — 규칙 빌더 (드래그앤드롭)
|
||||
|
||||
팔레트에서 테이블 노드 + 제어 노드를 캔버스에 드래그, I/O 포트로 연결
|
||||
|
||||
mockup 참조: `js/07-rule-builder.js`의 `dropTable()`, `dropControl()`, `initPortEvents()`
|
||||
|
||||
---
|
||||
|
||||
## 3. 읽기 모드 상세
|
||||
|
||||
### 3.1 진입
|
||||
|
||||
캔버스 상단 툴바의 **`⚡ 제어`** 버튼 클릭 → 제어 모드 진입
|
||||
|
||||
### 3.2 시각적 변화
|
||||
|
||||
1. 캔버스 격자가 시안 톤으로 변경 (`rgba(0,206,201,.22)`)
|
||||
2. 모든 카드 반투명 (opacity 0.5)
|
||||
3. 편집 모드 자동 비활성화
|
||||
|
||||
### 3.3 카드 클릭 → 흐름 표시
|
||||
|
||||
1. 클릭된 카드만 좌측 고정 + opacity 1, 나머지 fade out (0.08)
|
||||
2. 카드의 `data-source-table`에서 소스 테이블 추출
|
||||
3. Phase 1의 `GET /api/meta/tables/{tableName}/relations`로 관계 조회
|
||||
4. BFS로 도달 가능한 전체 체인 계산
|
||||
5. **트리 확산 애니메이션**: 선이 그려짐 → 노드 reveal → 또 선 → 또 노드 (depth별 지연)
|
||||
|
||||
### 3.4 시각 요소
|
||||
|
||||
**테이블 노드** (mockup: `.tbl-node`):
|
||||
```
|
||||
┌─ 🏢 DEPARTMENT ─── 4컬럼 ──┐
|
||||
│ ● dept_code VARCHAR PK │
|
||||
│ ○ dept_name VARCHAR │
|
||||
│ ○ company_code VARCHAR FK │
|
||||
│ ○ parent_dept VARCHAR FK │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
**연결선 4종** (SVG bezier):
|
||||
|
||||
| 타입 | 색상 | CSS 클래스 | 용도 |
|
||||
|---|---|---|---|
|
||||
| 소스 | 핑크 (#fd79a8) | `ctrl-line-tpl` | 카드 → 소스 테이블 |
|
||||
| 자동실행 | 보라 (#6c5ce7) | `ctrl-line-auto` | 테이블 A → 테이블 B 자동 등록 |
|
||||
| 조건분기 | 앰버 (#fdcb6e) | `ctrl-line-cond` | 조건 충족 시 실행 |
|
||||
| FK | 시안 (#00cec9) | `ctrl-line` | 외래키 관계 (기본 비표시, 비즈니스 룰만) |
|
||||
|
||||
**연결선 위 뱃지** (mockup: `.ctrl-badge`):
|
||||
- 클릭 가능, 해당 룰의 상세 정보 표시
|
||||
- 조건분기 뱃지: Yes/No 분기 경로 표시
|
||||
|
||||
### 3.5 라이트 모드 보정
|
||||
|
||||
라이트 모드에서 연결선은 더 진하게 (배경 대비):
|
||||
- 시안 → `#00a89e`, 보라 → `#5b4acf`, 앰버 → `#d4a017`, 핑크 → `#e0559e`
|
||||
|
||||
---
|
||||
|
||||
## 4. 규칙 빌더 상세
|
||||
|
||||
### 4.1 제어 노드 16종
|
||||
|
||||
mockup의 `CTRL_NODE_TYPES` (js/07-rule-builder.js):
|
||||
|
||||
| 카테고리 | 노드 | 아이콘 | RGB | 특수 출력 포트 |
|
||||
|---|---|---|---|---|
|
||||
| 트리거 | 타이머 | ⏱ | 0,206,201 | — |
|
||||
| 조건 | 조건분기 | ◇ | 253,203,110 | Yes/No |
|
||||
| 조건 | 데이터 검증 | ✔ | 255,107,129 | Pass/Fail |
|
||||
| 액션 | 상태 변경 | 🔄 | 108,92,231 | — |
|
||||
| 액션 | 자동 등록 | 📝 | 85,239,196 | — |
|
||||
| 액션 | 계산/수식 | 🧮 | 45,152,218 | — |
|
||||
| 액션 | 삭제/보관 | 🗑 | 255,71,87 | — |
|
||||
| 액션 | 문서 생성 | 📄 | 162,155,254 | — |
|
||||
| 흐름 | 승인/결재 | ✋ | 255,165,2 | Approved/Rejected |
|
||||
| 흐름 | 대기/지연 | ⏳ | 72,219,251 | — |
|
||||
| 흐름 | 반복 | 🔁 | 223,142,254 | Each/Done |
|
||||
| 흐름 | 병렬 실행 | 🔀 | 0,206,201 | — |
|
||||
| 흐름 | 병합/합류 | ⤵ | 149,175,192 | — |
|
||||
| 연동 | 외부 호출 | 🌐 | 116,185,255 | — |
|
||||
| 연동 | 알림 발송 | 📨 | 253,121,168 | — |
|
||||
| 기록 | 로그 기록 | 📜 | 150,150,160 | — |
|
||||
|
||||
### 4.2 노드 구조
|
||||
|
||||
```
|
||||
┌─ [In] ──────────────────── [Out] ──┐
|
||||
│ 📝 자동 등록 │
|
||||
│ ───────────────────────── │
|
||||
│ 클릭하여 설정 │
|
||||
│ (대상 테이블, 필드 매핑) │ [Yes]
|
||||
│ │ [No]
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
각 노드에 I/O 포트:
|
||||
- **Input 포트** (좌측): 데이터/이벤트를 받음
|
||||
- **Output 포트** (우측): 결과를 내보냄
|
||||
- 조건분기/승인 등은 다중 output (Yes/No)
|
||||
|
||||
### 4.3 포트 연결 인터랙션
|
||||
|
||||
1. output 포트 mousedown → 연결선 드래그 시작 (SVG 임시선)
|
||||
2. input 포트 위에서 mouseup → 연결 완료
|
||||
3. 연결 중간에 삭제 뱃지 (hover 시 표시)
|
||||
4. 같은 노드끼리 연결 금지, 중복 연결 금지
|
||||
|
||||
### 4.4 노드 설정 팝오버
|
||||
|
||||
노드 body 클릭 → 설정 팝오버:
|
||||
|
||||
| 노드 타입 | 설정 항목 |
|
||||
|---|---|
|
||||
| 자동 등록 | 대상 테이블, 필드 매핑 (소스→대상), 조건 |
|
||||
| 상태 변경 | 대상 테이블, 대상 필드, 변경값 |
|
||||
| 조건분기 | 조건식 (필드, 연산자, 값) |
|
||||
| 승인/결재 | 결재선, 승인자 |
|
||||
| 타이머 | 실행 주기, 시작 조건 |
|
||||
| 외부 호출 | URL, 메서드, 파라미터 매핑 |
|
||||
| 알림 발송 | 대상 (사용자/이메일/슬랙), 메시지 템플릿 |
|
||||
| 계산/수식 | 수식 (computed 문법), 대상 필드 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 모델
|
||||
|
||||
### 5.1 비즈니스 룰 DB
|
||||
|
||||
**`business_rules` 테이블 (신규 생성):**
|
||||
|
||||
```sql
|
||||
CREATE TABLE business_rules (
|
||||
rule_id VARCHAR(50) PRIMARY KEY,
|
||||
dashboard_id VARCHAR(50), -- ★ DB FK 제약조건 안 걸음 (앱 레벨 관리)
|
||||
name VARCHAR(200),
|
||||
description TEXT,
|
||||
nodes JSONB NOT NULL, -- 노드 배열 (위치, 타입, 설정)
|
||||
connections JSONB NOT NULL, -- 연결 배열 (from → to)
|
||||
is_enabled BOOLEAN DEFAULT TRUE,
|
||||
company_code VARCHAR(20) NOT NULL,
|
||||
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_brules_dashboard ON business_rules(dashboard_id);
|
||||
```
|
||||
|
||||
### 5.2 노드 JSON 구조
|
||||
|
||||
```
|
||||
★ 별도 인터페이스 정의 안 함 — Record<string, any> 사용
|
||||
★ JSONB로 저장되므로 구조만 문서화
|
||||
|
||||
노드 (Record<string, any>):
|
||||
id, type(CTRL_NODE_TYPES 키 또는 'table'), label, x, y,
|
||||
config(타입별 설정), table_name(table 노드일 때)
|
||||
|
||||
연결 (Record<string, any>):
|
||||
id, from_node_id, from_port('out'|'yes'|'no'|'each'|'done'),
|
||||
to_node_id, to_port('in')
|
||||
|
||||
비즈니스 룰 (Record<string, any>):
|
||||
rule_id, dashboard_id, name, nodes(배열), connections(배열), is_enabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 백엔드 API
|
||||
|
||||
### 6.1 관계 조회 (★ table_relationships 기반 — Phase 1과 소스 분리)
|
||||
|
||||
```
|
||||
GET /api/meta/tables/{tableName}/relations → 테이블 간 업무 관계
|
||||
```
|
||||
|
||||
**★ 2소스 책임 분리:**
|
||||
- Phase 1 `getTableFields()` → `table_type_columns` (필드 레벨 entity 참조 → FieldConfig.ref)
|
||||
- Phase 5 `getMetaRelations()` → `table_relationships` (테이블 간 업무 관계 → 제어 모드 흐름)
|
||||
- 제어 모드에서 두 소스 필요하면 **서비스 레이어에서 읽기 시점에만 합친다** (저장 구조 통합 안 함)
|
||||
|
||||
### 6.2 비즈니스 룰 CRUD
|
||||
|
||||
```
|
||||
GET /api/dashboards/{dashboard_id}/rules → 해당 대시보드의 룰 목록
|
||||
GET /api/rules/{rule_id} → 룰 상세 (노드+연결)
|
||||
POST /api/dashboards/{dashboard_id}/rules → 룰 생성
|
||||
PUT /api/rules/{rule_id} → 룰 수정
|
||||
DELETE /api/rules/{rule_id} → 룰 삭제
|
||||
PUT /api/rules/{rule_id}/toggle → 활성/비활성 토글
|
||||
```
|
||||
|
||||
**★ 덕일 스타일 3레이어. 파일명 1:1 매칭.**
|
||||
|
||||
| Java 파일 | XML | namespace |
|
||||
|---|---|---|
|
||||
| `BusinessRuleController.java` | `businessRule.xml` | `businessRule` |
|
||||
| `BusinessRuleService.java` | | |
|
||||
|
||||
```java
|
||||
@Service
|
||||
@Slf4j
|
||||
public class BusinessRuleService extends BaseService {
|
||||
|
||||
@Autowired
|
||||
private CommonService commonService;
|
||||
|
||||
public Map<String, Object> getBusinessRuleList(Map<String, Object> params) {
|
||||
commonService.applyPagination(params);
|
||||
int totalCount = sqlSession.selectOne("businessRule.getBusinessRuleListCnt", params);
|
||||
List<Map<String, Object>> list = sqlSession.selectList("businessRule.getBusinessRuleList", params);
|
||||
return commonService.buildListResponse(list, totalCount, params);
|
||||
}
|
||||
|
||||
public Map<String, Object> getBusinessRuleInfo(Map<String, Object> params) {
|
||||
return sqlSession.selectOne("businessRule.getBusinessRuleInfo", params);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> insertBusinessRule(Map<String, Object> params) {
|
||||
// nodes, connections → ObjectMapper로 JSON 문자열 변환 후 #{nodes}::jsonb
|
||||
sqlSession.insert("businessRule.insertBusinessRule", params);
|
||||
return params;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateBusinessRule(Map<String, Object> params) {
|
||||
sqlSession.update("businessRule.updateBusinessRule", params);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int deleteBusinessRule(Map<String, Object> params) {
|
||||
return sqlSession.update("businessRule.deleteBusinessRule", params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 프론트엔드 구현
|
||||
|
||||
### 7.1 파일 구조
|
||||
|
||||
```
|
||||
frontend/components/control/
|
||||
├── ControlMode.tsx ← 제어 모드 오버레이 (캔버스 위에)
|
||||
├── ControlToolbar.tsx ← 제어 모드 툴바 (읽기/편집 전환)
|
||||
├── FlowViewer.tsx ← 읽기 모드: 카드 흐름 시각화
|
||||
├── RuleBuilder.tsx ← 편집 모드: 규칙 빌더
|
||||
├── TableNode.tsx ← 테이블 노드 UI
|
||||
├── ControlNode.tsx ← 제어 노드 UI (16종)
|
||||
├── NodeConfigPopover.tsx ← 노드 설정 팝오버
|
||||
├── PortHandle.tsx ← I/O 포트 (드래그 연결)
|
||||
├── ConnectionLine.tsx ← SVG bezier 연결선
|
||||
├── ControlPalette.tsx ← 제어 모드 팔레트 (사이드바 교체)
|
||||
└── hooks/
|
||||
├── useControlMode.ts ← 제어 모드 상태 관리
|
||||
├── usePortDrag.ts ← 포트 연결 드래그 로직
|
||||
└── useFlowAnimation.ts ← 트리 확산 애니메이션
|
||||
```
|
||||
|
||||
### 7.2 SVG 연결선 렌더링
|
||||
|
||||
mockup의 bezier 곡선 방식 그대로:
|
||||
|
||||
```typescript
|
||||
// from 좌표 (x1,y1) → to 좌표 (x2,y2)
|
||||
const dx = x2 - x1;
|
||||
const d = `M${x1},${y1} C${x1+dx*0.5},${y1} ${x1+dx*0.5},${y2} ${x2},${y2}`;
|
||||
// SVG <path d={d} class="ctrl-line-auto" marker-end="url(#arr-auto)" />
|
||||
```
|
||||
|
||||
선이 그려지는 애니메이션: `stroke-dashoffset` transition
|
||||
|
||||
---
|
||||
|
||||
## 8. 스타일
|
||||
|
||||
제어 모드 전용 스타일은 mockup의 `css/07-control-mode.css` + `css/08-rule-builder.css` 참조.
|
||||
|
||||
핵심:
|
||||
- 캔버스 격자: 시안 톤 (`rgba(0,206,201,.22)`)
|
||||
- 테이블 노드: 시안 보더 + 글래스 배경
|
||||
- 연결선: 점선 + 펄스 애니메이션 (`stroke-dasharray: 6 3`)
|
||||
- 뱃지: 글래스 + 시안/보라/앰버 보더
|
||||
|
||||
---
|
||||
|
||||
## 9. 참고 파일
|
||||
|
||||
| 파일 | 용도 |
|
||||
|---|---|
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/js/06-control-mode.js` | **흐름 시각화 (진실의 원천)** — 996줄, enterControlMode, showCardFlow, buildCtrlTree, calcFlowPositions |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/js/07-rule-builder.js` | **규칙 빌더 (진실의 원천)** — 752줄, 16종 노드, 포트 드래그, 연결 관리, 설정 팝오버 |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/css/07-control-mode.css` | 제어 모드 스타일 |
|
||||
| `notes/gbpark/2026-04-08-invyone-mockup/css/08-rule-builder.css` | 규칙 빌더 스타일 |
|
||||
| `notes/gbpark/2026-04-09-invyone-architecture.md` (Section 9 Phase 3) | 제어 플로우 로드맵 |
|
||||
| `notes/gbpark/2026-04-08-lowcode-platform-spec.md` | 비즈니스 룰 정의 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 완료 기준
|
||||
|
||||
1. **제어 모드 진입/탈출**: ⚡ 버튼으로 토글, 캔버스 시각이 변함
|
||||
2. **카드 클릭 → 흐름 표시**: 소스 테이블 + 관련 테이블 + 비즈니스 룰이 트리로 표시
|
||||
3. **트리 확산 애니메이션**: 선 → 노드 순서로 연쇄 등장
|
||||
4. **4종 연결선**: 소스(핑크), 자동(보라), 조건(앰버), FK(시안) 구분
|
||||
5. **팔레트에서 노드 드래그앤드롭**: 테이블 노드 + 제어 노드 캔버스에 배치
|
||||
6. **포트 연결**: output → input 드래그로 연결, SVG bezier 곡선
|
||||
7. **노드 설정 팝오버**: 타입별 설정 폼이 동작
|
||||
8. **규칙 저장/로드**: DB에 저장하고 다시 열면 복원
|
||||
9. **빈 영역 클릭 → 흐름 닫기**
|
||||
10. **다크/라이트 모드 지원**
|
||||
|
||||
---
|
||||
|
||||
## 11. 다음 단계 연결
|
||||
|
||||
Phase 6 (자동생성/프리셋):
|
||||
- 테이블 선택 → FieldConfig 기반으로 Template 자동 생성
|
||||
- 프리셋 3종 (basic/split/tabs) 자동 배치
|
||||
- 이 시점에서 모든 기능이 갖춰져 있으므로, 자동생성이 올바른 Template JSON을 생성할 수 있음
|
||||
@@ -0,0 +1,368 @@
|
||||
# Phase 5 구현 작업기록 — 제어 모드 (비즈니스 룰 / 데이터 흐름)
|
||||
|
||||
> **작업일**: 2026-04-10
|
||||
> **설계서**: `notes/gbpark/2026-04-10-phase5-control-mode.md`
|
||||
> **상태**: 구현 완료 + 빌드 검증 통과 + DB 테이블 생성 완료
|
||||
|
||||
---
|
||||
|
||||
## 1. 생성/수정된 파일 (22개)
|
||||
|
||||
### 1.1 백엔드 (6개)
|
||||
|
||||
| 파일 | 상태 | 역할 |
|
||||
|---|---|---|
|
||||
| `backend-spring/src/main/resources/mapper/meta.xml` | 수정 | `getMetaRelations` 쿼리 추가 (table_relationships) |
|
||||
| `backend-spring/src/main/java/com/erp/service/MetaService.java` | 수정 | `getMetaRelations()` 메서드 추가 |
|
||||
| `backend-spring/src/main/java/com/erp/controller/MetaController.java` | 수정 | `GET /api/meta/tables/{tableName}/relations` 엔드포인트 |
|
||||
| `backend-spring/src/main/resources/mapper/businessRule.xml` | 신규 | 비즈니스 룰 CRUD 7쿼리 (namespace=`businessRule`) |
|
||||
| `backend-spring/src/main/java/com/erp/service/BusinessRuleService.java` | 신규 | 비즈니스 룰 서비스 (JSONB 파싱/직렬화) |
|
||||
| `backend-spring/src/main/java/com/erp/controller/BusinessRuleController.java` | 신규 | 룰 CRUD + 토글 엔드포인트 6개 |
|
||||
|
||||
### 1.2 프론트엔드 (16개)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `frontend/lib/api/meta.ts` | `getMetaRelations()` 추가 |
|
||||
| `frontend/lib/api/businessRule.ts` | 비즈니스 룰 CRUD API 클라이언트 (신규) |
|
||||
| `frontend/styles/control-mode.css` | 제어 모드 + 규칙 빌더 CSS (~350줄, mockup 포팅) |
|
||||
| `frontend/components/control/hooks/useControlMode.ts` | Zustand 상태관리 + `CTRL_NODE_TYPES` 16종 |
|
||||
| `frontend/components/control/hooks/useFlowAnimation.ts` | BFS 체인 + 위치 계산 + 애니메이션 타이밍 |
|
||||
| `frontend/components/control/hooks/usePortDrag.ts` | 포트 연결 드래그 로직 |
|
||||
| `frontend/components/control/TableNode.tsx` | 테이블 노드 UI (컬럼 목록 + 드래그) |
|
||||
| `frontend/components/control/ConnectionLine.tsx` | SVG bezier 연결선 4종 + 마커 + 뱃지 |
|
||||
| `frontend/components/control/FlowViewer.tsx` | 읽기 모드: 카드 클릭 → 흐름 시각화 |
|
||||
| `frontend/components/control/ControlNode.tsx` | 제어 노드 16종 (I/O 포트 포함) |
|
||||
| `frontend/components/control/PortHandle.tsx` | I/O 포트 핸들 (드래그 연결 시작/끝) |
|
||||
| `frontend/components/control/NodeConfigPopover.tsx` | 노드 설정 팝오버 (타입별 폼) |
|
||||
| `frontend/components/control/ControlPalette.tsx` | 제어 팔레트 (사이드바 교체) |
|
||||
| `frontend/components/control/RuleBuilder.tsx` | 편집 모드: 규칙 빌더 (드래그앤드롭) |
|
||||
| `frontend/components/control/ControlToolbar.tsx` | 읽기/편집 모드 전환 + 저장 버튼 |
|
||||
| `frontend/components/control/ControlMode.tsx` | 제어 모드 오버레이 메인 컴포넌트 |
|
||||
|
||||
### 1.3 기존 파일 수정 (3개)
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|---|---|
|
||||
| `frontend/components/dash/DashboardToolbar.tsx` | ⚡ 제어 모드 토글 버튼 추가 (제어 진입 시 편집 모드 자동 해제) |
|
||||
| `frontend/components/dash/DashboardLayout.tsx` | `ControlMode` 오버레이 통합 + 제어 편집 시 사이드바→팔레트 교체 |
|
||||
| `frontend/components/dash/DashboardCanvas.tsx` | `forwardRef` + `control-mode` CSS 클래스 + 제어 모드 시 드래그 비활성화 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 백엔드 상세
|
||||
|
||||
### 2.1 Meta Relations API (2소스 책임 분리)
|
||||
|
||||
```
|
||||
GET /api/meta/tables/{tableName}/relations → table_relationships 기반
|
||||
```
|
||||
|
||||
**★ Phase 1과 소스 분리:**
|
||||
- Phase 1 `getMetaFields()` → `table_type_columns` (필드 레벨 entity 참조 → FieldConfig.ref)
|
||||
- Phase 5 `getMetaRelations()` → `table_relationships` (테이블 간 업무 관계 → 제어 모드 흐름)
|
||||
- 제어 모드에서 두 소스 필요하면 **서비스 레이어에서 읽기 시점에만 합침**
|
||||
|
||||
### 2.2 BusinessRule CRUD (덕일 스타일 3레이어)
|
||||
|
||||
| 엔드포인트 | 메서드 | 설명 |
|
||||
|---|---|---|
|
||||
| `/api/dashboards/{id}/rules` | GET | 대시보드별 룰 목록 (페이지네이션) |
|
||||
| `/api/dashboards/{id}/rules` | POST | 룰 생성 (`rule_` + UUID 12자) |
|
||||
| `/api/rules/{id}` | GET | 룰 상세 (JSONB → Object 파싱) |
|
||||
| `/api/rules/{id}` | PUT | 룰 수정 (Object → JSON 직렬화) |
|
||||
| `/api/rules/{id}` | DELETE | 룰 소프트 삭제 (IS_ACTIVE='D') |
|
||||
| `/api/rules/{id}/toggle` | PUT | 활성/비활성 토글 |
|
||||
|
||||
**businessRule.xml 쿼리 7개:**
|
||||
`getBusinessRuleList`, `getBusinessRuleListCnt`, `getBusinessRuleInfo`, `insertBusinessRule`, `updateBusinessRule`, `deleteBusinessRule`, `toggleBusinessRule`
|
||||
|
||||
**JSONB 처리**: `nodes`/`connections` → `ObjectMapper`로 파싱/직렬화 + `#{nodes}::jsonb` 캐스팅
|
||||
|
||||
---
|
||||
|
||||
## 3. 프론트엔드 상세
|
||||
|
||||
### 3.1 useControlMode — Zustand 상태관리 (~160줄)
|
||||
|
||||
| 상태 | 타입 | 설명 |
|
||||
|---|---|---|
|
||||
| `active` | boolean | 제어 모드 ON/OFF |
|
||||
| `mode` | 'view' \| 'edit' | 읽기/편집 모드 |
|
||||
| `activeFlowCardId` | string \| null | 흐름 표시 중인 카드 |
|
||||
| `flowEdges` | Record[] | BFS 결과 엣지 배열 |
|
||||
| `tablePositions` | Record | 테이블 노드 위치 |
|
||||
| `ruleNodes` | Record[] | 규칙 빌더 노드 |
|
||||
| `ruleConnections` | Record[] | 규칙 빌더 연결 |
|
||||
| `configNodeId` | string \| null | 설정 팝오버 대상 노드 |
|
||||
|
||||
**CTRL_NODE_TYPES 16종 (mockup 그대로):**
|
||||
|
||||
| 카테고리 | 노드 | 아이콘 | 특수 포트 |
|
||||
|---|---|---|---|
|
||||
| 트리거 | 타이머 | ⏱ | — |
|
||||
| 조건 | 조건분기 | ◇ | Yes/No |
|
||||
| 조건 | 데이터 검증 | ✔ | Pass/Fail |
|
||||
| 액션 | 상태 변경 | 🔄 | — |
|
||||
| 액션 | 자동 등록 | 📝 | — |
|
||||
| 액션 | 계산/수식 | 🧮 | — |
|
||||
| 액션 | 삭제/보관 | 🗑 | — |
|
||||
| 액션 | 문서 생성 | 📄 | — |
|
||||
| 흐름 | 승인/결재 | ✋ | Approved/Rejected |
|
||||
| 흐름 | 대기/지연 | ⏳ | — |
|
||||
| 흐름 | 반복 | 🔁 | Each/Done |
|
||||
| 흐름 | 병렬 실행 | 🔀 | — |
|
||||
| 흐름 | 병합/합류 | ⤵ | — |
|
||||
| 연동 | 외부 호출 | 🌐 | — |
|
||||
| 연동 | 알림 발송 | 📨 | — |
|
||||
| 기록 | 로그 기록 | 📜 | — |
|
||||
|
||||
### 3.2 FlowViewer — 읽기 모드 (카드 클릭 → 흐름 시각화)
|
||||
|
||||
**흐름:**
|
||||
1. 캔버스에서 카드 클릭
|
||||
2. `getMetaRelations(sourceTable)` → 업무 관계 조회
|
||||
3. BFS로 도달 가능한 전체 체인 계산 (depth 무제한)
|
||||
4. `calcFlowPositions()` — 카드 우측에 depth별 트리 배치 (colGap=270~350, rowGap=240)
|
||||
5. `calcAnimationTimings()` — depth별 지연 (STEP=500ms, NODE_D=350ms)
|
||||
6. **트리 확산 애니메이션**: 선 → 노드 순서로 연쇄 등장
|
||||
|
||||
**시각 효과:**
|
||||
- 선택된 카드: opacity 1 + 좌측 고정 + 시안 보더
|
||||
- 나머지 카드: opacity 0.08
|
||||
- 테이블 노드: scale(0.3) → scale(1) 트랜지션
|
||||
- 연결선: `stroke-dashoffset` draw 애니메이션 → pulse 복원
|
||||
- 빈 영역 클릭: 흐름 닫기 (모든 카드 0.5로 복원)
|
||||
- 노드 드래그: 실시간 위치 업데이트 + 선 재그리기
|
||||
|
||||
### 3.3 RuleBuilder — 편집 모드 (규칙 빌더)
|
||||
|
||||
**흐름:**
|
||||
1. 사이드바 → 제어 팔레트 교체 (DB 테이블 + 제어 노드 16종)
|
||||
2. 팔레트에서 캔버스로 드래그앤드롭 → 노드 생성
|
||||
3. 노드 헤더 드래그 → 이동
|
||||
4. output 포트 mousedown → bezier 임시선 → input 포트 mouseup → 연결 생성
|
||||
5. 연결 중간 hover → 삭제 뱃지 (✕)
|
||||
6. 노드 body 클릭 → 설정 팝오버 (타입별 폼)
|
||||
7. "규칙 저장" → `insertBusinessRule()` / `updateBusinessRule()`
|
||||
|
||||
**포트 연결 규칙:**
|
||||
- 같은 노드끼리 연결 금지
|
||||
- 중복 연결 금지
|
||||
- 드래그 중 모든 input 포트 pulse 애니메이션
|
||||
|
||||
### 3.4 SVG 연결선 4종
|
||||
|
||||
| 타입 | 색상 | CSS 클래스 | 마커 | 용도 |
|
||||
|---|---|---|---|---|
|
||||
| 소스 | 핑크 #fd79a8 | `ctrl-line-tpl` | arr-src | 카드 → 소스 테이블 |
|
||||
| 자동실행 | 보라 #6c5ce7 | `ctrl-line-auto` | arr-auto | 테이블 → 테이블 자동 등록 |
|
||||
| 조건분기 | 앰버 #fdcb6e | `ctrl-line-cond` | arr-cond | 조건 충족 시 실행 |
|
||||
| FK | 시안 #00cec9 | `ctrl-line` | arr-fk | 외래키 관계 |
|
||||
|
||||
**라이트 모드 보정**: 시안→#00a89e, 보라→#5b4acf, 앰버→#d4a017, 핑크→#e0559e
|
||||
|
||||
### 3.5 연결선 위 뱃지
|
||||
|
||||
- 일반: 글래스 배경 + 시안 보더 + 라벨 텍스트
|
||||
- 조건분기: 확장형 (`cb-head` + `cb-cond` + `cb-paths` Yes/No)
|
||||
- 소스: 핑크 보더
|
||||
- 자동실행: 보라 보더
|
||||
|
||||
### 3.6 NodeConfigPopover — 타입별 설정 폼
|
||||
|
||||
| 노드 타입 | 설정 항목 |
|
||||
|---|---|
|
||||
| 조건분기 | 필드, 연산자(=, ≠, >, <, 기한경과, 포함), 값 |
|
||||
| 상태 변경 | 대상 테이블, 변경 필드, 변경값 |
|
||||
| 자동 등록 | 대상 테이블 |
|
||||
| 타이머 | 기준 필드, 경과량, 단위(일/시간/주) |
|
||||
| 알림 발송 | 채널(이메일/SMS/푸시/Slack), 수신자, 메시지 |
|
||||
| 승인/결재 | 승인자, 승인 조건 |
|
||||
| 계산/수식 | 대상 테이블, 결과 필드, 수식 |
|
||||
| 외부 호출 | URL, 메서드(POST/GET/PUT/DELETE) |
|
||||
| 데이터 검증 | 대상 필드, 검증 규칙 |
|
||||
| 로그 기록 | 내용 |
|
||||
|
||||
### 3.7 Dashboard 통합
|
||||
|
||||
**DashboardToolbar:**
|
||||
- ⚡ 버튼 추가 (제어 모드 진입 시 편집 모드 자동 해제)
|
||||
- 제어 모드 활성 중: 편집/템플릿추가/저장 버튼 숨김
|
||||
|
||||
**DashboardLayout:**
|
||||
- `ControlMode` 오버레이 (ControlToolbar + FlowViewer/RuleBuilder)
|
||||
- 제어 편집 모드: 사이드바 → `ControlPalette` 교체
|
||||
|
||||
**DashboardCanvas:**
|
||||
- `forwardRef`로 부모에서 캔버스 DOM 참조
|
||||
- `control-mode` CSS 클래스 (시안 격자 배경)
|
||||
- 제어 모드 시 카드 드래그/리사이즈 비활성화
|
||||
|
||||
---
|
||||
|
||||
## 4. DB 변경
|
||||
|
||||
### 4.1 business_rules 테이블 (✅ 생성 완료)
|
||||
|
||||
```sql
|
||||
CREATE TABLE business_rules (
|
||||
rule_id VARCHAR(50) PRIMARY KEY,
|
||||
dashboard_id VARCHAR(50),
|
||||
name VARCHAR(200),
|
||||
description TEXT,
|
||||
nodes JSONB NOT NULL,
|
||||
connections JSONB NOT NULL,
|
||||
is_enabled BOOLEAN DEFAULT TRUE,
|
||||
company_code VARCHAR(20) NOT NULL,
|
||||
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_brules_dashboard ON business_rules(dashboard_id);
|
||||
```
|
||||
|
||||
**실행 대상**: `211.115.91.141:11134/test_dev` (2026-04-10 실행 완료)
|
||||
★ DB FK 제약조건 안 걸음 (설계서 명시 — 앱 레벨 관리)
|
||||
|
||||
---
|
||||
|
||||
## 5. 덕일 스타일 / 프로젝트 컨벤션 준수
|
||||
|
||||
| 규칙 | 준수 |
|
||||
|---|---|
|
||||
| 3레이어 (Controller → Service → XML) | ✅ |
|
||||
| Mapper Interface 금지 | ✅ — sqlSession 직접 호출 |
|
||||
| Map<String, Object> 사용, DTO 금지 | ✅ — ApiResponse만 예외 |
|
||||
| BaseService 상속 | ✅ |
|
||||
| @Autowired CommonService | ✅ |
|
||||
| XML 파일명: 소문자, Mapper 안 붙임 | ✅ — `businessRule.xml` |
|
||||
| XML namespace: 파일명과 동일 | ✅ — `namespace="businessRule"` |
|
||||
| SQL: UPPER_SNAKE | ✅ |
|
||||
| SELECT 쉼표: 앞에 | ✅ |
|
||||
| #{파라미터}: snake_case | ✅ |
|
||||
| OGNL test: 바깥 작은따옴표 | ✅ |
|
||||
| 프론트 타입: Record<string, any> | ✅ — FieldConfig만 예외 |
|
||||
| v5/ctrl CSS 변수 사용, 즉흥 값 금지 | ✅ |
|
||||
| 컴팩트 폰트 사이즈 (0.42~0.68rem) | ✅ |
|
||||
| mockup 진실의 원천 | ✅ — 06-control-mode.js, 07-rule-builder.js |
|
||||
|
||||
---
|
||||
|
||||
## 6. CSS 토큰 사용 목록
|
||||
|
||||
| 용도 | 토큰 |
|
||||
|---|---|
|
||||
| 시안 | `--ctrl-cyan` (#00cec9), `--ctrl-cyan-glow` |
|
||||
| 보라 | `--ctrl-primary` (#6c5ce7) |
|
||||
| 앰버 | `--ctrl-amber` (#fdcb6e) |
|
||||
| 핑크 | `--ctrl-pink` (#fd79a8) |
|
||||
| 그린 | `--ctrl-green` (#55efc4) |
|
||||
| 레드 | `--ctrl-red` (#ff4757) |
|
||||
| 유리 배경 | `--ctrl-glass`, `--ctrl-glass-strong` |
|
||||
| 유리 보더 | `--ctrl-glass-border` |
|
||||
| 텍스트 | `var(--v5-text)`, `var(--v5-text-sec)`, `var(--v5-text-muted)` |
|
||||
| 보더 | `var(--v5-border)` |
|
||||
| 서피스 | `var(--v5-surface)`, `var(--v5-surface-hover)` |
|
||||
| 블러 | `backdrop-filter: blur(20px) saturate(1.4)` (v5 글래스 패턴) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 검증 결과
|
||||
|
||||
| 검증 항목 | 결과 |
|
||||
|---|---|
|
||||
| `./gradlew compileJava` | BUILD SUCCESSFUL |
|
||||
| `./gradlew bootJar` | BUILD SUCCESSFUL |
|
||||
| `npx tsc --noEmit` (Phase 5 파일) | 에러 0개 |
|
||||
| DB 테이블 생성 (test_dev) | CREATE TABLE + INDEX 성공 |
|
||||
| 기존 레거시 에러 | 2827개 (Phase 5 무관, 변동 없음) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 완료 기준 대비 상태
|
||||
|
||||
| # | 기준 | 상태 |
|
||||
|---|---|---|
|
||||
| 1 | ⚡ 버튼으로 제어 모드 토글 | ✅ |
|
||||
| 2 | 캔버스 시각 변화 (시안 격자, 카드 반투명) | ✅ |
|
||||
| 3 | 카드 클릭 → 흐름 표시 (소스 테이블 + 관련 테이블 + 비즈니스 룰 트리) | ✅ |
|
||||
| 4 | 트리 확산 애니메이션 (선→노드 순차 등장) | ✅ |
|
||||
| 5 | 4종 연결선 (소스/자동/조건/FK) | ✅ |
|
||||
| 6 | 팔레트에서 노드 드래그앤드롭 | ✅ |
|
||||
| 7 | 포트 연결 (output→input 드래그, bezier 곡선) | ✅ |
|
||||
| 8 | 노드 설정 팝오버 (타입별 폼) | ✅ |
|
||||
| 9 | 규칙 저장/로드 (DB JSONB) | ✅ |
|
||||
| 10 | 빈 영역 클릭 → 흐름 닫기 | ✅ |
|
||||
| 11 | 다크/라이트 모드 | ✅ (라이트 보정 CSS 포함) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 접속 경로
|
||||
|
||||
- **대시보드 (제어 모드 포함)**: `/dash` → ⚡ 버튼 클릭
|
||||
- 개발자 빌더: `/admin/builder` (Phase 3)
|
||||
- 테스트 페이지: `/test-fc` (Phase 2)
|
||||
|
||||
---
|
||||
|
||||
## 10. Phase 5 범위 밖 (다음에 해야 할 것)
|
||||
|
||||
| 항목 | 비고 |
|
||||
|---|---|
|
||||
| `table_relationships` 테이블에 실제 데이터 INSERT | 현재 빈 테이블이면 흐름이 소스→1개만 표시 |
|
||||
| 비즈니스 룰 실행 엔진 (트리거/조건 평가/액션 실행) | 현재는 시각 편집만 |
|
||||
| 규칙 로드 UI (기존 규칙 목록에서 선택 → 복원) | 현재는 새 규칙 생성만 |
|
||||
| computed 수식 파서 (AST 기반 안전한 파서) | Phase 5+ |
|
||||
| DataPort 이벤트 버스 (카드 간 실시간 데이터 전달) | Phase 5+ |
|
||||
| 자동생성/프리셋 (Phase 6) | 모든 기능 갖춰진 후 마지막 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 파일별 코드 요약
|
||||
|
||||
### businessRule.xml (107줄)
|
||||
```
|
||||
7개 쿼리: getBusinessRuleList, getBusinessRuleListCnt, getBusinessRuleInfo,
|
||||
insertBusinessRule, updateBusinessRule, deleteBusinessRule, toggleBusinessRule
|
||||
common include: companyCodeFilter, dynamicOrderBy, pagination
|
||||
```
|
||||
|
||||
### BusinessRuleService.java (~95줄)
|
||||
```
|
||||
퍼블릭: getBusinessRuleList, getBusinessRuleInfo, insertBusinessRule,
|
||||
updateBusinessRule, deleteBusinessRule, toggleBusinessRule
|
||||
프라이빗: parseJsonField, stringifyJsonField (JSONB 유틸)
|
||||
```
|
||||
|
||||
### BusinessRuleController.java (~100줄)
|
||||
```
|
||||
6개 엔드포인트: GET/POST rules + GET/PUT/DELETE/toggle rule
|
||||
```
|
||||
|
||||
### useControlMode.ts (~160줄)
|
||||
```
|
||||
Zustand store: active, mode, flowEdges, ruleNodes, ruleConnections
|
||||
+ CTRL_NODE_TYPES 16종 정의 + genNodeId/genConnId 헬퍼
|
||||
```
|
||||
|
||||
### FlowViewer.tsx (~180줄)
|
||||
```
|
||||
카드 클릭 이벤트 → getMetaRelations → BFS → 위치 계산 → 순차 reveal
|
||||
TableNode + ConnectionSvg + FlowLine + FlowBadge 렌더
|
||||
```
|
||||
|
||||
### RuleBuilder.tsx (~180줄)
|
||||
```
|
||||
캔버스 드래그앤드롭 → 노드 생성 (테이블/제어)
|
||||
포트 연결 SVG + 삭제 뱃지 + NodeConfigPopover
|
||||
```
|
||||
|
||||
### control-mode.css (~350줄)
|
||||
```
|
||||
--ctrl-* 변수 체계, 연결선 4종 + pulse 애니메이션,
|
||||
테이블 노드, 제어 노드, I/O 포트, 설정 팝오버, 팔레트
|
||||
다크/라이트 모드 보정
|
||||
```
|
||||
Reference in New Issue
Block a user