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
This commit is contained in:
parent
429f1ba6ee
commit
7a65ab0f85
|
|
@ -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 | 회사 코드 |
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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<HTMLInputElement>(null);
|
||||
const combinedRef = (node: HTMLInputElement | null) => {
|
||||
(innerRef as React.MutableRefObject<HTMLInputElement | null>).current = node;
|
||||
if (typeof ref === "function") ref(node);
|
||||
else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Input
|
||||
ref={ref}
|
||||
type="number"
|
||||
value={value ?? ""}
|
||||
ref={combinedRef}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder || "숫자 입력"}
|
||||
readOnly={readonly}
|
||||
disabled={disabled}
|
||||
className={cn("h-full w-full", className)}
|
||||
style={inputStyle}
|
||||
style={{ ...inputStyle, textAlign: "right" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
inputMode="decimal"
|
||||
value={displayValue}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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<SelectedItemsDetailInpu
|
|||
// 숫자 포맷팅 헬퍼: 콤마 표시 + 실제 값은 숫자만 저장
|
||||
const rawNum = value ? String(value).replace(/,/g, "") : "";
|
||||
const displayNum = rawNum && !isNaN(Number(rawNum))
|
||||
? new Intl.NumberFormat("ko-KR").format(Number(rawNum))
|
||||
? centralFormatNumber(Number(rawNum))
|
||||
: rawNum;
|
||||
|
||||
// 계산된 단가는 읽기 전용 + 강조 표시
|
||||
|
|
@ -1511,9 +1512,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
if (matched) return matched.label;
|
||||
}
|
||||
|
||||
// 숫자는 천 단위 구분
|
||||
// 숫자는 천 단위 구분 (공통 formatNumber 사용)
|
||||
if (renderType === "number" && !isNaN(Number(strValue))) {
|
||||
return new Intl.NumberFormat("ko-KR").format(Number(strValue));
|
||||
return centralFormatNumber(Number(strValue));
|
||||
}
|
||||
|
||||
return strValue;
|
||||
|
|
@ -1646,11 +1647,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
switch (displayItem.format) {
|
||||
case "currency":
|
||||
// 천 단위 구분
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
||||
formattedValue = centralFormatNumber(Number(fieldValue) || 0);
|
||||
break;
|
||||
case "number":
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
||||
formattedValue = centralFormatNumber(Number(fieldValue) || 0);
|
||||
break;
|
||||
case "date":
|
||||
// YYYY.MM.DD 형식
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ComponentRendererProps } from "@/types/component";
|
|||
import { useCalculation } from "./useCalculation";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||
|
||||
export interface SimpleRepeaterTableComponentProps extends ComponentRendererProps {
|
||||
config?: SimpleRepeaterTableProps;
|
||||
|
|
@ -519,18 +520,17 @@ export function SimpleRepeaterTableComponent({
|
|||
return result;
|
||||
}, [value, summaryConfig]);
|
||||
|
||||
// 합계 값 포맷팅
|
||||
// 합계 값 포맷팅 (공통 formatNumber 사용)
|
||||
const formatSummaryValue = (field: SummaryFieldConfig, value: number): string => {
|
||||
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 (
|
||||
<div className="px-2 py-1">
|
||||
{column.type === "number"
|
||||
? typeof cellValue === "number"
|
||||
? cellValue.toLocaleString()
|
||||
: cellValue || "0"
|
||||
? (cellValue !== null && cellValue !== undefined && cellValue !== ""
|
||||
? centralFormatNumber(cellValue)
|
||||
: "0")
|
||||
: cellValue || "-"}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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 (
|
||||
<Input
|
||||
type="number"
|
||||
value={cellValue || ""}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SplitPanelLayoutComponentProps>
|
|||
.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<SplitPanelLayoutComponentProps>
|
|||
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 });
|
||||
}
|
||||
|
||||
// 카테고리 매핑 찾기 (여러 키 형태 시도)
|
||||
|
|
|
|||
|
|
@ -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<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
}
|
||||
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":
|
||||
|
|
|
|||
Loading…
Reference in New Issue