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":