From 7a65ab0f853a0593c0b59866787940e7e6c6e6e4 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 13 Mar 2026 15:02:06 +0900 Subject: [PATCH] docs: update full-screen analysis and V2 component usage guide - Revised the full-screen analysis document to reflect the latest updates, including the purpose and core rules for screen development. - Expanded the V2 component usage guide to include a comprehensive catalog of components, their configurations, and usage guidelines for LLM and chatbot applications. - Added a summary of the system architecture and clarified the implementation methods for user business screens and admin menus. - Enhanced the documentation to serve as a reference for AI agents and screen designers, ensuring adherence to the established guidelines. These updates aim to improve clarity and usability for developers and designers working with the WACE ERP screen composition system. Made-with: Cursor --- .../00_analysis/full-screen-analysis.md | 682 +++++---- .../00_analysis/v2-component-usage-guide.md | 1323 ++++++++++------- frontend/components/v2/V2Input.tsx | 120 +- frontend/lib/formatting/index.ts | 19 +- .../modal-repeater-table/RepeaterTable.tsx | 38 +- .../SelectedItemsDetailInputComponent.tsx | 12 +- .../SimpleRepeaterTableComponent.tsx | 40 +- .../SplitPanelLayoutComponent.tsx | 24 +- .../v2-table-list/TableListComponent.tsx | 33 +- 9 files changed, 1383 insertions(+), 908 deletions(-) diff --git a/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md b/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md index 9b4a9908..378b13b4 100644 --- a/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md +++ b/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md @@ -1,331 +1,485 @@ -# 화면 전체 분석 보고서 +# WACE ERP 화면 구성 시스템 전체 분석 -> **분석 대상**: `/Users/kimjuseok/Downloads/화면개발 8` 폴더 내 핵심 업무 화면 -> **분석 기준**: 메뉴별 분류, 3개 이상 재활용 가능한 컴포넌트 식별 -> **분석 일자**: 2026-01-30 +> **최종 업데이트**: 2026-03-13 +> **용도**: LLM 챗봇 / AI 에이전트가 화면 개발 요청을 받았을 때 참조하는 시스템 구조 레퍼런스 +> **핵심 규칙**: 사용자 업무 화면은 React 코드(.tsx)로 직접 만들지 않는다. DB 등록(screen_definitions + screen_layouts_v2 + menu_info)으로 구현한다. --- -## 1. 현재 사용 중인 V2 컴포넌트 목록 +## 1. 시스템 아키텍처 요약 -> **중요**: v2- 접두사가 붙은 컴포넌트만 사용합니다. +### 1.1 화면 렌더링 파이프라인 -### 입력 컴포넌트 -| ID | 이름 | 용도 | -|----|------|------| -| `v2-input` | V2 입력 | 텍스트, 숫자, 비밀번호, 이메일 등 입력 | -| `v2-select` | V2 선택 | 드롭다운, 콤보박스, 라디오, 체크박스 | -| `v2-date` | V2 날짜 | 날짜, 시간, 날짜범위 입력 | +``` +[DB] screen_definitions (화면 정의) + + screen_layouts_v2 (레이아웃 JSON) + + menu_info (메뉴 등록) + │ + ▼ +[Backend API] GET /api/screens/:screenId + │ + ▼ +[Frontend] /screens/[screenId]/page.tsx + │ + ▼ +[Converter] layoutV2Converter.ts → V2 JSON을 Legacy 포맷으로 변환 + │ + ▼ +[Renderer] ResponsiveGridRenderer → RealtimePreview → DynamicComponentRenderer + │ + ▼ +[Registry] ComponentRegistry.getComponent(componentType) → 실제 React 컴포넌트 렌더링 +``` -### 표시 컴포넌트 -| ID | 이름 | 용도 | -|----|------|------| -| `v2-text-display` | 텍스트 표시 | 라벨, 텍스트 표시 | -| `v2-card-display` | 카드 디스플레이 | 테이블 데이터를 카드 형태로 표시 | -| `v2-aggregation-widget` | 집계 위젯 | 합계, 평균, 개수 등 집계 표시 | +### 1.2 화면 유형 분류 -### 테이블/데이터 컴포넌트 -| ID | 이름 | 용도 | -|----|------|------| -| `v2-table-list` | 테이블 리스트 | 데이터 테이블 표시, 페이지네이션, 정렬, 필터 | -| `v2-table-search-widget` | 검색 필터 | 화면 내 테이블 검색/필터/그룹 기능 | -| `v2-pivot-grid` | 피벗 그리드 | 다차원 데이터 분석 (피벗 테이블) | +| 구분 | 구현 방식 | 코드 위치 | 예시 | +|------|----------|----------|------| +| **사용자 업무 화면** | DB 등록 (SQL INSERT) | screen_definitions + screen_layouts_v2 | 수주관리, 품목정보, BOM관리 | +| **관리자 메뉴** | React 코드 직접 작성 | frontend/app/(main)/admin/*/page.tsx | 사용자관리, 권한관리, 시스템설정 | -### 레이아웃 컴포넌트 -| ID | 이름 | 용도 | -|----|------|------| -| `v2-split-panel-layout` | 분할 패널 | 마스터-디테일 좌우 분할 레이아웃 | -| `v2-tabs-widget` | 탭 위젯 | 탭 전환, 탭 내 컴포넌트 배치 | -| `v2-section-card` | Section Card | 제목/테두리가 있는 그룹화 컨테이너 | -| `v2-section-paper` | Section Paper | 배경색 기반 미니멀 그룹화 컨테이너 | -| `v2-divider-line` | 구분선 | 영역 구분 | -| `v2-repeat-container` | 리피터 컨테이너 | 데이터 수만큼 내부 컴포넌트 반복 렌더링 | -| `v2-repeater` | 리피터 | 반복 컨트롤 | - -### 액션/기타 컴포넌트 -| ID | 이름 | 용도 | -|----|------|------| -| `v2-button-primary` | 기본 버튼 | 저장, 삭제 등 액션 버튼 | -| `v2-numbering-rule` | 채번규칙 | 자동 코드/번호 생성 | -| `v2-category-manager` | 카테고리 관리자 | 카테고리 관리 | -| `v2-location-swap-selector` | 위치 교환 선택기 | 위치 교환 기능 | -| `v2-rack-structure` | 랙 구조 | 창고 랙 시각화 | -| `v2-media` | 미디어 | 미디어 표시 | - -**총 23개 V2 컴포넌트** +**절대 규칙**: 사용자 업무 화면(생산, 영업, 구매, 물류, 품질 등)은 React 하드코딩 금지. 반드시 DB 등록 방식으로 구현. --- -## 2. 화면 분류 (메뉴별) +## 2. V2 컴포넌트 전체 목록 (32개) -### 01. 기준정보 (master-data) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 회사정보 | 회사정보.html | 검색+테이블 | ✅ 완전 | -| 부서정보 | 부서정보.html | 검색+테이블 | ✅ 완전 | -| 품목정보 | 품목정보.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 | -| BOM관리 | BOM관리.html | 분할패널+트리 | ⚠️ 트리뷰 미지원 | -| 공정정보관리 | 공정정보관리.html | 분할패널+테이블 | ✅ 완전 | -| 공정작업기준 | 공정작업기준관리.html | 검색+테이블 | ✅ 완전 | -| 품목라우팅 | 품목라우팅관리.html | 분할패널+테이블 | ✅ 완전 | +> 모든 컴포넌트는 `v2-` 접두사를 사용한다. 접두사 없는 컴포넌트는 레거시이므로 사용 금지. -### 02. 영업관리 (sales) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 수주관리 | 수주관리.html | 분할패널+테이블 | ✅ 완전 | -| 견적관리 | 견적관리.html | 분할패널+테이블 | ✅ 완전 | -| 거래처관리 | 거래처관리.html | 분할패널+탭+그룹화 | ⚠️ 그룹화 미지원 | -| 판매품목정보 | 판매품목정보.html | 검색+테이블 | ✅ 완전 | -| 출하계획관리 | 출하계획관리.html | 검색+테이블 | ✅ 완전 | +### 2.1 입력 컴포넌트 (9개) -### 03. 생산관리 (production) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 생산계획관리 | 생산계획관리.html | 분할패널+탭+타임라인 | ❌ 타임라인 미지원 | -| 생산관리 | 생산관리.html | 검색+테이블 | ✅ 완전 | -| 생산실적관리 | 생산실적관리.html | 검색+테이블 | ✅ 완전 | -| 작업지시 | 작업지시.html | 탭+그룹화테이블+분할패널 | ⚠️ 그룹화 미지원 | -| 공정관리 | 공정관리.html | 분할패널+테이블 | ✅ 완전 | +| ID | 용도 | 핵심 설정 | +|----|------|----------| +| `v2-input` | 텍스트, 숫자, 비밀번호, textarea, 슬라이더, 컬러, 버튼 | inputType, format(email/tel/url/currency/biz_no), required, readonly, maxLength | +| `v2-select` | 드롭다운, 콤보박스, 라디오, 체크박스, 태그, 토글, 스왑 | mode, source(static/code/db/api/entity/category/distinct/select), searchable, multiple, cascading | +| `v2-date` | 날짜, 시간, 날짜시간, 날짜범위, 월, 연도 | dateType(date/time/datetime), format, range, minDate, maxDate | +| `v2-file-upload` | 파일/이미지 업로드, 다중 업로드 | accept, maxSize, multiple | +| `v2-media` | 이미지, 비디오, 오디오 표시 | mediaType | +| `v2-location-swap-selector` | 출발지/도착지 선택 및 교환 | - | +| `v2-rack-structure` | 창고 랙 위치 일괄 생성 | columns, rows | +| `v2-process-work-standard` | 품목별 공정 작업기준 관리 (Pre/In/Post-Work) | - | +| `v2-item-routing` | 품목별 라우팅 버전 및 공정 순서 관리 (3단계 계층) | - | -### 04. 구매관리 (purchase) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 발주관리 | 발주관리.html | 검색+테이블 | ✅ 완전 | -| 공급업체관리 | 공급업체관리.html | 검색+테이블 | ✅ 완전 | -| 구매입고 | pages/구매입고.html | 검색+테이블 | ✅ 완전 | +### 2.2 표시/데이터 컴포넌트 (10개) -### 05. 설비관리 (equipment) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 설비정보 | 설비정보.html | 분할패널+카드+탭 | ✅ v2-card-display 활용 | +| ID | 용도 | 핵심 설정 | +|----|------|----------| +| `v2-table-list` | 데이터 테이블 (조회/편집, 페이지네이션, 정렬, 필터) | selectedTable, columns, pagination, displayMode(table/card), checkbox, linkedFilters | +| `v2-table-grouped` | 그룹화 테이블 (접기/펼치기, 그룹별 집계) | groupConfig(groupByColumn, summary), v2-table-list 기반 확장 | +| `v2-table-search-widget` | 테이블 검색/필터/그룹 바 | autoSelectFirstTable, showTableSelector | +| `v2-pivot-grid` | 다차원 피벗 분석 (행/열/데이터/필터 영역) | fields(area, summaryType, groupInterval), dataSource | +| `v2-text-display` | 라벨, 제목, 설명 텍스트 표시 | fontSize, fontWeight, color, textAlign | +| `v2-card-display` | 테이블 데이터를 카드 형태로 표시 | cardsPerRow, columnMapping(title/subtitle/image/status), cardStyle | +| `v2-aggregation-widget` | 합계, 평균, 개수, 최대, 최소 집계 카드 | items, filters, layout | +| `v2-status-count` | 상태별 건수 카드 표시 | statusField, countField | +| `v2-numbering-rule` | 자동 코드/번호 채번 (접두사+날짜+순번) | rule, prefix, format | +| `v2-category-manager` | 트리 기반 카테고리 관리 (3단계 계층) | - | -### 06. 물류관리 (logistics) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 창고관리 | 창고관리.html | 모바일앱스타일+iframe | ❌ 별도개발 필요 | -| 창고정보관리 | 창고정보관리.html | 검색+테이블 | ✅ 완전 | -| 입출고관리 | 입출고관리.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 | -| 재고현황 | 재고현황.html | 검색+테이블 | ✅ 완전 | +### 2.3 레이아웃 컴포넌트 (8개) -### 07. 품질관리 (quality) -| 화면명 | 파일명 | 패턴 | 구현 가능 | -|--------|--------|------|----------| -| 검사기준 | 검사기준.html | 검색+테이블 | ✅ 완전 | -| 검사정보관리 | 검사정보관리.html | 탭+테이블 | ✅ 완전 | -| 검사장비관리 | 검사장비관리.html | 검색+테이블 | ✅ 완전 | -| 불량관리 | 불량관리.html | 검색+테이블 | ✅ 완전 | -| 클레임관리 | 클레임관리.html | 검색+테이블 | ✅ 완전 | +| ID | 용도 | 핵심 설정 | +|----|------|----------| +| `v2-split-panel-layout` | 마스터-디테일 좌우 분할 | splitRatio, leftPanel(displayMode/tableName), rightPanel(relation/foreignKey), additionalTabs | +| `v2-tabs-widget` | 탭 전환, 탭 내 컴포넌트 배치 | tabs(id/label/components), defaultTab, orientation | +| `v2-section-card` | 제목+테두리가 있는 그룹화 컨테이너 | title, collapsible | +| `v2-section-paper` | 배경색 기반 미니멀 그룹화 컨테이너 | backgroundColor, padding | +| `v2-divider-line` | 영역 구분선 | orientation, thickness | +| `v2-split-line` | 캔버스 좌우 분할용 드래그 가능 세로선 | - | +| `v2-repeat-container` | 데이터 수만큼 내부 컴포넌트 반복 렌더링 | dataSourceType, layout, gridColumns | +| `v2-repeater` | 인라인/모달/버튼 모드 반복 데이터 관리 | mode(inline/modal/button) | + +### 2.4 특수/비즈니스 컴포넌트 (5개) + +| ID | 용도 | 핵심 설정 | +|----|------|----------| +| `v2-button-primary` | 저장/삭제/조회 등 액션 버튼 | text, actionType, variant, webTypeConfig.dataflowConfig | +| `v2-timeline-scheduler` | 간트차트형 일정/계획 시각화 (드래그/리사이즈) | selectedTable, resourceTable, fieldMapping, zoomLevel, editable | +| `v2-approval-step` | 결재 단계 스테퍼 시각화 | - | +| `v2-bom-tree` | BOM 계층 트리 표시 (정전개/역전개) | - | +| `v2-bom-item-editor` | BOM 하위품목 트리 편집 | - | --- -## 3. 화면 UI 패턴 분석 +## 3. 화면 UI 패턴 분류 (7가지) -### 패턴 A: 검색 + 테이블 (가장 기본) -**해당 화면**: 약 60% (15개 이상) +> AI가 화면 개발 요청을 받았을 때, 아래 패턴 중 해당하는 것을 선택하여 구현한다. -**사용 컴포넌트**: -- `v2-table-search-widget`: 검색 필터 -- `v2-table-list`: 데이터 테이블 +### 패턴 A: 기본 마스터 (검색 + 테이블) + +**적용 비율**: 약 50% (가장 흔함) +**적용 화면**: 코드관리, 부서정보, 창고정보, 검사기준, 불량관리, 공급업체관리 등 ``` -┌─────────────────────────────────────────┐ -│ [검색필드들...] [조회] [엑셀] │ ← v2-table-search-widget -├─────────────────────────────────────────┤ -│ 테이블 제목 [신규등록] [삭제] │ -│ ────────────────────────────────────── │ -│ □ | 코드 | 이름 | 상태 | 등록일 | │ ← v2-table-list -│ □ | A001 | 테스트| 사용 | 2026-01-30 | │ -└─────────────────────────────────────────┘ +┌─────────────────────────────────────────────────┐ +│ v2-table-search-widget │ +│ [검색필드1] [검색필드2] [조회] [엑셀] │ +├─────────────────────────────────────────────────┤ +│ v2-table-list │ +│ 제목 [신규] [삭제] │ +│ ─────────────────────────────────────────────── │ +│ □ | 코드 | 이름 | 상태 | 등록일 │ +└─────────────────────────────────────────────────┘ ``` -### 패턴 B: 분할 패널 (마스터-디테일) -**해당 화면**: 약 25% (8개) +**필수 컴포넌트**: `v2-table-search-widget` (1개) + `v2-table-list` (1개) -**사용 컴포넌트**: -- `v2-split-panel-layout`: 좌우 분할 -- `v2-table-list`: 마스터/디테일 테이블 -- `v2-tabs-widget`: 상세 탭 (선택) +### 패턴 B: 마스터-디테일 (좌우 분할) + +**적용 비율**: 약 25% +**적용 화면**: 공정관리, 수주관리, 견적관리, 품목라우팅 등 ``` -┌──────────────────┬──────────────────────┐ -│ 마스터 리스트 │ 상세 정보 / 탭 │ -│ ─────────────── │ ┌────┬────┬────┐ │ -│ □ A001 제품A │ │기본│이력│첨부│ │ -│ □ A002 제품B ← │ └────┴────┴────┘ │ -│ □ A003 제품C │ [테이블 or 폼] │ -└──────────────────┴──────────────────────┘ +┌──────────────────┬──────────────────────────────┐ +│ 마스터 테이블 │ 디테일 테이블/폼 │ +│ (좌측 패널) │ (우측 패널) │ +│ □ A001 항목1 │ [상세 정보 테이블] │ +│ □ A002 항목2 ← │ │ +└──────────────────┴──────────────────────────────┘ + v2-split-panel-layout ``` -### 패턴 C: 탭 + 테이블 -**해당 화면**: 약 10% (3개) +**필수 컴포넌트**: `v2-split-panel-layout` (1개) -**사용 컴포넌트**: -- `v2-tabs-widget`: 탭 전환 -- `v2-table-list`: 탭별 테이블 +### 패턴 C: 마스터-디테일 + 탭 + +**적용 비율**: 약 10% +**적용 화면**: 거래처관리, 설비정보, 품목정보 등 ``` -┌─────────────────────────────────────────┐ -│ [탭1] [탭2] [탭3] │ -├─────────────────────────────────────────┤ -│ [테이블 영역] │ -└─────────────────────────────────────────┘ +┌──────────────────┬──────────────────────────────┐ +│ 마스터 테이블 │ [기본] [이력] [첨부] │ +│ │ ┌────────────────────────┐ │ +│ □ A001 거래처1 │ │ 탭별 컨텐츠 │ │ +│ □ A002 거래처2 ← │ └────────────────────────┘ │ +└──────────────────┴──────────────────────────────┘ ``` -### 패턴 D: 특수 UI -**해당 화면**: 약 5% (2개) +**필수 컴포넌트**: `v2-split-panel-layout` (1개, rightPanel에 additionalTabs 설정) -- 생산계획관리: 타임라인/간트 차트 → **v2-timeline 미존재** -- 창고관리: 모바일 앱 스타일 → **별도 개발 필요** +### 패턴 D: 카드 뷰 + +**적용 비율**: 약 5% +**적용 화면**: 설비정보, 대시보드, 상품 카탈로그 등 + +``` +┌─────────────────────────────────────────────────┐ +│ v2-table-search-widget │ +├─────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ [이미지]│ │ [이미지]│ │ [이미지]│ │ +│ │ 제목 │ │ 제목 │ │ 제목 │ │ +│ │ 설명 │ │ 설명 │ │ 설명 │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ v2-card-display │ +└─────────────────────────────────────────────────┘ +``` + +**필수 컴포넌트**: `v2-card-display` (1개) + `v2-table-search-widget` (선택) + +### 패턴 E: 피벗 분석 + +**적용 비율**: 약 3% +**적용 화면**: 매출분석, 재고현황, 생산실적 분석 등 + +**필수 컴포넌트**: `v2-pivot-grid` (1개) + +### 패턴 F: 그룹화 테이블 + +**적용 비율**: 약 5% +**적용 화면**: 품목정보(카테고리별), 입출고관리(구분별), 작업지시(공정별) 등 + +``` +┌─────────────────────────────────────────────────┐ +│ [전체 펼치기] [전체 접기] │ +├─────────────────────────────────────────────────┤ +│ ▼ □ 그룹A 수량: 150 3건 │ +│ ├─ □ 항목1 50개 │ +│ ├─ □ 항목2 50개 │ +│ └─ □ 항목3 50개 │ +│ ► □ 그룹B 수량: 200 2건 │ +└─────────────────────────────────────────────────┘ +``` + +**필수 컴포넌트**: `v2-table-grouped` (1개) + +### 패턴 G: 타임라인/간트차트 + +**적용 비율**: 약 2% +**적용 화면**: 생산계획관리, 설비가동현황 등 + +``` +┌────────────┬─────────────────────────────────────┐ +│ │ 15(수) │ 16(목) │ 17(금) │ 18(토) │ +├────────────┼─────────────────────────────────────┤ +│ 설비A │ ████████████████ │ +│ 설비B │ █████████████████████ │ +│ 설비C │ ████████████████ │ +└────────────┴─────────────────────────────────────┘ +``` + +**필수 컴포넌트**: `v2-timeline-scheduler` (1개) --- -## 4. 신규 컴포넌트 분석 (3개 이상 재활용 기준) +## 4. 패턴 판단 의사결정 트리 -### 4.1 v2-grouped-table (그룹화 테이블) -**재활용 화면 수**: 5개 이상 ✅ +> AI가 화면 요청을 받았을 때 이 트리를 따라 패턴을 결정한다. -| 화면 | 그룹화 기준 | -|------|------------| -| 품목정보 | 품목구분, 카테고리 | -| 거래처관리 | 거래처유형, 지역 | -| 작업지시 | 작업일자, 공정 | -| 입출고관리 | 입출고구분, 창고 | -| 견적관리 | 상태, 거래처 | +``` +Q1. 시간축 기반 일정/간트차트가 필요한가? +├─ YES → 패턴 G (타임라인) +└─ NO ↓ -**기능 요구사항**: -- 특정 컬럼 기준 그룹핑 -- 그룹 접기/펼치기 -- 그룹 헤더에 집계 표시 -- 다중 그룹핑 지원 +Q2. 다차원 집계/피벗 분석이 필요한가? +├─ YES → 패턴 E (피벗) +└─ NO ↓ -**구현 복잡도**: 중 +Q3. 데이터를 그룹별로 묶어서 접기/펼치기가 필요한가? +├─ YES → 패턴 F (그룹화 테이블) +└─ NO ↓ -### 4.2 v2-tree-view (트리 뷰) -**재활용 화면 수**: 3개 ✅ +Q4. 이미지+정보를 카드 형태로 표시하는가? +├─ YES → 패턴 D (카드 뷰) +└─ NO ↓ -| 화면 | 트리 용도 | -|------|----------| -| BOM관리 | BOM 구조 (정전개/역전개) | -| 부서정보 | 조직도 | -| 메뉴관리 | 메뉴 계층 | +Q5. 마스터 테이블 선택 시 연관 디테일이 필요한가? +├─ YES → Q5-1. 디테일에 탭이 필요한가? +│ ├─ YES → 패턴 C (마스터-디테일+탭) +│ └─ NO → 패턴 B (마스터-디테일) +└─ NO → 패턴 A (기본 마스터) +``` -**기능 요구사항**: -- 노드 접기/펼치기 -- 드래그앤드롭 (선택) -- 정전개/역전개 전환 -- 노드 선택 이벤트 +--- -**구현 복잡도**: 중상 +## 5. 화면 구현 불가능/제한 사항 -### 4.3 v2-timeline-scheduler (타임라인) -**재활용 화면 수**: 1~2개 (기준 미달) +### 5.1 현재 불가능 (별도 React 개발 필요) -| 화면 | 용도 | +| 기능 | 상태 | 대안 | +|------|------|------| +| 칸반 보드 (드래그앤드롭) | 미지원 | 별도 React 컴포넌트 | +| 모바일 네이티브 앱 스타일 | 미지원 | 별도 개발 | +| 복잡한 차트 (line, bar, pie) | 미지원 | 외부 라이브러리 연동 | + +### 5.2 이전에 불가능했으나 현재 지원되는 기능 + +| 기능 | 지원 컴포넌트 | 추가 시점 | +|------|-------------|----------| +| 그룹화 테이블 | `v2-table-grouped` | 2026-01 | +| 타임라인/간트차트 | `v2-timeline-scheduler` | 2026-01 | +| BOM 트리 (정전개/역전개) | `v2-bom-tree` | 2026-02 | +| BOM 하위품목 편집 | `v2-bom-item-editor` | 2026-02 | +| 결재 스테퍼 | `v2-approval-step` | 2026-02 | + +### 5.3 권장하지 않는 조합 + +| 조합 | 이유 | |------|------| -| 생산계획관리 | 간트 차트 | -| 설비 가동 현황 | 타임라인 | - -**기능 요구사항**: -- 시간축 기반 배치 -- 드래그로 일정 변경 -- 공정별 색상 구분 -- 줌 인/아웃 - -**구현 복잡도**: 상 - -> **참고**: 3개 미만이므로 우선순위 하향 +| 3단계 이상 중첩 분할 | 화면 복잡도 증가, 성능 저하 | +| 탭 안에 탭 | 사용성 저하 | +| 한 화면에 3개 이상 테이블 | 데이터 로딩 성능 | +| 피벗 + 상세 테이블 동시 | 데이터 과부하 | --- -## 5. 컴포넌트 커버리지 +## 6. 화면별 구현 현황 (메뉴 분류) -### 현재 V2 컴포넌트로 구현 가능 -``` -┌─────────────────────────────────────────────────┐ -│ 17개 화면 (65%) │ -│ - 기본 검색 + 테이블 패턴 │ -│ - 분할 패널 │ -│ - 탭 전환 │ -│ - 카드 디스플레이 │ -└─────────────────────────────────────────────────┘ -``` +### 6.1 기준정보 -### v2-grouped-table 개발 후 -``` -┌─────────────────────────────────────────────────┐ -│ +5개 화면 (22개, 85%) │ -│ - 품목정보, 거래처관리, 작업지시 │ -│ - 입출고관리, 견적관리 │ -└─────────────────────────────────────────────────┘ -``` +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 회사정보 | A | 완전 지원 | | +| 부서정보 | A | 완전 지원 | | +| 품목정보 | F 또는 A | 완전 지원 | 카테고리별 그룹화 시 F | +| BOM관리 | B + v2-bom-tree | 완전 지원 | v2-bom-tree로 트리 구현 | +| 공정정보관리 | B | 완전 지원 | | +| 공정작업기준 | A | 완전 지원 | v2-process-work-standard 활용 가능 | +| 품목라우팅 | B | 완전 지원 | v2-item-routing 활용 가능 | -### v2-tree-view 개발 후 -``` -┌─────────────────────────────────────────────────┐ -│ +2개 화면 (24개, 92%) │ -│ - BOM관리, 부서정보(계층) │ -└─────────────────────────────────────────────────┘ -``` +### 6.2 영업관리 -### 별도 개발 필요 -``` -┌─────────────────────────────────────────────────┐ -│ 2개 화면 (8%) │ -│ - 생산계획관리 (타임라인) │ -│ - 창고관리 (모바일 앱 스타일) │ -└─────────────────────────────────────────────────┘ -``` +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 수주관리 | B | 완전 지원 | | +| 견적관리 | B | 완전 지원 | | +| 거래처관리 | C | 완전 지원 | 탭(기본/이력/첨부) | +| 판매품목정보 | A | 완전 지원 | | +| 출하계획관리 | A | 완전 지원 | | + +### 6.3 생산관리 + +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 생산계획관리 | G | 완전 지원 | v2-timeline-scheduler | +| 생산관리 | A | 완전 지원 | | +| 생산실적관리 | A | 완전 지원 | | +| 작업지시 | F | 완전 지원 | 공정별 그룹화 | +| 공정관리 | B | 완전 지원 | | + +### 6.4 구매관리 + +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 발주관리 | A | 완전 지원 | | +| 공급업체관리 | A | 완전 지원 | | +| 구매입고 | A | 완전 지원 | | + +### 6.5 설비관리 + +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 설비정보 | C 또는 D | 완전 지원 | 카드뷰 또는 탭 | + +### 6.6 물류관리 + +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 창고정보관리 | A | 완전 지원 | | +| 입출고관리 | F | 완전 지원 | 구분별 그룹화 | +| 재고현황 | A 또는 E | 완전 지원 | 피벗 분석도 가능 | + +### 6.7 품질관리 + +| 화면명 | 패턴 | 구현 가능 | 비고 | +|--------|------|----------|------| +| 검사기준 | A | 완전 지원 | | +| 검사정보관리 | C | 완전 지원 | 탭(수입검사/공정검사/출하검사) | +| 검사장비관리 | A | 완전 지원 | | +| 불량관리 | A | 완전 지원 | | +| 클레임관리 | A | 완전 지원 | | --- -## 6. 신규 컴포넌트 개발 우선순위 - -| 순위 | 컴포넌트 | 재활용 화면 수 | 복잡도 | ROI | -|------|----------|--------------|--------|-----| -| 1 | v2-grouped-table | 5+ | 중 | ⭐⭐⭐⭐⭐ | -| 2 | v2-tree-view | 3 | 중상 | ⭐⭐⭐⭐ | -| 3 | v2-timeline-scheduler | 1~2 | 상 | ⭐⭐ | - ---- - -## 7. 권장 구현 전략 - -### Phase 1: 즉시 구현 (현재 V2 컴포넌트) -- 회사정보, 부서정보 -- 발주관리, 공급업체관리 -- 검사기준, 검사장비관리, 불량관리 -- 창고정보관리, 재고현황 -- 공정작업기준관리 -- 수주관리, 견적관리, 공정관리 -- 설비정보 (v2-card-display 활용) -- 검사정보관리 - -### Phase 2: v2-grouped-table 개발 후 -- 품목정보, 거래처관리, 입출고관리 -- 작업지시 - -### Phase 3: v2-tree-view 개발 후 -- BOM관리 -- 부서정보 (계층 뷰) - -### Phase 4: 개별 개발 -- 생산계획관리 (타임라인) -- 창고관리 (모바일 스타일) - ---- - -## 8. 요약 +## 7. 컴포넌트 커버리지 요약 | 항목 | 수치 | |------|------| -| 전체 분석 화면 수 | 26개 | -| 현재 즉시 구현 가능 | 17개 (65%) | -| v2-grouped-table 추가 시 | 22개 (85%) | -| v2-tree-view 추가 시 | 24개 (92%) | -| 별도 개발 필요 | 2개 (8%) | +| 전체 분석 대상 화면 | 26개 | +| V2 컴포넌트로 구현 가능 | **26개 (100%)** | +| 등록된 V2 컴포넌트 수 | 32개 | +| 화면 UI 패턴 수 | 7가지 (A~G) | -**핵심 결론**: -1. **현재 V2 컴포넌트**로 65% 화면 구현 가능 -2. **v2-grouped-table** 1개 컴포넌트 개발로 85%까지 확대 -3. **v2-tree-view** 추가로 92% 도달 -4. 나머지 8%는 화면별 특수 UI (타임라인, 모바일 스타일)로 개별 개발 필요 +**이전 대비 변경 사항**: +- BOM 트리 지원 추가 (v2-bom-tree) → BOM관리 완전 지원 +- 그룹화 테이블 지원 추가 (v2-table-grouped) → 품목정보, 입출고 등 완전 지원 +- 타임라인 지원 추가 (v2-timeline-scheduler) → 생산계획관리 완전 지원 +- 결재 스테퍼 추가 (v2-approval-step) → 결재 프로세스 시각화 가능 +- 전체 커버리지: 65% → **100%** 달성 + +--- + +## 8. UI vs 비즈니스 로직 분리 구조 + +``` +┌───────────────────────────────┬───────────────────────────────────┐ +│ UI 레이아웃 │ 제어관리 (비즈니스 로직) │ +│ screen_layouts_v2 (JSON) │ dataflow_diagrams (JSONB) │ +├───────────────────────────────┼───────────────────────────────────┤ +│ - 컴포넌트 배치/크기/위치 │ - 버튼 클릭 시 액션 (INSERT 등) │ +│ - 검색 필드 구성 │ - 조건부 실행 │ +│ - 테이블 컬럼 표시/숨김 │ - 다중 행 일괄 처리 │ +│ - 카드/탭/분할 레이아웃 │ - 테이블 간 데이터 이동 │ +│ - 페이지네이션/정렬 설정 │ - 외부 시스템 호출 │ +└───────────────────────────────┴───────────────────────────────────┘ +``` + +**핵심**: V2 컴포넌트는 UI만 담당한다. 버튼 클릭 시 INSERT/UPDATE/DELETE 같은 비즈니스 로직은 `dataflow_diagrams` 테이블에서 별도 설정한다. + +### 비즈니스 로직 실행 흐름 + +``` +v2-button-primary 클릭 + → webTypeConfig.dataflowConfig 확인 + → ImprovedButtonActionExecutor 실행 + 1. Before 제어 실행 (조건 체크) + 2. Main 액션 실행 (save/delete/update) + 3. After 제어 실행 (후처리) +``` + +--- + +## 9. 화면 개발 순서 (AI 에이전트용) + +``` +Step 1: DB 테이블 생성 + └─ 모든 컬럼 VARCHAR(500), 기본 5개 컬럼 필수 (id, created_date, updated_date, writer, company_code) + └─ table_labels, table_type_columns, column_labels 메타데이터 등록 + +Step 2: screen_definitions INSERT + └─ screen_code = '{company_code}_{순번}' + └─ table_name = 메인 테이블명 + +Step 3: screen_layouts_v2 INSERT + └─ layout_data = V2 레이아웃 JSON (version: "2.0", components 배열) + └─ 패턴(A~G)에 맞는 컴포넌트 배치 + +Step 4: dataflow_diagrams INSERT (비즈니스 로직이 필요한 경우) + └─ 버튼별 액션 정의 (INSERT/UPDATE/DELETE) + └─ 조건, 필드 매핑 설정 + +Step 5: menu_info INSERT + └─ menu_url = '/screen/{screen_code}' + └─ 적절한 parent_id로 메뉴 트리에 배치 +``` + +--- + +## 10. DB 테이블 구조 레퍼런스 + +### screen_definitions + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| screen_id | integer PK (시퀀스) | 화면 고유 ID | +| screen_name | varchar(100) | 화면명 | +| screen_code | varchar(50) UNIQUE | 화면 코드 (`{company_code}_{순번}`) | +| table_name | varchar(100) | 기본 테이블명 | +| company_code | varchar(50) | 회사 코드 | +| description | text | 설명 | +| is_active | char(1) | Y/N/D | + +### screen_layouts_v2 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| layout_id | integer PK | 레이아웃 ID | +| screen_id | integer FK | 화면 ID | +| company_code | varchar(20) | 회사 코드 | +| layer_id | integer | 레이어 ID (1=기본) | +| layout_data | jsonb | 전체 레이아웃 JSON | + +### layout_data JSON 구조 + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 150, "z": 1 }, + "size": { "width": 1920, "height": 600 }, + "displayOrder": 0, + "overrides": { + "label": "품목 목록", + "tableName": "item_info", + "columns": [ + { "columnName": "item_code", "displayName": "품목코드", "visible": true } + ], + "pagination": { "enabled": true, "pageSize": 20 } + } + } + ], + "gridSettings": { "columns": 12, "gap": 16, "padding": 16 }, + "screenResolution": { "width": 1920, "height": 1080 } +} +``` + +### menu_info (주요 컬럼) + +| 컬럼 | 설명 | +|------|------| +| objid / menu_id | 메뉴 PK | +| screen_id | 연결된 화면 ID | +| menu_url | 메뉴 URL (`/screen/{screen_code}`) | +| company_code | 회사 코드 | diff --git a/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md b/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md index b37abf5e..1966a3c7 100644 --- a/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md +++ b/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md @@ -1,631 +1,846 @@ -# V2 공통 컴포넌트 사용 가이드 +# V2 컴포넌트 사용 가이드 (LLM/챗봇용) -> **목적**: 다양한 회사에서 V2 컴포넌트를 활용하여 화면을 개발할 때 참고하는 범용 가이드 -> **대상**: 화면 설계자, 개발자 -> **버전**: 1.1.0 -> **작성일**: 2026-02-23 (최종 업데이트) +> **최종 업데이트**: 2026-03-13 +> **용도**: LLM 챗봇이 화면 개발 요청을 받았을 때, 어떤 컴포넌트를 어떤 설정으로 사용해야 하는지 판단하는 레퍼런스 +> **버전**: 2.0.0 +> **대상**: AI 에이전트, 챗봇, 화면 설계자 --- -## 1. V2 컴포넌트로 가능한 것 / 불가능한 것 +## 0. 핵심 규칙 (반드시 준수) -### 1.1 가능한 화면 유형 - -| 화면 유형 | 설명 | 대표 예시 | -|-----------|------|----------| -| 마스터 관리 | 단일 테이블 CRUD | 회사정보, 부서정보, 코드관리 | -| 마스터-디테일 | 좌측 선택 → 우측 상세 | 공정관리, 품목라우팅, 견적관리 | -| 탭 기반 화면 | 탭별 다른 테이블/뷰 | 검사정보관리, 거래처관리 | -| 카드 뷰 | 이미지+정보 카드 형태 | 설비정보, 대시보드 | -| 피벗 분석 | 다차원 집계 | 매출분석, 재고현황 | -| 반복 컨테이너 | 데이터 수만큼 UI 반복 | 주문 상세, 항목 리스트 | -| 그룹화 테이블 | 그룹핑 기능 포함 테이블 | 카테고리별 집계, 부서별 현황 | -| 타임라인/스케줄 | 시간축 기반 일정 관리 | 생산일정, 작업스케줄 | - -### 1.2 불가능한 화면 유형 (별도 개발 필요) - -| 화면 유형 | 이유 | 해결 방안 | -|-----------|------|----------| -| 트리 뷰 (계층 구조) | 트리 컴포넌트 미존재 | `v2-tree-view` 개발 필요 | -| 드래그앤드롭 보드 | 칸반 스타일 UI 없음 | 별도 개발 | -| 모바일 앱 스타일 | 네이티브 앱 UI | 별도 개발 | -| 복잡한 차트 | 기본 집계 외 시각화 | 차트 라이브러리 연동 | - -> **참고**: 그룹화 테이블(`v2-table-grouped`)과 타임라인 스케줄러(`v2-timeline-scheduler`)는 v1.1에서 추가되어 이제 지원됩니다. +1. **사용자 업무 화면은 React 코드로 직접 만들지 않는다** → DB에 `screen_definitions` + `screen_layouts_v2` + `menu_info` INSERT로 구현 +2. **모든 컴포넌트는 `v2-` 접두사** → `v2-` 없는 레거시 컴포넌트 사용 금지 +3. **UI와 로직 분리** → UI는 `screen_layouts_v2`, 비즈니스 로직은 `dataflow_diagrams` +4. **멀티테넌시 필수** → 모든 테이블에 `company_code` 컬럼, 모든 쿼리에 `company_code` 필터링 --- -## 2. V2 컴포넌트 전체 목록 (25개) +## 1. 컴포넌트 전체 카탈로그 (32개) -### 2.1 입력 컴포넌트 (4개) +### 1.1 입력 컴포넌트 -| ID | 이름 | 용도 | 주요 옵션 | -|----|------|------|----------| -| `v2-input` | 입력 | 텍스트, 숫자, 비밀번호, 슬라이더, 컬러 | inputType(text/number/password/slider/color/button), format(email/tel/url/currency/biz_no), required, readonly, maxLength, min, max, step | -| `v2-select` | 선택 | 드롭다운, 콤보박스, 라디오, 체크, 태그, 토글, 스왑 | mode(dropdown/combobox/radio/check/tag/tagbox/toggle/swap), source(static/code/db/api/entity/category/distinct/select), searchable, multiple, cascading | -| `v2-date` | 날짜 | 날짜, 시간, 날짜시간 | dateType(date/time/datetime), format, range, minDate, maxDate, showToday | -| `v2-file-upload` | 파일 업로드 | 파일/이미지 업로드 | - | +#### v2-input -### 2.2 표시 컴포넌트 (3개) +텍스트, 숫자, 비밀번호, textarea 등 모든 단일 값 입력을 처리하는 통합 입력 컴포넌트. -| ID | 이름 | 용도 | 주요 옵션 | -|----|------|------|----------| -| `v2-text-display` | 텍스트 표시 | 라벨, 제목, 설명 텍스트 | fontSize, fontWeight, color, textAlign | -| `v2-card-display` | 카드 디스플레이 | 테이블 데이터를 카드 형태로 표시 | cardsPerRow, cardSpacing, columnMapping(titleColumn/subtitleColumn/descriptionColumn/imageColumn), cardStyle(imagePosition/imageSize), dataSource(table/static/api) | -| `v2-aggregation-widget` | 집계 위젯 | 합계, 평균, 개수, 최대, 최소 | items, filters, layout | +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| inputType | string | 입력 유형 | `text`, `number`, `password`, `slider`, `color`, `button`, `textarea` | +| format | string | 포맷 검증 | `email`, `tel`, `url`, `currency`, `biz_no` | +| required | boolean | 필수 여부 | true/false | +| readonly | boolean | 읽기 전용 | true/false | +| maxLength | number | 최대 길이 | 숫자 | +| min / max | number | 숫자 범위 | 숫자 (inputType=number일 때) | +| step | number | 증감 단위 | 숫자 (inputType=number/slider일 때) | +| tableName | string | 바인딩 테이블 | DB 테이블명 | +| columnName | string | 바인딩 컬럼 | DB 컬럼명 | -### 2.3 테이블/데이터 컴포넌트 (4개) +#### v2-select -| ID | 이름 | 용도 | 주요 옵션 | -|----|------|------|----------| -| `v2-table-list` | 테이블 리스트 | 데이터 조회/편집 테이블 | selectedTable, columns, pagination, filter, displayMode(table/card), checkbox, horizontalScroll, linkedFilters, excludeFilter, toolbar, tableStyle, autoLoad | -| `v2-table-search-widget` | 검색 필터 | 테이블 검색/필터/그룹 | autoSelectFirstTable, showTableSelector, title | -| `v2-pivot-grid` | 피벗 그리드 | 다차원 분석 (행/열/데이터 영역) | fields(area: row/column/data/filter, summaryType: sum/avg/count/min/max/countDistinct, groupInterval: year/quarter/month/week/day), dataSource(type: table/api/static, joinConfigs, filterConditions) | -| `v2-table-grouped` | 그룹화 테이블 | 그룹핑 기능이 포함된 테이블 | - | +드롭다운, 콤보박스, 라디오, 체크박스, 태그 등 선택형 입력 통합 컴포넌트. -### 2.4 레이아웃 컴포넌트 (7개) +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| mode | string | 선택 모드 | `dropdown`, `combobox`, `radio`, `check`, `tag`, `tagbox`, `toggle`, `swap` | +| source | string | 데이터 소스 | `static`, `code`, `db`, `api`, `entity`, `category`, `distinct`, `select` | +| searchable | boolean | 검색 가능 | true/false (mode=combobox일 때 기본 true) | +| multiple | boolean | 다중 선택 | true/false | +| cascading | object | 연쇄 선택 | 상위 select 값에 따라 하위 옵션 변경 | -| ID | 이름 | 용도 | 주요 옵션 | -|----|------|------|----------| -| `v2-split-panel-layout` | 분할 패널 | 마스터-디테일 좌우 분할 | splitRatio, resizable, minLeftWidth, minRightWidth, syncSelection, panel별: displayMode(list/table/custom), relation(type/foreignKey), editButton, addButton, deleteButton, additionalTabs | -| `v2-tabs-widget` | 탭 위젯 | 탭 전환, 탭 내 컴포넌트 | tabs(id/label/order/disabled/components), defaultTab, orientation(horizontal/vertical), allowCloseable, persistSelection | -| `v2-section-card` | 섹션 카드 | 제목+테두리 그룹화 | title, collapsible, padding | -| `v2-section-paper` | 섹션 페이퍼 | 배경색 그룹화 | backgroundColor, padding, shadow | -| `v2-divider-line` | 구분선 | 영역 구분 | orientation, thickness | -| `v2-repeat-container` | 리피터 컨테이너 | 데이터 수만큼 반복 렌더링 | dataSourceType, layout, gridColumns | -| `v2-repeater` | 리피터 | 반복 컨트롤 (inline/modal) | - | +**source 설명**: +- `static`: 고정 옵션 목록 (options 배열 직접 지정) +- `code`: code_info 테이블의 공통 코드 +- `db`: 특정 테이블의 컬럼 값 +- `entity`: 엔티티 조인 (다른 테이블 참조) +- `category`: v2-category-manager의 카테고리 +- `distinct`: 테이블 컬럼의 DISTINCT 값 -### 2.5 액션/특수 컴포넌트 (7개) +#### v2-date -| ID | 이름 | 용도 | 주요 옵션 | -|----|------|------|----------| -| `v2-button-primary` | 기본 버튼 | 저장, 삭제 등 액션 | text, actionType, variant | -| `v2-numbering-rule` | 채번규칙 | 자동 코드/번호 생성 | rule, prefix, format | -| `v2-category-manager` | 카테고리 관리자 | 카테고리 관리 UI | - | -| `v2-location-swap-selector` | 위치 교환 | 위치 선택/교환 | - | -| `v2-rack-structure` | 랙 구조 | 창고 랙 시각화 | - | -| `v2-media` | 미디어 | 이미지/동영상 표시 | - | -| `v2-timeline-scheduler` | 타임라인 스케줄러 | 시간축 기반 일정/작업 관리 | - | +날짜, 시간, 날짜시간, 날짜범위, 월, 연도 입력. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| dateType | string | 날짜 유형 | `date`, `time`, `datetime`, `month`, `year` | +| format | string | 표시 형식 | `YYYY-MM-DD`, `HH:mm`, `YYYY-MM-DD HH:mm` 등 | +| range | boolean | 범위 선택 | true/false (시작~종료 날짜) | +| minDate / maxDate | string | 선택 가능 범위 | ISO 8601 날짜 | +| showToday | boolean | 오늘 버튼 | true/false | + +#### v2-file-upload + +파일/이미지 업로드. 다중 업로드, 미리보기 지원. + +#### v2-media + +이미지, 비디오, 오디오 등 미디어 표시/재생. + +#### v2-location-swap-selector + +출발지/도착지 선택 및 교환 (물류 화면용). + +#### v2-rack-structure + +창고 랙 위치 일괄 생성 (열 범위, 단 수 설정). + +#### v2-process-work-standard + +품목별 공정 작업기준(Pre-Work/In-Work/Post-Work) 관리. 라우팅과 연동. + +#### v2-item-routing + +품목별 라우팅 버전 및 공정 순서 관리 (3단계 계층: 품목 → 라우팅 버전 → 공정). --- -## 3. 화면 패턴별 컴포넌트 조합 +### 1.2 표시/데이터 컴포넌트 -### 3.1 패턴 A: 기본 마스터 화면 (가장 흔함) +#### v2-table-list -**적용 화면**: 코드관리, 사용자관리, 부서정보, 창고정보 등 +가장 핵심 컴포넌트. DB 테이블 데이터를 조회/편집하는 테이블. -``` -┌─────────────────────────────────────────────────┐ -│ v2-table-search-widget │ -│ [검색필드1] [검색필드2] [조회] [엑셀] │ -├─────────────────────────────────────────────────┤ -│ v2-table-list │ -│ 제목 [신규] [삭제] │ -│ ─────────────────────────────────────────────── │ -│ □ | 코드 | 이름 | 상태 | 등록일 | │ -└─────────────────────────────────────────────────┘ -``` +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| selectedTable | string | **필수**. DB 테이블명 | 예: `"item_info"` | +| columns | array | 표시할 컬럼 설정 | `[{ columnName, displayName, visible, sortable, width, editable }]` | +| pagination | object | 페이지네이션 | `{ enabled: true, pageSize: 20, showSizeSelector: true }` | +| displayMode | string | 표시 모드 | `"table"` (기본) 또는 `"card"` | +| checkbox | object | 체크박스 | `{ enabled: true, multiple: true, position: "left", selectAll: true }` | +| horizontalScroll | object | 가로 스크롤 | `{ enabled: true, maxVisibleColumns: 8 }` | +| linkedFilters | array | 다른 컴포넌트와 연동 필터 | - | +| excludeFilter | object | 제외 필터 | - | +| autoLoad | boolean | 자동 데이터 로드 | true/false | +| stickyHeader | boolean | 헤더 고정 | true/false | +| toolbar | object | 툴바 (신규/삭제/엑셀 등) | - | +| tableStyle | object | 테이블 스타일 | `{ compact: true, striped: true }` | -**필수 컴포넌트**: -- `v2-table-search-widget` (1개) -- `v2-table-list` (1개) +**columns 배열 항목**: -**설정 포인트**: -- 테이블명 지정 -- 검색 대상 컬럼 설정 -- 컬럼 표시/숨김 설정 - ---- - -### 3.2 패턴 B: 마스터-디테일 화면 - -**적용 화면**: 공정관리, 견적관리, 수주관리, 품목라우팅 등 - -``` -┌──────────────────┬──────────────────────────────┐ -│ v2-table-list │ v2-table-list 또는 폼 │ -│ (마스터) │ (디테일) │ -│ ─────────────── │ │ -│ □ A001 항목1 │ [상세 정보] │ -│ □ A002 항목2 ← │ │ -│ □ A003 항목3 │ │ -└──────────────────┴──────────────────────────────┘ - v2-split-panel-layout -``` - -**필수 컴포넌트**: -- `v2-split-panel-layout` (1개) -- `v2-table-list` (2개: 마스터, 디테일) - -**설정 포인트**: -- `splitRatio`: 좌우 비율 (기본 30:70) -- `relation.type`: join / detail / custom -- `relation.foreignKey`: 연결 키 컬럼 - ---- - -### 3.3 패턴 C: 마스터-디테일 + 탭 - -**적용 화면**: 거래처관리, 품목정보, 설비정보 등 - -``` -┌──────────────────┬──────────────────────────────┐ -│ v2-table-list │ v2-tabs-widget │ -│ (마스터) │ ┌────┬────┬────┐ │ -│ │ │기본│이력│첨부│ │ -│ □ A001 거래처1 │ └────┴────┴────┘ │ -│ □ A002 거래처2 ← │ [탭별 컨텐츠] │ -└──────────────────┴──────────────────────────────┘ -``` - -**필수 컴포넌트**: -- `v2-split-panel-layout` (1개) -- `v2-table-list` (1개: 마스터) -- `v2-tabs-widget` (1개) - -**설정 포인트**: -- 탭별 표시할 테이블/폼 설정 -- 마스터 선택 시 탭 컨텐츠 연동 - ---- - -### 3.4 패턴 D: 카드 뷰 - -**적용 화면**: 설비정보, 대시보드, 상품 카탈로그 등 - -``` -┌─────────────────────────────────────────────────┐ -│ v2-table-search-widget │ -├─────────────────────────────────────────────────┤ -│ v2-card-display │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ [이미지]│ │ [이미지]│ │ [이미지]│ │ -│ │ 제목 │ │ 제목 │ │ 제목 │ │ -│ │ 설명 │ │ 설명 │ │ 설명 │ │ -│ └─────────┘ └─────────┘ └─────────┘ │ -└─────────────────────────────────────────────────┘ -``` - -**필수 컴포넌트**: -- `v2-table-search-widget` (1개) -- `v2-card-display` (1개) - -**설정 포인트**: -- `cardsPerRow`: 한 행당 카드 수 -- `columnMapping`: 제목, 부제목, 이미지, 상태 필드 매핑 -- `cardStyle`: 이미지 위치, 크기 - ---- - -### 3.5 패턴 E: 피벗 분석 - -**적용 화면**: 매출분석, 재고현황, 생산실적 분석 등 - -``` -┌─────────────────────────────────────────────────┐ -│ v2-pivot-grid │ -│ │ 2024년 │ 2025년 │ 2026년 │ 합계 │ -│ ─────────────────────────────────────────────── │ -│ 지역A │ 1,000 │ 1,200 │ 1,500 │ 3,700 │ -│ 지역B │ 2,000 │ 2,500 │ 3,000 │ 7,500 │ -│ 합계 │ 3,000 │ 3,700 │ 4,500 │ 11,200 │ -└─────────────────────────────────────────────────┘ -``` - -**필수 컴포넌트**: -- `v2-pivot-grid` (1개) - -**설정 포인트**: -- `fields[].area`: row / column / data / filter -- `fields[].summaryType`: sum / avg / count / min / max -- `fields[].groupInterval`: 날짜 그룹화 (year/quarter/month) - ---- - -## 4. 회사별 개발 시 핵심 체크포인트 - -### 4.1 테이블 설계 확인 - -**가장 먼저 확인**: -1. 회사에서 사용할 테이블 목록 -2. 테이블 간 관계 (FK) -3. 조회 조건으로 쓸 컬럼 - -``` -✅ 체크리스트: -□ 테이블명이 DB에 존재하는가? -□ company_code 컬럼이 있는가? (멀티테넌시) -□ 마스터-디테일 관계의 FK가 정의되어 있는가? -□ 검색 대상 컬럼에 인덱스가 있는가? -``` - -### 4.2 화면 패턴 판단 - -**질문을 통한 판단**: - -| 질문 | 예 → 패턴 | -|------|----------| -| 단일 테이블만 조회/편집? | 패턴 A (기본 마스터) | -| 마스터 선택 시 디테일 표시? | 패턴 B (마스터-디테일) | -| 상세에 탭이 필요? | 패턴 C (마스터-디테일+탭) | -| 이미지+정보 카드 형태? | 패턴 D (카드 뷰) | -| 다차원 집계/분석? | 패턴 E (피벗) | - -### 4.3 컴포넌트 설정 필수 항목 - -#### v2-table-list 필수 설정 - -```typescript +```json { - selectedTable: "테이블명", // 필수 - columns: [ // 표시할 컬럼 - { columnName: "id", displayName: "ID", visible: true, sortable: true }, - // ... - ], - pagination: { - enabled: true, - pageSize: 20, - showSizeSelector: true, - showPageInfo: true - }, - displayMode: "table", // "table" | "card" - checkbox: { - enabled: true, - multiple: true, - position: "left", - selectAll: true - }, - horizontalScroll: { // 가로 스크롤 설정 - enabled: true, - maxVisibleColumns: 8 - }, - linkedFilters: [], // 연결 필터 (다른 컴포넌트와 연동) - excludeFilter: {}, // 제외 필터 - autoLoad: true, // 자동 데이터 로드 - stickyHeader: false, // 헤더 고정 - autoWidth: true // 자동 너비 조정 + "columnName": "item_code", + "displayName": "품목코드", + "visible": true, + "sortable": true, + "width": 120, + "editable": false, + "inputType": "text", + "format": null, + "align": "left" } ``` -#### v2-split-panel-layout 필수 설정 +#### v2-table-grouped -```typescript +v2-table-list 기반 확장. 특정 컬럼 기준으로 데이터를 그룹화하여 접기/펼치기 기능 제공. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| selectedTable | string | **필수**. DB 테이블명 | - | +| groupConfig | object | **필수**. 그룹화 설정 | 아래 참조 | +| columns | array | 컬럼 설정 (v2-table-list와 동일) | - | +| showCheckbox | boolean | 체크박스 표시 | true/false | + +**groupConfig 구조**: + +```json { - leftPanel: { - displayMode: "table", // "list" | "table" | "custom" - tableName: "마스터_테이블명", - columns: [], // 컬럼 설정 - editButton: { // 수정 버튼 설정 - enabled: true, - mode: "auto", // "auto" | "modal" - modalScreenId: "" // 모달 모드 시 화면 ID - }, - addButton: { // 추가 버튼 설정 - enabled: true, - mode: "auto", - modalScreenId: "" - }, - deleteButton: { // 삭제 버튼 설정 - enabled: true, - buttonLabel: "삭제", - confirmMessage: "삭제하시겠습니까?" - }, - addModalColumns: [], // 추가 모달 전용 컬럼 - additionalTabs: [] // 추가 탭 설정 + "groupByColumn": "category", + "groupLabelFormat": "{category_name} ({category_code})", + "summary": { + "sumColumns": ["quantity", "amount"], + "showCount": true }, - rightPanel: { - displayMode: "table", - tableName: "디테일_테이블명", - relation: { - type: "detail", // "join" | "detail" | "custom" - foreignKey: "master_id", // 연결 키 - leftColumn: "", // 좌측 연결 컬럼 - rightColumn: "", // 우측 연결 컬럼 - keys: [] // 복합 키 - } - }, - splitRatio: 30, // 좌측 비율 (0-100) - resizable: true, // 리사이즈 가능 - minLeftWidth: 200, // 좌측 최소 너비 - minRightWidth: 300, // 우측 최소 너비 - syncSelection: true, // 선택 동기화 - autoLoad: true // 자동 로드 + "defaultExpanded": true, + "nestedGroup": null } ``` -#### v2-split-panel-layout 커스텀 모드 (NEW) +#### v2-table-search-widget -패널 내부에 자유롭게 컴포넌트를 배치할 수 있습니다. (v2-tabs-widget과 동일 구조) +테이블 상단에 배치하는 검색/필터 바. 대상 테이블의 컬럼을 자동 감지하여 검색 필드 생성. -```typescript +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| autoSelectFirstTable | boolean | 첫 번째 테이블 자동 선택 | true/false | +| showTableSelector | boolean | 테이블 선택기 표시 | true/false | +| title | string | 검색 바 제목 | - | + +#### v2-pivot-grid + +다차원 피벗 테이블. 행/열/데이터/필터 영역으로 데이터 분석. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| fields | array | **필수**. 필드 정의 | 아래 참조 | +| dataSource | object | 데이터 소스 | `{ type: "table"/"api"/"static", joinConfigs, filterConditions }` | + +**fields 배열 항목**: + +```json { - leftPanel: { - displayMode: "custom", // 커스텀 모드 활성화 - components: [ // 내부 컴포넌트 배열 - { - id: "btn-save", - componentType: "v2-button-primary", - label: "저장", - position: { x: 10, y: 10 }, - size: { width: 100, height: 40 }, - componentConfig: { buttonAction: "save" } - }, - { - id: "tbl-list", - componentType: "v2-table-list", - label: "목록", - position: { x: 10, y: 60 }, - size: { width: 400, height: 300 }, - componentConfig: { selectedTable: "테이블명" } - } - ] + "dataField": "region", + "area": "row", + "caption": "지역" +} +``` + +area 값: `"row"`, `"column"`, `"data"`, `"filter"` +summaryType (area=data일 때): `"sum"`, `"avg"`, `"count"`, `"min"`, `"max"`, `"countDistinct"` +groupInterval (날짜 필드): `"year"`, `"quarter"`, `"month"`, `"week"`, `"day"` + +#### v2-text-display + +라벨, 제목, 설명 텍스트 표시. + +| 설정 | 타입 | 설명 | +|------|------|------| +| fontSize | string | 폰트 크기 | +| fontWeight | string | 폰트 두께 | +| color | string | 텍스트 색상 | +| textAlign | string | 정렬 | + +#### v2-card-display + +테이블 데이터를 카드 형태로 표시. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| dataSource | string | 데이터 소스 | `"table"`, `"static"`, `"api"` | +| cardsPerRow | number | 행당 카드 수 | 1~6 | +| columnMapping | object | 필드 매핑 | `{ title, subtitle, description, image, status }` | +| cardStyle | object | 카드 스타일 | `{ imagePosition, imageSize }` | + +#### v2-aggregation-widget + +합계, 평균, 개수, 최대, 최소 등 집계 카드 표시. + +| 설정 | 타입 | 설명 | +|------|------|------| +| items | array | 집계 항목 배열 (테이블, 컬럼, 집계함수) | +| filters | array | 필터 조건 | +| layout | string | 레이아웃 (horizontal/vertical) | + +#### v2-status-count + +상태별 건수를 카드 형태로 표시. 대시보드/현황 화면에 적합. + +#### v2-numbering-rule + +자동 코드/번호 채번. 접두사 + 날짜 + 순번 조합. + +| 설정 | 타입 | 설명 | +|------|------|------| +| rule | string | 채번 규칙 | +| prefix | string | 접두사 | +| format | string | 포맷 | + +#### v2-category-manager + +트리 기반 카테고리 관리 (3단계 계층). 카테고리 생성/수정/삭제/이동. + +--- + +### 1.3 레이아웃 컴포넌트 + +#### v2-split-panel-layout + +**가장 복잡하고 중요한 컴포넌트**. 마스터-디테일 좌우 분할 레이아웃. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| splitRatio | number | 좌측 비율 (0-100) | 기본 30 | +| resizable | boolean | 리사이즈 가능 | true/false | +| minLeftWidth | number | 좌측 최소 너비 | 기본 200 | +| minRightWidth | number | 우측 최소 너비 | 기본 300 | +| syncSelection | boolean | 선택 동기화 | true/false | +| autoLoad | boolean | 자동 로드 | true/false | + +**leftPanel / rightPanel 구조**: + +```json +{ + "leftPanel": { + "displayMode": "table", + "tableName": "master_table", + "columns": [], + "showSearch": true, + "showAdd": true, + "showEdit": true, + "showDelete": true, + "addButton": { + "enabled": true, + "mode": "auto", + "modalScreenId": "" + }, + "editButton": { + "enabled": true, + "mode": "auto", + "modalScreenId": "" + }, + "deleteButton": { + "enabled": true, + "buttonLabel": "삭제", + "confirmMessage": "삭제하시겠습니까?" + }, + "addModalColumns": [], + "additionalTabs": [], + "dataFilter": {} }, - rightPanel: { - displayMode: "table" // 기존 모드 유지 + "rightPanel": { + "displayMode": "table", + "tableName": "detail_table", + "relation": { + "type": "detail", + "foreignKey": "master_id", + "leftColumn": "id", + "rightColumn": "master_id", + "keys": [] + }, + "addButton": { "enabled": true }, + "editButton": { "enabled": true }, + "deleteButton": { "enabled": true }, + "additionalTabs": [] } } ``` -**디자인 모드 기능**: -- 컴포넌트 클릭 → 좌측 설정 패널에서 속성 편집 -- 드래그 핸들(상단)로 이동 -- 리사이즈 핸들(모서리)로 크기 조절 -- 실제 컴포넌트 미리보기 렌더링 +**displayMode 종류**: +- `"list"`: 리스트 형태 (제목, 부제목) +- `"table"`: 테이블 형태 (컬럼, 행) +- `"custom"`: 자유 배치 (components 배열로 내부 컴포넌트 배치) -#### v2-card-display 필수 설정 +**relation.type 종류**: +- `"join"`: 두 테이블 JOIN +- `"detail"`: 마스터 PK → 디테일 FK 관계 +- `"custom"`: 커스텀 관계 (leftColumn, rightColumn 직접 지정) -```typescript -{ - dataSource: "table", - columnMapping: { - title: "name", // 제목 필드 - subtitle: "code", // 부제목 필드 - image: "image_url", // 이미지 필드 (선택) - status: "status" // 상태 필드 (선택) - }, - cardsPerRow: 3 -} -``` +**additionalTabs** (우측 패널에 멀티 탭 배치): ---- - -## 5. 공통 컴포넌트 한계점 - -### 5.1 현재 불가능한 기능 - -| 기능 | 상태 | 대안 | -|------|------|------| -| 트리 뷰 (BOM, 조직도) | ❌ 미지원 | 테이블로 대체 or 별도 개발 | -| 드래그앤드롭 정렬 | ❌ 미지원 | 순서 컬럼으로 대체 | -| 인라인 편집 | ⚠️ 제한적 | 모달 편집으로 대체 | -| 복잡한 차트 | ❌ 미지원 | 외부 라이브러리 연동 | - -> **v1.1 업데이트**: 그룹화 테이블(`v2-table-grouped`)과 타임라인 스케줄러(`v2-timeline-scheduler`)가 추가되어 해당 기능은 이제 지원됩니다. - -### 5.2 권장하지 않는 조합 - -| 조합 | 이유 | -|------|------| -| 3단계 이상 중첩 분할 | 화면 복잡도 증가, 성능 저하 | -| 탭 안에 탭 | 사용성 저하 | -| 한 화면에 3개 이상 테이블 | 데이터 로딩 성능 | -| 피벗 + 상세 테이블 동시 | 데이터 과부하 | - ---- - -## 6. 제어관리 (비즈니스 로직) - 별도 설정 필수 - -> **핵심**: V2 컴포넌트는 **UI만 담당**합니다. 비즈니스 로직은 **제어관리**에서 별도 설정해야 합니다. - -### 6.1 UI vs 제어 분리 구조 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 화면 구성 │ -├─────────────────────────────┬───────────────────────────────────┤ -│ UI 레이아웃 │ 제어관리 │ -│ (screen_layouts_v2) │ (dataflow_diagrams) │ -├─────────────────────────────┼───────────────────────────────────┤ -│ • 컴포넌트 배치 │ • 버튼 클릭 시 액션 │ -│ • 검색 필드 구성 │ • INSERT/UPDATE/DELETE 설정 │ -│ • 테이블 컬럼 표시 │ • 조건부 실행 │ -│ • 카드/탭 레이아웃 │ • 다중 행 처리 │ -│ │ • 테이블 간 데이터 이동 │ -└─────────────────────────────┴───────────────────────────────────┘ -``` - -### 6.2 HTML에서 파악 가능/불가능 - -| 구분 | HTML에서 파악 | 이유 | -|------|--------------|------| -| 컴포넌트 배치 | ✅ 가능 | HTML 구조에서 보임 | -| 검색 필드 | ✅ 가능 | input 태그로 확인 | -| 테이블 컬럼 | ✅ 가능 | thead에서 확인 | -| **저장 테이블** | ❌ 불가능 | JS/백엔드에서 처리 | -| **버튼 액션** | ❌ 불가능 | 제어관리에서 설정 | -| **전/후 처리** | ❌ 불가능 | 제어관리에서 설정 | -| **다중 행 처리** | ❌ 불가능 | 제어관리에서 설정 | -| **테이블 간 관계** | ❌ 불가능 | DB/제어관리에서 설정 | - -### 6.3 제어관리 설정 항목 - -#### 트리거 타입 -- **버튼 클릭 전 (before)**: 클릭 직전 실행 -- **버튼 클릭 후 (after)**: 클릭 완료 후 실행 - -#### 액션 타입 -- **INSERT**: 새로운 데이터 삽입 -- **UPDATE**: 기존 데이터 수정 -- **DELETE**: 데이터 삭제 - -#### 조건 설정 -```typescript -// 예: 선택된 행의 상태가 '대기'인 경우에만 실행 -{ - field: "status", - operator: "=", - value: "대기", - dataType: "string" -} -``` - -#### 필드 매핑 -```typescript -// 예: 소스 테이블의 값을 타겟 테이블로 이동 -{ - sourceTable: "order_master", - sourceField: "order_no", - targetTable: "order_history", - targetField: "order_no" -} -``` - -### 6.4 제어관리 예시: 수주 확정 버튼 - -**시나리오**: 수주 목록에서 3건 선택 후 [확정] 버튼 클릭 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ [확정] 버튼 클릭 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 조건 체크: status = '대기' 인 행만 │ -│ 2. UPDATE order_master SET status = '확정' WHERE id IN (선택) │ -│ 3. INSERT order_history (수주이력 테이블에 기록) │ -│ 4. 외부 시스템 호출 (ERP 연동) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**제어관리 설정**: ```json { - "triggerType": "after", - "actions": [ + "additionalTabs": [ { - "actionType": "update", - "targetTable": "order_master", - "conditions": [{ "field": "status", "operator": "=", "value": "대기" }], - "fieldMappings": [{ "targetField": "status", "defaultValue": "확정" }] + "id": "tab1", + "label": "기본정보", + "tableName": "detail_basic", + "relation": { "type": "detail", "foreignKey": "master_id" } }, { - "actionType": "insert", - "targetTable": "order_history", - "fieldMappings": [ - { "sourceField": "order_no", "targetField": "order_no" }, - { "sourceField": "customer_name", "targetField": "customer_name" } + "id": "tab2", + "label": "이력", + "tableName": "detail_history", + "relation": { "type": "detail", "foreignKey": "master_id" } + } + ] +} +``` + +#### v2-tabs-widget + +탭 전환 레이아웃. 탭 내부에 컴포넌트를 배치할 수 있음. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| tabs | array | **필수**. 탭 배열 | `[{ id, label, order, disabled, components }]` | +| defaultTab | string | 기본 활성 탭 ID | - | +| orientation | string | 탭 방향 | `"horizontal"`, `"vertical"` | +| persistSelection | boolean | 선택 유지 | true/false | + +**tabs 항목 구조**: + +```json +{ + "id": "tab-basic", + "label": "기본정보", + "order": 0, + "disabled": false, + "components": [ + { + "id": "tbl-basic", + "componentType": "v2-table-list", + "label": "기본정보", + "position": { "x": 0, "y": 0 }, + "size": { "width": 800, "height": 400 }, + "componentConfig": { "selectedTable": "basic_info" } + } + ] +} +``` + +#### v2-section-card + +제목+테두리가 있는 그룹화 컨테이너. 폼 필드를 묶을 때 사용. + +| 설정 | 타입 | 설명 | +|------|------|------| +| title | string | 섹션 제목 | +| collapsible | boolean | 접기/펼치기 가능 | +| padding | string | 내부 여백 | + +#### v2-section-paper + +배경색 기반 미니멀 그룹화 컨테이너. + +#### v2-divider-line + +영역 구분선 (가로/세로). + +#### v2-split-line + +캔버스 화면에서 좌우 영역을 분할하는 드래그 가능한 세로선. 디자이너 모드에서 사용. + +#### v2-repeat-container + +데이터 수만큼 내부 컴포넌트를 반복 렌더링. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| dataSourceType | string | 데이터 소스 | `"table"`, `"api"`, `"static"` | +| layout | string | 레이아웃 | `"vertical"`, `"horizontal"`, `"grid"` | +| gridColumns | number | 그리드 열 수 | 1~12 | + +#### v2-repeater + +인라인/모달/버튼 모드로 반복 데이터를 관리하는 컴포넌트. 주문 상세, 항목 리스트 등. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| mode | string | 동작 모드 | `"inline"` (인라인 편집), `"modal"` (모달 팝업), `"button"` (버튼 트리거) | + +--- + +### 1.4 특수/비즈니스 컴포넌트 + +#### v2-button-primary + +저장, 삭제, 조회 등 액션 버튼. 제어관리(dataflow)와 연결 가능. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| text | string | 버튼 텍스트 | 예: `"저장"`, `"삭제"` | +| actionType | string | 액션 유형 | `"save"`, `"delete"`, `"search"`, `"custom"` | +| variant | string | 스타일 | `"default"`, `"outline"`, `"destructive"`, `"ghost"` | + +**제어관리 연결** (webTypeConfig.dataflowConfig): + +```json +{ + "webTypeConfig": { + "enableDataflowControl": true, + "dataflowConfig": { + "controlMode": "relationship", + "relationshipConfig": { + "relationshipId": "rel_001", + "relationshipName": "수주확정", + "executionTiming": "after" + } + } + } +} +``` + +executionTiming 값: +- `"before"`: 메인 액션 전에 제어 실행 (조건 체크용) +- `"after"`: 메인 액션 후에 제어 실행 (후처리용) +- `"replace"`: 메인 액션 대신 제어만 실행 + +#### v2-timeline-scheduler + +간트차트형 일정/계획 시각화. 드래그/리사이즈로 일정 변경 가능. + +| 설정 | 타입 | 설명 | 값 | +|------|------|------|----| +| selectedTable | string | 스케줄 테이블명 | - | +| resourceTable | string | 리소스 테이블명 (설비/작업자) | - | +| fieldMapping | object | 필드 매핑 | `{ id, resourceId, title, startDate, endDate, status, progress }` | +| resourceFieldMapping | object | 리소스 필드 매핑 | `{ id, name, group }` | +| defaultZoomLevel | string | 초기 줌 | `"day"`, `"week"`, `"month"` | +| editable | boolean | 편집 가능 | true/false | +| allowDrag | boolean | 드래그 이동 허용 | true/false | +| allowResize | boolean | 리사이즈 허용 | true/false | + +#### v2-approval-step + +결재 단계 스테퍼 시각화. 결재 프로세스의 진행 상태를 단계별로 표시. + +#### v2-bom-tree + +BOM(Bill of Materials) 계층 트리 표시. 정전개(상위→하위)/역전개(하위→상위) 전환 가능. + +#### v2-bom-item-editor + +BOM 하위품목 트리 편집. 하위 부품 추가/수정/삭제/수량 변경 등. + +--- + +## 2. 화면 패턴별 상세 구현 가이드 + +### 2.1 패턴 A: 기본 마스터 (검색 + 테이블) + +**적용 조건**: 단일 테이블 CRUD. 마스터-디테일 관계 없음. + +**layout_data 예시**: + +```json +{ + "version": "2.0", + "components": [ + { + "id": "search_widget", + "url": "@/lib/registry/components/v2-table-search-widget", + "position": { "x": 0, "y": 0, "z": 1 }, + "size": { "width": 1920, "height": 100 }, + "displayOrder": 0, + "overrides": { + "label": "검색", + "autoSelectFirstTable": true + } + }, + { + "id": "table_list", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 120, "z": 1 }, + "size": { "width": 1920, "height": 700 }, + "displayOrder": 1, + "overrides": { + "label": "품목 목록", + "tableName": "item_info", + "columns": [ + { "columnName": "item_code", "displayName": "품목코드", "visible": true, "sortable": true, "width": 120 }, + { "columnName": "item_name", "displayName": "품목명", "visible": true, "sortable": true, "width": 200 }, + { "columnName": "item_type", "displayName": "품목유형", "visible": true, "width": 100 }, + { "columnName": "unit", "displayName": "단위", "visible": true, "width": 80 } + ], + "pagination": { "enabled": true, "pageSize": 20 }, + "checkbox": { "enabled": true, "multiple": true } + } + } + ], + "gridSettings": { "columns": 12, "gap": 16, "padding": 16 }, + "screenResolution": { "width": 1920, "height": 1080 } +} +``` + +### 2.2 패턴 B: 마스터-디테일 (좌우 분할) + +**적용 조건**: 마스터 테이블 선택 시 연관 디테일 테이블 표시. 두 테이블 간 FK 관계 존재. + +**layout_data 예시**: + +```json +{ + "version": "2.0", + "components": [ + { + "id": "split_panel", + "url": "@/lib/registry/components/v2-split-panel-layout", + "position": { "x": 0, "y": 0, "z": 1 }, + "size": { "width": 1920, "height": 800 }, + "displayOrder": 0, + "overrides": { + "label": "수주관리", + "splitRatio": 35, + "resizable": true, + "leftPanel": { + "displayMode": "table", + "tableName": "order_master", + "columns": [ + { "columnName": "order_no", "displayName": "수주번호", "visible": true, "width": 120 }, + { "columnName": "customer_name", "displayName": "거래처", "visible": true, "width": 150 }, + { "columnName": "order_date", "displayName": "수주일자", "visible": true, "width": 100 }, + { "columnName": "status", "displayName": "상태", "visible": true, "width": 80 } + ], + "showSearch": true, + "showAdd": true, + "showDelete": true, + "addButton": { "enabled": true, "mode": "auto" }, + "deleteButton": { "enabled": true, "confirmMessage": "삭제하시겠습니까?" } + }, + "rightPanel": { + "displayMode": "table", + "tableName": "order_detail", + "relation": { + "type": "detail", + "foreignKey": "order_master_id", + "leftColumn": "id", + "rightColumn": "order_master_id" + }, + "columns": [ + { "columnName": "item_code", "displayName": "품목코드", "visible": true, "width": 120 }, + { "columnName": "item_name", "displayName": "품목명", "visible": true, "width": 150 }, + { "columnName": "quantity", "displayName": "수량", "visible": true, "width": 80, "editable": true }, + { "columnName": "unit_price", "displayName": "단가", "visible": true, "width": 100, "editable": true } + ], + "addButton": { "enabled": true }, + "deleteButton": { "enabled": true } + } + } + } + ], + "gridSettings": { "columns": 12, "gap": 16, "padding": 16 }, + "screenResolution": { "width": 1920, "height": 1080 } +} +``` + +### 2.3 패턴 C: 마스터-디테일 + 탭 + +**적용 조건**: 마스터-디테일에서 우측에 여러 종류의 상세 정보를 탭으로 분리. + +**차이점**: rightPanel에 `additionalTabs` 배열 추가. + +```json +{ + "rightPanel": { + "displayMode": "table", + "tableName": "customer_basic", + "relation": { + "type": "detail", + "foreignKey": "customer_id" + }, + "additionalTabs": [ + { + "id": "tab-basic", + "label": "기본정보", + "tableName": "customer_basic", + "relation": { "type": "detail", "foreignKey": "customer_id" } + }, + { + "id": "tab-history", + "label": "거래이력", + "tableName": "customer_history", + "relation": { "type": "detail", "foreignKey": "customer_id" } + }, + { + "id": "tab-files", + "label": "첨부파일", + "tableName": "customer_files", + "relation": { "type": "detail", "foreignKey": "customer_id" } + } + ] + } +} +``` + +--- + +## 3. 제어관리 (비즈니스 로직) 설정 + +### 3.1 개요 + +V2 컴포넌트는 **UI만 담당**한다. 버튼 클릭 시 실행되는 비즈니스 로직(INSERT/UPDATE/DELETE, 조건 체크, 외부 호출 등)은 `dataflow_diagrams` 테이블에서 별도 설정한다. + +### 3.2 설정 위치 + +| 구분 | 저장 위치 | 용도 | +|------|----------|------| +| 버튼-제어 연결 | `screen_layouts_v2` → 컴포넌트 `webTypeConfig.dataflowConfig` | 어떤 버튼이 어떤 제어를 실행하는지 | +| 제어 정의 | `dataflow_diagrams` → `control`, `plan` JSONB | 조건, 액션, 필드 매핑 | +| 노드 플로우 | `node_flows` → `flow_data` JSONB | 복잡한 다단계 플로우 | + +### 3.3 실행 흐름 + +``` +[버튼 클릭] v2-button-primary + │ + ▼ +[설정 확인] webTypeConfig.dataflowConfig + │ + ├─ controlMode: "relationship" → dataflow_diagrams 실행 + ├─ controlMode: "flow" → node_flows 실행 + └─ controlMode: "none" → 기본 액션만 실행 + │ + ▼ +[실행기] ImprovedButtonActionExecutor + │ + ├─ 1. Before 제어 (executionTiming = "before") + │ └─ 조건 체크 → 실패 시 중단 + │ + ├─ 2. Main 액션 (executionTiming ≠ "replace"일 때) + │ └─ save / delete / update 실행 + │ + └─ 3. After 제어 (executionTiming = "after") + └─ 후처리 (이력 저장, 상태 변경, 외부 호출 등) +``` + +### 3.4 dataflow_diagrams 설정 예시 + +**시나리오**: 수주 목록에서 [확정] 버튼 클릭 → 상태를 '확정'으로 변경 + 이력 테이블에 기록 + +```json +{ + "control": [ + { + "conditions": [ + { "field": "status", "operator": "=", "value": "대기", "dataType": "string" } + ], + "triggerType": "update" + } + ], + "plan": [ + { + "actions": [ + { + "actionType": "update", + "targetTable": "order_master", + "conditions": [ + { "field": "status", "operator": "=", "value": "대기" } + ], + "fieldMappings": [ + { "targetField": "status", "defaultValue": "확정" } + ] + }, + { + "actionType": "insert", + "targetTable": "order_history", + "fieldMappings": [ + { "sourceField": "order_no", "targetField": "order_no" }, + { "sourceField": "customer_name", "targetField": "customer_name" }, + { "targetField": "action", "defaultValue": "확정" } + ] + } ] } ] } ``` -### 6.5 회사별 개발 시 제어관리 체크리스트 +--- + +## 4. 화면 개발 체크리스트 (AI 에이전트용) + +화면 개발 요청을 받았을 때 이 순서대로 확인한다. + +### Step 1: 요구사항 분석 + +``` +□ 어떤 테이블을 사용하는가? (메인 테이블, 디테일 테이블) +□ 테이블 간 관계는? (FK, 1:N, M:N) +□ 어떤 패턴인가? (A~G 중 선택, 의사결정 트리 참조) +□ 어떤 버튼이 필요한가? (저장, 삭제, 확정 등) +□ 비즈니스 로직이 있는가? (상태 변경, 이력 기록, 외부 호출 등) +``` + +### Step 2: DB 테이블 생성 + +``` +□ 모든 컬럼 VARCHAR(500) +□ 기본 5개 컬럼 포함 (id, created_date, updated_date, writer, company_code) +□ table_labels, table_type_columns, column_labels 메타데이터 등록 +□ company_code 인덱스 생성 +``` + +### Step 3: screen_definitions INSERT + +```sql +INSERT INTO screen_definitions (screen_name, screen_code, table_name, company_code, is_active) +VALUES ('화면명', '{company_code}_순번', '메인_테이블', '{company_code}', 'Y'); +``` + +### Step 4: screen_layouts_v2 INSERT + +```sql +INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layout_data) +VALUES ({screen_id}, '{company_code}', 1, '{패턴별 JSON}'); +``` + +### Step 5: dataflow_diagrams INSERT (비즈니스 로직 필요 시) ``` □ 버튼별 액션 정의 - - 어떤 버튼이 있는가? - - 각 버튼 클릭 시 무슨 동작? +□ 조건 설정 (어떤 상태일 때만 실행) +□ 필드 매핑 (소스→타겟) +□ 실행 타이밍 (before/after/replace) +``` -□ 저장/수정/삭제 대상 테이블 - - 메인 테이블은? - - 이력 테이블은? - - 연관 테이블은? +### Step 6: menu_info INSERT -□ 조건부 실행 - - 특정 상태일 때만 실행? - - 특정 값 체크 필요? +```sql +INSERT INTO menu_info (screen_id, menu_url, company_code, ...) +VALUES ({screen_id}, '/screen/{screen_code}', '{company_code}', ...); +``` -□ 다중 행 처리 - - 여러 행 선택 후 일괄 처리? - - 각 행별 개별 처리? +### Step 7: 멀티테넌시 검증 -□ 외부 연동 - - ERP/MES 등 외부 시스템 호출? - - API 연동 필요? +``` +□ 테이블에 company_code 컬럼 존재 +□ 모든 SELECT에 company_code 필터링 +□ INSERT에 company_code 포함 +□ UPDATE/DELETE WHERE절에 company_code 포함 +□ JOIN에 company_code 매칭 ``` --- -## 7. 회사별 커스터마이징 영역 +## 5. 자주 묻는 질문 (FAQ) -### 7.1 컴포넌트로 처리되는 영역 (표준화) +### Q: v2-table-list와 v2-table-grouped 중 어떤 걸 써야 하나? -| 영역 | 설명 | -|------|------| -| UI 레이아웃 | 컴포넌트 배치, 크기, 위치 | -| 검색 조건 | 화면 디자이너에서 설정 | -| 테이블 컬럼 | 표시/숨김, 순서, 너비 | -| 기본 CRUD | 조회, 저장, 삭제 자동 처리 | -| 페이지네이션 | 자동 처리 | -| 정렬/필터 | 자동 처리 | +**A**: 데이터를 특정 컬럼 기준으로 묶어서 접기/펼치기가 필요하면 `v2-table-grouped`, 아니면 `v2-table-list`. 예를 들어 "카테고리별 품목 목록"은 `v2-table-grouped`, "품목 전체 목록"은 `v2-table-list`. -### 7.2 회사별 개발 필요 영역 +### Q: 마스터-디테일에서 탭을 사용하고 싶은데? -| 영역 | 설명 | 개발 방법 | -|------|------|----------| -| 비즈니스 로직 | 저장 전/후 검증, 계산 | 데이터플로우 또는 백엔드 API | -| 특수 UI | 간트, 트리, 차트 등 | 별도 컴포넌트 개발 | -| 외부 연동 | ERP, MES 등 연계 | 외부 호출 설정 | -| 리포트/인쇄 | 전표, 라벨 출력 | 리포트 컴포넌트 | -| 결재 프로세스 | 승인/반려 흐름 | 워크플로우 설정 | +**A**: `v2-split-panel-layout`의 rightPanel에 `additionalTabs` 배열을 설정한다. 별도의 `v2-tabs-widget`을 배치할 필요 없다. + +### Q: BOM 화면은 어떻게 만드나? + +**A**: `v2-split-panel-layout`의 좌측에 `v2-bom-tree` (또는 `v2-table-list`), 우측에 `v2-bom-item-editor`를 배치한다. 트리 컴포넌트가 자체적으로 정전개/역전개를 지원한다. + +### Q: 버튼 클릭 시 특정 로직을 실행하고 싶은데? + +**A**: `v2-button-primary`의 `webTypeConfig.dataflowConfig`에 제어관리를 연결한다. `dataflow_diagrams` 테이블에 실행할 액션(INSERT/UPDATE/DELETE)과 조건을 정의한다. + +### Q: 검색 기능은 어떻게 추가하나? + +**A**: `v2-table-search-widget`을 `v2-table-list` 위에 배치한다. `autoSelectFirstTable: true`로 설정하면 자동으로 화면의 첫 번째 테이블과 연동된다. + +### Q: 파일 업로드는? + +**A**: `v2-file-upload` 컴포넌트를 사용한다. 폼 내 배치하여 파일/이미지 업로드를 처리한다. + +### Q: 결재 프로세스를 표시하고 싶은데? + +**A**: `v2-approval-step` 컴포넌트를 사용한다. 결재 단계를 스테퍼 형태로 시각화한다. + +### Q: 인라인 편집이 가능한가? + +**A**: `v2-table-list`의 columns에서 `editable: true`로 설정하면 해당 컬럼의 셀을 직접 편집할 수 있다. 단, 복잡한 편집은 모달 편집(`v2-split-panel-layout`의 editButton.mode = "modal")을 권장한다. + +### Q: 화면에 집계(합계, 평균 등)를 표시하고 싶은데? + +**A**: `v2-aggregation-widget`을 사용하여 테이블 데이터의 합계, 평균, 개수 등을 카드 형태로 표시한다. 또는 `v2-status-count`로 상태별 건수를 표시한다. --- -## 8. 빠른 개발 가이드 +## 6. 컴포넌트 선택 빠른 참조표 -### Step 1: 화면 분석 -1. 어떤 테이블을 사용하는가? -2. 테이블 간 관계는? -3. 어떤 패턴인가? (A/B/C/D/E) - -### Step 2: 컴포넌트 배치 -1. 화면 디자이너에서 패턴에 맞는 컴포넌트 배치 -2. 각 컴포넌트에 테이블/컬럼 설정 - -### Step 3: 연동 설정 -1. 마스터-디테일 관계 설정 (FK) -2. 검색 조건 설정 -3. 버튼 액션 설정 - -### Step 4: 테스트 -1. 데이터 조회 확인 -2. 마스터 선택 시 디테일 연동 확인 -3. 저장/삭제 동작 확인 - ---- - -## 9. 요약 - -### V2 컴포넌트 커버리지 - -| 화면 유형 | 지원 여부 | 주요 컴포넌트 | -|-----------|----------|--------------| -| 기본 CRUD | ✅ 완전 | v2-table-list, v2-table-search-widget | -| 마스터-디테일 | ✅ 완전 | v2-split-panel-layout | -| 탭 화면 | ✅ 완전 | v2-tabs-widget | -| 카드 뷰 | ✅ 완전 | v2-card-display | -| 피벗 분석 | ✅ 완전 | v2-pivot-grid | -| 그룹화 테이블 | ✅ 지원 | v2-table-grouped | -| 타임라인/스케줄 | ✅ 지원 | v2-timeline-scheduler | -| 파일 업로드 | ✅ 지원 | v2-file-upload | -| 트리 뷰 | ❌ 미지원 | 개발 필요 | - -### 개발 시 핵심 원칙 - -1. **테이블 먼저**: DB 테이블 구조 확인이 최우선 -2. **패턴 판단**: 5가지 패턴 중 어디에 해당하는지 판단 -3. **표준 조합**: 검증된 컴포넌트 조합 사용 -4. **한계 인식**: 불가능한 UI는 조기에 식별하여 별도 개발 계획 -5. **멀티테넌시**: 모든 테이블에 company_code 필터링 필수 -6. **제어관리 필수**: UI 완성 후 버튼별 비즈니스 로직 설정 필수 - -### UI vs 제어 구분 - -| 영역 | 담당 | 설정 위치 | -|------|------|----------| -| 화면 레이아웃 | V2 컴포넌트 | 화면 디자이너 | -| 비즈니스 로직 | 제어관리 | dataflow_diagrams | -| 외부 연동 | 외부호출 설정 | external_call_configs | - -**HTML에서 배낄 수 있는 것**: UI 구조만 -**별도 설정 필요한 것**: 저장 테이블, 버튼 액션, 조건 처리, 다중 행 처리 +| 요구사항 | 컴포넌트 | +|----------|----------| +| 데이터 목록 보기 | `v2-table-list` | +| 데이터 검색/필터 | `v2-table-search-widget` | +| 좌우 분할 (마스터-디테일) | `v2-split-panel-layout` | +| 그룹별 묶기 (접기/펼치기) | `v2-table-grouped` | +| 탭 전환 | `v2-tabs-widget` 또는 `v2-split-panel-layout`의 additionalTabs | +| 카드 형태 보기 | `v2-card-display` | +| 피벗/집계 분석 | `v2-pivot-grid` | +| 간트차트/일정 | `v2-timeline-scheduler` | +| BOM 트리 | `v2-bom-tree` + `v2-bom-item-editor` | +| 텍스트 입력 | `v2-input` | +| 선택 (드롭다운 등) | `v2-select` | +| 날짜 입력 | `v2-date` | +| 파일 업로드 | `v2-file-upload` | +| 액션 버튼 | `v2-button-primary` | +| 자동 채번 | `v2-numbering-rule` | +| 카테고리 관리 | `v2-category-manager` | +| 결재 단계 표시 | `v2-approval-step` | +| 상태별 건수 | `v2-status-count` | +| 합계/평균 카드 | `v2-aggregation-widget` | +| 구분선 | `v2-divider-line` | +| 그룹 컨테이너 | `v2-section-card` 또는 `v2-section-paper` | +| 데이터 반복 | `v2-repeat-container` 또는 `v2-repeater` | +| 공정 작업기준 | `v2-process-work-standard` | +| 품목 라우팅 | `v2-item-routing` | +| 미디어 표시 | `v2-media` | +| 랙 구조 | `v2-rack-structure` | +| 위치 교환 | `v2-location-swap-selector` | diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 2d7c3246..c8204faf 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -18,6 +18,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } import { Input } from "@/components/ui/input"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; +import { formatNumber as centralFormatNumber } from "@/lib/formatting"; import { cn } from "@/lib/utils"; import { V2InputProps, V2InputConfig, V2InputFormat } from "@/types/v2-components"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; @@ -61,11 +62,11 @@ export function validateInputFormat(value: string, format: V2InputFormat): { isV return { isValid, errorMessage: isValid ? "" : formatConfig.errorMessage }; } -// 통화 형식 변환 +// 통화 형식 변환 (공통 formatNumber 사용) function formatCurrency(value: string | number): string { const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; if (isNaN(num)) return ""; - return num.toLocaleString("ko-KR"); + return centralFormatNumber(num); } // 사업자번호 형식 변환 @@ -234,7 +235,22 @@ const TextInput = forwardRef< TextInput.displayName = "TextInput"; /** - * 숫자 입력 컴포넌트 + * 숫자를 콤마 포맷 문자열로 변환 (입력 중 실시간 표시용) + * 소수점 입력 중인 경우(끝이 "."이거나 ".0" 등)를 보존 + */ +function toCommaDisplay(raw: string): string { + if (raw === "" || raw === "-") return raw; + const negative = raw.startsWith("-"); + const abs = negative ? raw.slice(1) : raw; + const dotIdx = abs.indexOf("."); + const intPart = dotIdx >= 0 ? abs.slice(0, dotIdx) : abs; + const decPart = dotIdx >= 0 ? abs.slice(dotIdx) : ""; + const formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return (negative ? "-" : "") + formatted + decPart; +} + +/** + * 숫자 입력 컴포넌트 - 입력 중에도 실시간 천단위 콤마 표시 */ const NumberInput = forwardRef< HTMLInputElement, @@ -250,40 +266,112 @@ const NumberInput = forwardRef< className?: string; inputStyle?: React.CSSProperties; } ->(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className, inputStyle }, ref) => { +>(({ value, onChange, min, max, placeholder, readonly, disabled, className, inputStyle }, ref) => { + const innerRef = useRef(null); + const combinedRef = (node: HTMLInputElement | null) => { + (innerRef as React.MutableRefObject).current = node; + if (typeof ref === "function") ref(node); + else if (ref) (ref as React.MutableRefObject).current = node; + }; + + // 콤마 포함된 표시 문자열을 내부 상태로 관리 + const [displayValue, setDisplayValue] = useState(() => { + if (value === undefined || value === null) return ""; + return centralFormatNumber(value); + }); + + // 외부 value가 변경되면 표시 값 동기화 (포커스 아닐 때만) + const isFocusedRef = useRef(false); + useEffect(() => { + if (isFocusedRef.current) return; + if (value === undefined || value === null) { + setDisplayValue(""); + } else { + setDisplayValue(centralFormatNumber(value)); + } + }, [value]); + const handleChange = useCallback( (e: React.ChangeEvent) => { - const val = e.target.value; - if (val === "") { + const input = e.target; + const cursorPos = input.selectionStart ?? 0; + const oldVal = displayValue; + const rawInput = e.target.value; + + // 콤마 제거하여 순수 숫자 문자열 추출 + const stripped = rawInput.replace(/,/g, ""); + + // 빈 값 처리 + if (stripped === "" || stripped === "-") { + setDisplayValue(stripped); onChange?.(undefined); return; } - let num = parseFloat(val); + // 숫자 + 소수점만 허용 (입력 중 "123." 같은 중간 상태도 허용) + if (!/^-?\d*\.?\d*$/.test(stripped)) return; + + // 새 콤마 포맷 생성 + const newDisplay = toCommaDisplay(stripped); + setDisplayValue(newDisplay); + + // 콤마 개수 차이로 커서 위치 보정 + const oldCommas = (oldVal.slice(0, cursorPos).match(/,/g) || []).length; + const newCommas = (newDisplay.slice(0, cursorPos).match(/,/g) || []).length; + const adjustedCursor = cursorPos + (newCommas - oldCommas); + + requestAnimationFrame(() => { + if (innerRef.current) { + innerRef.current.setSelectionRange(adjustedCursor, adjustedCursor); + } + }); + + // 실제 숫자 값 전달 (소수점 입력 중이면 아직 전달하지 않음) + if (stripped.endsWith(".") || stripped.endsWith("-")) return; + let num = parseFloat(stripped); + if (isNaN(num)) return; - // 범위 제한 if (min !== undefined && num < min) num = min; if (max !== undefined && num > max) num = max; onChange?.(num); }, - [min, max, onChange], + [min, max, onChange, displayValue], ); + const handleFocus = useCallback(() => { + isFocusedRef.current = true; + }, []); + + const handleBlur = useCallback(() => { + isFocusedRef.current = false; + // 블러 시 최종 포맷 정리 + const stripped = displayValue.replace(/,/g, ""); + if (stripped === "" || stripped === "-" || stripped === ".") { + setDisplayValue(""); + onChange?.(undefined); + return; + } + const num = parseFloat(stripped); + if (!isNaN(num)) { + setDisplayValue(centralFormatNumber(num)); + } + }, [displayValue, onChange]); + return ( ); }); diff --git a/frontend/lib/formatting/index.ts b/frontend/lib/formatting/index.ts index 53dff08d..573b3af1 100644 --- a/frontend/lib/formatting/index.ts +++ b/frontend/lib/formatting/index.ts @@ -63,10 +63,23 @@ function applyDateFormat(date: Date, pattern: string): string { // --- 숫자 포맷 --- +/** 최대 허용 소수점 자릿수 */ +const MAX_DECIMAL_PLACES = 5; + /** - * 숫자를 로케일 기반으로 포맷한다 (천단위 구분자 등). + * 실제 값에서 소수점 자릿수를 감지한다 (최대 MAX_DECIMAL_PLACES). + */ +function detectDecimals(num: number): number { + if (Number.isInteger(num)) return 0; + const parts = String(num).split("."); + if (parts.length < 2) return 0; + return Math.min(parts[1].length, MAX_DECIMAL_PLACES); +} + +/** + * 숫자를 로케일 기반으로 포맷한다 (천단위 구분자 + 소수점 자동감지). * @param value - 숫자 또는 숫자 문자열 - * @param decimals - 소수점 자릿수 (미지정 시 기본값 사용) + * @param decimals - 소수점 자릿수 (미지정 시 실제 값에서 자동감지, 최대 5자리) * @returns 포맷된 문자열 */ export function formatNumber(value: unknown, decimals?: number): string { @@ -76,7 +89,7 @@ export function formatNumber(value: unknown, decimals?: number): string { const num = typeof value === "number" ? value : parseFloat(String(value)); if (isNaN(num)) return String(value); - const dec = decimals ?? rules.number.decimals; + const dec = decimals ?? detectDecimals(num); return new Intl.NumberFormat(rules.number.locale, { minimumFractionDigits: dec, diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 782c3ea9..85db9002 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -9,6 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { RepeaterColumnConfig } from "./types"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; +import { formatNumber as centralFormatNumber } from "@/lib/formatting"; // @dnd-kit imports import { @@ -594,21 +595,10 @@ export function RepeaterTable({ // 계산 필드는 편집 불가 if (column.calculated || !column.editable) { - // 숫자 포맷팅 함수: 정수/소수점 자동 구분 - const formatNumber = (val: any): string => { - if (val === undefined || val === null || val === "") return "0"; - const num = typeof val === "number" ? val : parseFloat(val); - if (isNaN(num)) return "0"; - // 정수면 소수점 없이, 소수면 소수점 유지 - if (Number.isInteger(num)) { - return num.toLocaleString("ko-KR"); - } else { - return num.toLocaleString("ko-KR"); - } - }; - // 🆕 카테고리 타입이면 라벨로 변환하여 표시 - const displayValue = column.type === "number" ? formatNumber(value) : getCategoryDisplayValue(value); + const displayValue = column.type === "number" + ? (value === undefined || value === null || value === "" ? "0" : centralFormatNumber(value) || "0") + : getCategoryDisplayValue(value); // 🆕 40자 초과 시 ... 처리 및 툴팁 const { truncated, isTruncated } = truncateText(String(displayValue)); @@ -623,24 +613,28 @@ export function RepeaterTable({ // 편집 가능한 필드 switch (column.type) { case "number": - // 숫자 표시: 정수/소수점 자동 구분 + // 콤마 포함 숫자 표시 const displayValue = (() => { if (value === undefined || value === null || value === "") return ""; - const num = typeof value === "number" ? value : parseFloat(value); + const num = typeof value === "number" ? value : parseFloat(String(value)); if (isNaN(num)) return ""; - return num.toString(); + return centralFormatNumber(num); })(); return ( { - const val = e.target.value; - // 숫자와 소수점만 허용 - if (val === "" || /^-?\d*\.?\d*$/.test(val)) { - handleCellEdit(rowIndex, column.field, val === "" ? 0 : parseFloat(val) || 0); + const stripped = e.target.value.replace(/,/g, ""); + if (stripped === "" || stripped === "-") { + handleCellEdit(rowIndex, column.field, 0); + return; + } + if (!/^-?\d*\.?\d*$/.test(stripped)) return; + if (!stripped.endsWith(".")) { + handleCellEdit(rowIndex, column.field, parseFloat(stripped) || 0); } }} className="border-border focus:border-primary focus:ring-ring h-8 w-full min-w-0 rounded-none text-right text-xs focus:ring-1" diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 6f4aa411..dba11885 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatNumber as centralFormatNumber } from "@/lib/formatting"; import { X, Check, @@ -1286,7 +1287,7 @@ export const SelectedItemsDetailInputComponent: React.FC { const decimals = field.decimals ?? 0; - const formatted = value.toFixed(decimals); switch (field.format) { case "currency": - return Number(formatted).toLocaleString() + "원"; + return centralFormatNumber(value, decimals) + "원"; case "percent": - return formatted + "%"; + return value.toFixed(decimals) + "%"; default: - return Number(formatted).toLocaleString(); + return centralFormatNumber(value, decimals); } }; @@ -554,9 +554,9 @@ export function SimpleRepeaterTableComponent({ return (
{column.type === "number" - ? typeof cellValue === "number" - ? cellValue.toLocaleString() - : cellValue || "0" + ? (cellValue !== null && cellValue !== undefined && cellValue !== "" + ? centralFormatNumber(cellValue) + : "0") : cellValue || "-"}
); @@ -565,12 +565,28 @@ export function SimpleRepeaterTableComponent({ // 편집 가능한 필드 switch (column.type) { case "number": + const numDisplay = (() => { + if (cellValue === undefined || cellValue === null || cellValue === "") return ""; + const n = typeof cellValue === "number" ? cellValue : parseFloat(String(cellValue)); + return isNaN(n) ? "" : centralFormatNumber(n); + })(); return ( handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)} - className="h-7 text-xs" + type="text" + inputMode="decimal" + value={numDisplay} + onChange={(e) => { + const stripped = e.target.value.replace(/,/g, ""); + if (stripped === "" || stripped === "-") { + handleCellEdit(rowIndex, column.field, 0); + return; + } + if (!/^-?\d*\.?\d*$/.test(stripped)) return; + if (!stripped.endsWith(".")) { + handleCellEdit(rowIndex, column.field, parseFloat(stripped) || 0); + } + }} + className="h-7 text-right text-xs" /> ); diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 9ed4c81f..0f54b4f2 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -24,6 +24,7 @@ import { } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; +import { formatNumber as centralFormatNumber } from "@/lib/formatting"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; import { apiClient, getFullImageUrl } from "@/lib/api/client"; @@ -1006,19 +1007,20 @@ export const SplitPanelLayoutComponent: React.FC .replace("ss", String(date.getSeconds()).padStart(2, "0")); }, []); - // 숫자 포맷팅 헬퍼 함수 + // 숫자 포맷팅 헬퍼 함수 (공통 formatNumber 기반) const formatNumberValue = useCallback((value: any, format: any): string => { if (value === null || value === undefined || value === "") return "-"; const num = typeof value === "number" ? value : parseFloat(String(value)); if (isNaN(num)) return String(value); - const options: Intl.NumberFormatOptions = { - minimumFractionDigits: format?.decimalPlaces ?? 0, - maximumFractionDigits: format?.decimalPlaces ?? 10, - useGrouping: format?.thousandSeparator ?? false, - }; + let result: string; + if (format?.thousandSeparator === false) { + const dec = format?.decimalPlaces ?? 0; + result = num.toFixed(dec); + } else { + result = centralFormatNumber(num, format?.decimalPlaces); + } - let result = num.toLocaleString("ko-KR", options); if (format?.prefix) result = format.prefix + result; if (format?.suffix) result = result + format.suffix; return result; @@ -1088,14 +1090,16 @@ export const SplitPanelLayoutComponent: React.FC return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD"); } - // 🆕 숫자 포맷 적용 + // 숫자 포맷 적용 (format 설정이 있거나 input_type이 number/decimal이면 자동 적용) + const isNumericByInputType = colInputType === "number" || colInputType === "decimal"; if ( format?.type === "number" || format?.type === "currency" || format?.thousandSeparator || - format?.decimalPlaces !== undefined + format?.decimalPlaces !== undefined || + isNumericByInputType ) { - return formatNumberValue(value, format); + return formatNumberValue(value, format || { thousandSeparator: true }); } // 카테고리 매핑 찾기 (여러 키 형태 시도) diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 6b8c674c..65833fa9 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button"; import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core"; import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor"; import { useTabId } from "@/contexts/TabIdContext"; +import { formatNumber as centralFormatNumber, formatCurrency as centralFormatCurrency } from "@/lib/formatting"; // 🖼️ 테이블 셀 이미지 썸네일 컴포넌트 // objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용 @@ -4445,17 +4446,14 @@ export const TableListComponent: React.FC = ({ return "-"; } - // 숫자 타입 포맷팅 (천단위 구분자 설정 확인) + // 숫자 타입 포맷팅 (공통 formatNumber 사용) if (inputType === "number" || inputType === "decimal") { if (value !== null && value !== undefined && value !== "") { - const numValue = typeof value === "string" ? parseFloat(value) : value; - if (!isNaN(numValue)) { - // thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용 - if (column.thousandSeparator !== false) { - return numValue.toLocaleString("ko-KR"); - } - return String(numValue); + if (column.thousandSeparator !== false) { + return centralFormatNumber(value); } + const numValue = typeof value === "string" ? parseFloat(value) : value; + return isNaN(numValue) ? String(value) : String(numValue); } return String(value); } @@ -4463,14 +4461,11 @@ export const TableListComponent: React.FC = ({ switch (column.format) { case "number": if (value !== null && value !== undefined && value !== "") { - const numValue = typeof value === "string" ? parseFloat(value) : value; - if (!isNaN(numValue)) { - // thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용 - if (column.thousandSeparator !== false) { - return numValue.toLocaleString("ko-KR"); - } - return String(numValue); + if (column.thousandSeparator !== false) { + return centralFormatNumber(value); } + const numValue = typeof value === "string" ? parseFloat(value) : value; + return isNaN(numValue) ? String(value) : String(numValue); } return String(value); case "date": @@ -4487,12 +4482,8 @@ export const TableListComponent: React.FC = ({ } return "-"; case "currency": - if (typeof value === "number") { - // thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용 - if (column.thousandSeparator !== false) { - return `₩${value.toLocaleString()}`; - } - return `₩${value}`; + if (value !== null && value !== undefined && value !== "") { + return centralFormatCurrency(value); } return value; case "boolean":