jskim-node #415
|
|
@ -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