jskim-node #415
|
|
@ -1,331 +1,485 @@
|
||||||
# 화면 전체 분석 보고서
|
# WACE ERP 화면 구성 시스템 전체 분석
|
||||||
|
|
||||||
> **분석 대상**: `/Users/kimjuseok/Downloads/화면개발 8` 폴더 내 핵심 업무 화면
|
> **최종 업데이트**: 2026-03-13
|
||||||
> **분석 기준**: 메뉴별 분류, 3개 이상 재활용 가능한 컴포넌트 식별
|
> **용도**: LLM 챗봇 / AI 에이전트가 화면 개발 요청을 받았을 때 참조하는 시스템 구조 레퍼런스
|
||||||
> **분석 일자**: 2026-01-30
|
> **핵심 규칙**: 사용자 업무 화면은 React 코드(.tsx)로 직접 만들지 않는다. DB 등록(screen_definitions + screen_layouts_v2 + menu_info)으로 구현한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 현재 사용 중인 V2 컴포넌트 목록
|
## 1. 시스템 아키텍처 요약
|
||||||
|
|
||||||
> **중요**: v2- 접두사가 붙은 컴포넌트만 사용합니다.
|
### 1.1 화면 렌더링 파이프라인
|
||||||
|
|
||||||
### 입력 컴포넌트
|
```
|
||||||
| ID | 이름 | 용도 |
|
[DB] screen_definitions (화면 정의)
|
||||||
|----|------|------|
|
+ screen_layouts_v2 (레이아웃 JSON)
|
||||||
| `v2-input` | V2 입력 | 텍스트, 숫자, 비밀번호, 이메일 등 입력 |
|
+ menu_info (메뉴 등록)
|
||||||
| `v2-select` | V2 선택 | 드롭다운, 콤보박스, 라디오, 체크박스 |
|
│
|
||||||
| `v2-date` | V2 날짜 | 날짜, 시간, 날짜범위 입력 |
|
▼
|
||||||
|
[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 컴포넌트 렌더링
|
||||||
|
```
|
||||||
|
|
||||||
### 표시 컴포넌트
|
### 1.2 화면 유형 분류
|
||||||
| ID | 이름 | 용도 |
|
|
||||||
|----|------|------|
|
|
||||||
| `v2-text-display` | 텍스트 표시 | 라벨, 텍스트 표시 |
|
|
||||||
| `v2-card-display` | 카드 디스플레이 | 테이블 데이터를 카드 형태로 표시 |
|
|
||||||
| `v2-aggregation-widget` | 집계 위젯 | 합계, 평균, 개수 등 집계 표시 |
|
|
||||||
|
|
||||||
### 테이블/데이터 컴포넌트
|
| 구분 | 구현 방식 | 코드 위치 | 예시 |
|
||||||
| ID | 이름 | 용도 |
|
|------|----------|----------|------|
|
||||||
|----|------|------|
|
| **사용자 업무 화면** | DB 등록 (SQL INSERT) | screen_definitions + screen_layouts_v2 | 수주관리, 품목정보, BOM관리 |
|
||||||
| `v2-table-list` | 테이블 리스트 | 데이터 테이블 표시, 페이지네이션, 정렬, 필터 |
|
| **관리자 메뉴** | React 코드 직접 작성 | frontend/app/(main)/admin/*/page.tsx | 사용자관리, 권한관리, 시스템설정 |
|
||||||
| `v2-table-search-widget` | 검색 필터 | 화면 내 테이블 검색/필터/그룹 기능 |
|
|
||||||
| `v2-pivot-grid` | 피벗 그리드 | 다차원 데이터 분석 (피벗 테이블) |
|
|
||||||
|
|
||||||
### 레이아웃 컴포넌트
|
**절대 규칙**: 사용자 업무 화면(생산, 영업, 구매, 물류, 품질 등)은 React 하드코딩 금지. 반드시 DB 등록 방식으로 구현.
|
||||||
| 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 컴포넌트**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 화면 분류 (메뉴별)
|
## 2. V2 컴포넌트 전체 목록 (32개)
|
||||||
|
|
||||||
### 01. 기준정보 (master-data)
|
> 모든 컴포넌트는 `v2-` 접두사를 사용한다. 접두사 없는 컴포넌트는 레거시이므로 사용 금지.
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|
||||||
|--------|--------|------|----------|
|
|
||||||
| 회사정보 | 회사정보.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 부서정보 | 부서정보.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 품목정보 | 품목정보.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 |
|
|
||||||
| BOM관리 | BOM관리.html | 분할패널+트리 | ⚠️ 트리뷰 미지원 |
|
|
||||||
| 공정정보관리 | 공정정보관리.html | 분할패널+테이블 | ✅ 완전 |
|
|
||||||
| 공정작업기준 | 공정작업기준관리.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 품목라우팅 | 품목라우팅관리.html | 분할패널+테이블 | ✅ 완전 |
|
|
||||||
|
|
||||||
### 02. 영업관리 (sales)
|
### 2.1 입력 컴포넌트 (9개)
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|
||||||
|--------|--------|------|----------|
|
|
||||||
| 수주관리 | 수주관리.html | 분할패널+테이블 | ✅ 완전 |
|
|
||||||
| 견적관리 | 견적관리.html | 분할패널+테이블 | ✅ 완전 |
|
|
||||||
| 거래처관리 | 거래처관리.html | 분할패널+탭+그룹화 | ⚠️ 그룹화 미지원 |
|
|
||||||
| 판매품목정보 | 판매품목정보.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 출하계획관리 | 출하계획관리.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
|
|
||||||
### 03. 생산관리 (production)
|
| ID | 용도 | 핵심 설정 |
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|----|------|----------|
|
||||||
|--------|--------|------|----------|
|
| `v2-input` | 텍스트, 숫자, 비밀번호, textarea, 슬라이더, 컬러, 버튼 | inputType, format(email/tel/url/currency/biz_no), required, readonly, maxLength |
|
||||||
| 생산계획관리 | 생산계획관리.html | 분할패널+탭+타임라인 | ❌ 타임라인 미지원 |
|
| `v2-select` | 드롭다운, 콤보박스, 라디오, 체크박스, 태그, 토글, 스왑 | mode, source(static/code/db/api/entity/category/distinct/select), searchable, multiple, cascading |
|
||||||
| 생산관리 | 생산관리.html | 검색+테이블 | ✅ 완전 |
|
| `v2-date` | 날짜, 시간, 날짜시간, 날짜범위, 월, 연도 | dateType(date/time/datetime), format, range, minDate, maxDate |
|
||||||
| 생산실적관리 | 생산실적관리.html | 검색+테이블 | ✅ 완전 |
|
| `v2-file-upload` | 파일/이미지 업로드, 다중 업로드 | accept, maxSize, multiple |
|
||||||
| 작업지시 | 작업지시.html | 탭+그룹화테이블+분할패널 | ⚠️ 그룹화 미지원 |
|
| `v2-media` | 이미지, 비디오, 오디오 표시 | mediaType |
|
||||||
| 공정관리 | 공정관리.html | 분할패널+테이블 | ✅ 완전 |
|
| `v2-location-swap-selector` | 출발지/도착지 선택 및 교환 | - |
|
||||||
|
| `v2-rack-structure` | 창고 랙 위치 일괄 생성 | columns, rows |
|
||||||
|
| `v2-process-work-standard` | 품목별 공정 작업기준 관리 (Pre/In/Post-Work) | - |
|
||||||
|
| `v2-item-routing` | 품목별 라우팅 버전 및 공정 순서 관리 (3단계 계층) | - |
|
||||||
|
|
||||||
### 04. 구매관리 (purchase)
|
### 2.2 표시/데이터 컴포넌트 (10개)
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|
||||||
|--------|--------|------|----------|
|
|
||||||
| 발주관리 | 발주관리.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 공급업체관리 | 공급업체관리.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 구매입고 | pages/구매입고.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
|
|
||||||
### 05. 설비관리 (equipment)
|
| ID | 용도 | 핵심 설정 |
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|----|------|----------|
|
||||||
|--------|--------|------|----------|
|
| `v2-table-list` | 데이터 테이블 (조회/편집, 페이지네이션, 정렬, 필터) | selectedTable, columns, pagination, displayMode(table/card), checkbox, linkedFilters |
|
||||||
| 설비정보 | 설비정보.html | 분할패널+카드+탭 | ✅ v2-card-display 활용 |
|
| `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)
|
### 2.3 레이아웃 컴포넌트 (8개)
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|
||||||
|--------|--------|------|----------|
|
|
||||||
| 창고관리 | 창고관리.html | 모바일앱스타일+iframe | ❌ 별도개발 필요 |
|
|
||||||
| 창고정보관리 | 창고정보관리.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
| 입출고관리 | 입출고관리.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 |
|
|
||||||
| 재고현황 | 재고현황.html | 검색+테이블 | ✅ 완전 |
|
|
||||||
|
|
||||||
### 07. 품질관리 (quality)
|
| ID | 용도 | 핵심 설정 |
|
||||||
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|
|----|------|----------|
|
||||||
|--------|--------|------|----------|
|
| `v2-split-panel-layout` | 마스터-디테일 좌우 분할 | splitRatio, leftPanel(displayMode/tableName), rightPanel(relation/foreignKey), additionalTabs |
|
||||||
| 검사기준 | 검사기준.html | 검색+테이블 | ✅ 완전 |
|
| `v2-tabs-widget` | 탭 전환, 탭 내 컴포넌트 배치 | tabs(id/label/components), defaultTab, orientation |
|
||||||
| 검사정보관리 | 검사정보관리.html | 탭+테이블 | ✅ 완전 |
|
| `v2-section-card` | 제목+테두리가 있는 그룹화 컨테이너 | title, collapsible |
|
||||||
| 검사장비관리 | 검사장비관리.html | 검색+테이블 | ✅ 완전 |
|
| `v2-section-paper` | 배경색 기반 미니멀 그룹화 컨테이너 | backgroundColor, padding |
|
||||||
| 불량관리 | 불량관리.html | 검색+테이블 | ✅ 완전 |
|
| `v2-divider-line` | 영역 구분선 | orientation, thickness |
|
||||||
| 클레임관리 | 클레임관리.html | 검색+테이블 | ✅ 완전 |
|
| `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: 검색 + 테이블 (가장 기본)
|
> AI가 화면 개발 요청을 받았을 때, 아래 패턴 중 해당하는 것을 선택하여 구현한다.
|
||||||
**해당 화면**: 약 60% (15개 이상)
|
|
||||||
|
|
||||||
**사용 컴포넌트**:
|
### 패턴 A: 기본 마스터 (검색 + 테이블)
|
||||||
- `v2-table-search-widget`: 검색 필터
|
|
||||||
- `v2-table-list`: 데이터 테이블
|
**적용 비율**: 약 50% (가장 흔함)
|
||||||
|
**적용 화면**: 코드관리, 부서정보, 창고정보, 검사기준, 불량관리, 공급업체관리 등
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────┐
|
||||||
│ [검색필드들...] [조회] [엑셀] │ ← v2-table-search-widget
|
│ v2-table-search-widget │
|
||||||
├─────────────────────────────────────────┤
|
│ [검색필드1] [검색필드2] [조회] [엑셀] │
|
||||||
│ 테이블 제목 [신규등록] [삭제] │
|
├─────────────────────────────────────────────────┤
|
||||||
│ ────────────────────────────────────── │
|
│ v2-table-list │
|
||||||
│ □ | 코드 | 이름 | 상태 | 등록일 | │ ← v2-table-list
|
│ 제목 [신규] [삭제] │
|
||||||
│ □ | A001 | 테스트| 사용 | 2026-01-30 | │
|
│ ─────────────────────────────────────────────── │
|
||||||
└─────────────────────────────────────────┘
|
│ □ | 코드 | 이름 | 상태 | 등록일 │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 패턴 B: 분할 패널 (마스터-디테일)
|
**필수 컴포넌트**: `v2-table-search-widget` (1개) + `v2-table-list` (1개)
|
||||||
**해당 화면**: 약 25% (8개)
|
|
||||||
|
|
||||||
**사용 컴포넌트**:
|
### 패턴 B: 마스터-디테일 (좌우 분할)
|
||||||
- `v2-split-panel-layout`: 좌우 분할
|
|
||||||
- `v2-table-list`: 마스터/디테일 테이블
|
**적용 비율**: 약 25%
|
||||||
- `v2-tabs-widget`: 상세 탭 (선택)
|
**적용 화면**: 공정관리, 수주관리, 견적관리, 품목라우팅 등
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────┬──────────────────────┐
|
┌──────────────────┬──────────────────────────────┐
|
||||||
│ 마스터 리스트 │ 상세 정보 / 탭 │
|
│ 마스터 테이블 │ 디테일 테이블/폼 │
|
||||||
│ ─────────────── │ ┌────┬────┬────┐ │
|
│ (좌측 패널) │ (우측 패널) │
|
||||||
│ □ A001 제품A │ │기본│이력│첨부│ │
|
│ □ A001 항목1 │ [상세 정보 테이블] │
|
||||||
│ □ A002 제품B ← │ └────┴────┴────┘ │
|
│ □ A002 항목2 ← │ │
|
||||||
│ □ A003 제품C │ [테이블 or 폼] │
|
└──────────────────┴──────────────────────────────┘
|
||||||
└──────────────────┴──────────────────────┘
|
v2-split-panel-layout
|
||||||
```
|
```
|
||||||
|
|
||||||
### 패턴 C: 탭 + 테이블
|
**필수 컴포넌트**: `v2-split-panel-layout` (1개)
|
||||||
**해당 화면**: 약 10% (3개)
|
|
||||||
|
|
||||||
**사용 컴포넌트**:
|
### 패턴 C: 마스터-디테일 + 탭
|
||||||
- `v2-tabs-widget`: 탭 전환
|
|
||||||
- `v2-table-list`: 탭별 테이블
|
**적용 비율**: 약 10%
|
||||||
|
**적용 화면**: 거래처관리, 설비정보, 품목정보 등
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌──────────────────┬──────────────────────────────┐
|
||||||
│ [탭1] [탭2] [탭3] │
|
│ 마스터 테이블 │ [기본] [이력] [첨부] │
|
||||||
├─────────────────────────────────────────┤
|
│ │ ┌────────────────────────┐ │
|
||||||
│ [테이블 영역] │
|
│ □ A001 거래처1 │ │ 탭별 컨텐츠 │ │
|
||||||
└─────────────────────────────────────────┘
|
│ □ A002 거래처2 ← │ └────────────────────────┘ │
|
||||||
|
└──────────────────┴──────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 패턴 D: 특수 UI
|
**필수 컴포넌트**: `v2-split-panel-layout` (1개, rightPanel에 additionalTabs 설정)
|
||||||
**해당 화면**: 약 5% (2개)
|
|
||||||
|
|
||||||
- 생산계획관리: 타임라인/간트 차트 → **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 (그룹화 테이블)
|
> AI가 화면 요청을 받았을 때 이 트리를 따라 패턴을 결정한다.
|
||||||
**재활용 화면 수**: 5개 이상 ✅
|
|
||||||
|
|
||||||
| 화면 | 그룹화 기준 |
|
```
|
||||||
|------|------------|
|
Q1. 시간축 기반 일정/간트차트가 필요한가?
|
||||||
| 품목정보 | 품목구분, 카테고리 |
|
├─ YES → 패턴 G (타임라인)
|
||||||
| 거래처관리 | 거래처유형, 지역 |
|
└─ NO ↓
|
||||||
| 작업지시 | 작업일자, 공정 |
|
|
||||||
| 입출고관리 | 입출고구분, 창고 |
|
|
||||||
| 견적관리 | 상태, 거래처 |
|
|
||||||
|
|
||||||
**기능 요구사항**:
|
Q2. 다차원 집계/피벗 분석이 필요한가?
|
||||||
- 특정 컬럼 기준 그룹핑
|
├─ YES → 패턴 E (피벗)
|
||||||
- 그룹 접기/펼치기
|
└─ NO ↓
|
||||||
- 그룹 헤더에 집계 표시
|
|
||||||
- 다중 그룹핑 지원
|
|
||||||
|
|
||||||
**구현 복잡도**: 중
|
Q3. 데이터를 그룹별로 묶어서 접기/펼치기가 필요한가?
|
||||||
|
├─ YES → 패턴 F (그룹화 테이블)
|
||||||
|
└─ NO ↓
|
||||||
|
|
||||||
### 4.2 v2-tree-view (트리 뷰)
|
Q4. 이미지+정보를 카드 형태로 표시하는가?
|
||||||
**재활용 화면 수**: 3개 ✅
|
├─ YES → 패턴 D (카드 뷰)
|
||||||
|
└─ NO ↓
|
||||||
|
|
||||||
| 화면 | 트리 용도 |
|
Q5. 마스터 테이블 선택 시 연관 디테일이 필요한가?
|
||||||
|------|----------|
|
├─ YES → Q5-1. 디테일에 탭이 필요한가?
|
||||||
| BOM관리 | BOM 구조 (정전개/역전개) |
|
│ ├─ YES → 패턴 C (마스터-디테일+탭)
|
||||||
| 부서정보 | 조직도 |
|
│ └─ NO → 패턴 B (마스터-디테일)
|
||||||
| 메뉴관리 | 메뉴 계층 |
|
└─ NO → 패턴 A (기본 마스터)
|
||||||
|
```
|
||||||
|
|
||||||
**기능 요구사항**:
|
---
|
||||||
- 노드 접기/펼치기
|
|
||||||
- 드래그앤드롭 (선택)
|
|
||||||
- 정전개/역전개 전환
|
|
||||||
- 노드 선택 이벤트
|
|
||||||
|
|
||||||
**구현 복잡도**: 중상
|
## 5. 화면 구현 불가능/제한 사항
|
||||||
|
|
||||||
### 4.3 v2-timeline-scheduler (타임라인)
|
### 5.1 현재 불가능 (별도 React 개발 필요)
|
||||||
**재활용 화면 수**: 1~2개 (기준 미달)
|
|
||||||
|
|
||||||
| 화면 | 용도 |
|
| 기능 | 상태 | 대안 |
|
||||||
|
|------|------|------|
|
||||||
|
| 칸반 보드 (드래그앤드롭) | 미지원 | 별도 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 컴포넌트로 구현 가능
|
### 6.1 기준정보
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 17개 화면 (65%) │
|
|
||||||
│ - 기본 검색 + 테이블 패턴 │
|
|
||||||
│ - 분할 패널 │
|
|
||||||
│ - 탭 전환 │
|
|
||||||
│ - 카드 디스플레이 │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### v2-grouped-table 개발 후
|
| 화면명 | 패턴 | 구현 가능 | 비고 |
|
||||||
```
|
|--------|------|----------|------|
|
||||||
┌─────────────────────────────────────────────────┐
|
| 회사정보 | A | 완전 지원 | |
|
||||||
│ +5개 화면 (22개, 85%) │
|
| 부서정보 | 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 개발 후
|
### 6.2 영업관리
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ +2개 화면 (24개, 92%) │
|
|
||||||
│ - BOM관리, 부서정보(계층) │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 별도 개발 필요
|
| 화면명 | 패턴 | 구현 가능 | 비고 |
|
||||||
```
|
|--------|------|----------|------|
|
||||||
┌─────────────────────────────────────────────────┐
|
| 수주관리 | B | 완전 지원 | |
|
||||||
│ 2개 화면 (8%) │
|
| 견적관리 | 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. 신규 컴포넌트 개발 우선순위
|
## 7. 컴포넌트 커버리지 요약
|
||||||
|
|
||||||
| 순위 | 컴포넌트 | 재활용 화면 수 | 복잡도 | 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. 요약
|
|
||||||
|
|
||||||
| 항목 | 수치 |
|
| 항목 | 수치 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 전체 분석 화면 수 | 26개 |
|
| 전체 분석 대상 화면 | 26개 |
|
||||||
| 현재 즉시 구현 가능 | 17개 (65%) |
|
| V2 컴포넌트로 구현 가능 | **26개 (100%)** |
|
||||||
| v2-grouped-table 추가 시 | 22개 (85%) |
|
| 등록된 V2 컴포넌트 수 | 32개 |
|
||||||
| v2-tree-view 추가 시 | 24개 (92%) |
|
| 화면 UI 패턴 수 | 7가지 (A~G) |
|
||||||
| 별도 개발 필요 | 2개 (8%) |
|
|
||||||
|
|
||||||
**핵심 결론**:
|
**이전 대비 변경 사항**:
|
||||||
1. **현재 V2 컴포넌트**로 65% 화면 구현 가능
|
- BOM 트리 지원 추가 (v2-bom-tree) → BOM관리 완전 지원
|
||||||
2. **v2-grouped-table** 1개 컴포넌트 개발로 85%까지 확대
|
- 그룹화 테이블 지원 추가 (v2-table-grouped) → 품목정보, 입출고 등 완전 지원
|
||||||
3. **v2-tree-view** 추가로 92% 도달
|
- 타임라인 지원 추가 (v2-timeline-scheduler) → 생산계획관리 완전 지원
|
||||||
4. 나머지 8%는 화면별 특수 UI (타임라인, 모바일 스타일)로 개별 개발 필요
|
- 결재 스테퍼 추가 (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 { Input } from "@/components/ui/input";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { V2InputProps, V2InputConfig, V2InputFormat } from "@/types/v2-components";
|
import { V2InputProps, V2InputConfig, V2InputFormat } from "@/types/v2-components";
|
||||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||||
|
|
@ -61,11 +62,11 @@ export function validateInputFormat(value: string, format: V2InputFormat): { isV
|
||||||
return { isValid, errorMessage: isValid ? "" : formatConfig.errorMessage };
|
return { isValid, errorMessage: isValid ? "" : formatConfig.errorMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 통화 형식 변환
|
// 통화 형식 변환 (공통 formatNumber 사용)
|
||||||
function formatCurrency(value: string | number): string {
|
function formatCurrency(value: string | number): string {
|
||||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||||
if (isNaN(num)) return "";
|
if (isNaN(num)) return "";
|
||||||
return num.toLocaleString("ko-KR");
|
return centralFormatNumber(num);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사업자번호 형식 변환
|
// 사업자번호 형식 변환
|
||||||
|
|
@ -234,7 +235,22 @@ const TextInput = forwardRef<
|
||||||
TextInput.displayName = "TextInput";
|
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<
|
const NumberInput = forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
|
|
@ -250,40 +266,112 @@ const NumberInput = forwardRef<
|
||||||
className?: string;
|
className?: string;
|
||||||
inputStyle?: React.CSSProperties;
|
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(
|
const handleChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const val = e.target.value;
|
const input = e.target;
|
||||||
if (val === "") {
|
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);
|
onChange?.(undefined);
|
||||||
return;
|
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 (min !== undefined && num < min) num = min;
|
||||||
if (max !== undefined && num > max) num = max;
|
if (max !== undefined && num > max) num = max;
|
||||||
|
|
||||||
onChange?.(num);
|
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 (
|
return (
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
ref={combinedRef}
|
||||||
type="number"
|
type="text"
|
||||||
value={value ?? ""}
|
inputMode="decimal"
|
||||||
|
value={displayValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={min}
|
onFocus={handleFocus}
|
||||||
max={max}
|
onBlur={handleBlur}
|
||||||
step={step}
|
|
||||||
placeholder={placeholder || "숫자 입력"}
|
placeholder={placeholder || "숫자 입력"}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn("h-full w-full", className)}
|
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 value - 숫자 또는 숫자 문자열
|
||||||
* @param decimals - 소수점 자릿수 (미지정 시 기본값 사용)
|
* @param decimals - 소수점 자릿수 (미지정 시 실제 값에서 자동감지, 최대 5자리)
|
||||||
* @returns 포맷된 문자열
|
* @returns 포맷된 문자열
|
||||||
*/
|
*/
|
||||||
export function formatNumber(value: unknown, decimals?: number): string {
|
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));
|
const num = typeof value === "number" ? value : parseFloat(String(value));
|
||||||
if (isNaN(num)) return 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, {
|
return new Intl.NumberFormat(rules.number.locale, {
|
||||||
minimumFractionDigits: dec,
|
minimumFractionDigits: dec,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { RepeaterColumnConfig } from "./types";
|
import { RepeaterColumnConfig } from "./types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||||
|
|
||||||
// @dnd-kit imports
|
// @dnd-kit imports
|
||||||
import {
|
import {
|
||||||
|
|
@ -594,21 +595,10 @@ export function RepeaterTable({
|
||||||
|
|
||||||
// 계산 필드는 편집 불가
|
// 계산 필드는 편집 불가
|
||||||
if (column.calculated || !column.editable) {
|
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자 초과 시 ... 처리 및 툴팁
|
// 🆕 40자 초과 시 ... 처리 및 툴팁
|
||||||
const { truncated, isTruncated } = truncateText(String(displayValue));
|
const { truncated, isTruncated } = truncateText(String(displayValue));
|
||||||
|
|
@ -623,24 +613,28 @@ export function RepeaterTable({
|
||||||
// 편집 가능한 필드
|
// 편집 가능한 필드
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
case "number":
|
case "number":
|
||||||
// 숫자 표시: 정수/소수점 자동 구분
|
// 콤마 포함 숫자 표시
|
||||||
const displayValue = (() => {
|
const displayValue = (() => {
|
||||||
if (value === undefined || value === null || value === "") return "";
|
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 "";
|
if (isNaN(num)) return "";
|
||||||
return num.toString();
|
return centralFormatNumber(num);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="decimal"
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value;
|
const stripped = e.target.value.replace(/,/g, "");
|
||||||
// 숫자와 소수점만 허용
|
if (stripped === "" || stripped === "-") {
|
||||||
if (val === "" || /^-?\d*\.?\d*$/.test(val)) {
|
handleCellEdit(rowIndex, column.field, 0);
|
||||||
handleCellEdit(rowIndex, column.field, val === "" ? 0 : parseFloat(val) || 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"
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
Check,
|
Check,
|
||||||
|
|
@ -1286,7 +1287,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
// 숫자 포맷팅 헬퍼: 콤마 표시 + 실제 값은 숫자만 저장
|
// 숫자 포맷팅 헬퍼: 콤마 표시 + 실제 값은 숫자만 저장
|
||||||
const rawNum = value ? String(value).replace(/,/g, "") : "";
|
const rawNum = value ? String(value).replace(/,/g, "") : "";
|
||||||
const displayNum = rawNum && !isNaN(Number(rawNum))
|
const displayNum = rawNum && !isNaN(Number(rawNum))
|
||||||
? new Intl.NumberFormat("ko-KR").format(Number(rawNum))
|
? centralFormatNumber(Number(rawNum))
|
||||||
: rawNum;
|
: rawNum;
|
||||||
|
|
||||||
// 계산된 단가는 읽기 전용 + 강조 표시
|
// 계산된 단가는 읽기 전용 + 강조 표시
|
||||||
|
|
@ -1511,9 +1512,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
if (matched) return matched.label;
|
if (matched) return matched.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 숫자는 천 단위 구분
|
// 숫자는 천 단위 구분 (공통 formatNumber 사용)
|
||||||
if (renderType === "number" && !isNaN(Number(strValue))) {
|
if (renderType === "number" && !isNaN(Number(strValue))) {
|
||||||
return new Intl.NumberFormat("ko-KR").format(Number(strValue));
|
return centralFormatNumber(Number(strValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return strValue;
|
return strValue;
|
||||||
|
|
@ -1646,11 +1647,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
|
|
||||||
switch (displayItem.format) {
|
switch (displayItem.format) {
|
||||||
case "currency":
|
case "currency":
|
||||||
// 천 단위 구분
|
formattedValue = centralFormatNumber(Number(fieldValue) || 0);
|
||||||
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
|
||||||
break;
|
break;
|
||||||
case "number":
|
case "number":
|
||||||
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
formattedValue = centralFormatNumber(Number(fieldValue) || 0);
|
||||||
break;
|
break;
|
||||||
case "date":
|
case "date":
|
||||||
// YYYY.MM.DD 형식
|
// YYYY.MM.DD 형식
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { ComponentRendererProps } from "@/types/component";
|
||||||
import { useCalculation } from "./useCalculation";
|
import { useCalculation } from "./useCalculation";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
import { isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer";
|
import { isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer";
|
||||||
|
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||||
|
|
||||||
export interface SimpleRepeaterTableComponentProps extends ComponentRendererProps {
|
export interface SimpleRepeaterTableComponentProps extends ComponentRendererProps {
|
||||||
config?: SimpleRepeaterTableProps;
|
config?: SimpleRepeaterTableProps;
|
||||||
|
|
@ -519,18 +520,17 @@ export function SimpleRepeaterTableComponent({
|
||||||
return result;
|
return result;
|
||||||
}, [value, summaryConfig]);
|
}, [value, summaryConfig]);
|
||||||
|
|
||||||
// 합계 값 포맷팅
|
// 합계 값 포맷팅 (공통 formatNumber 사용)
|
||||||
const formatSummaryValue = (field: SummaryFieldConfig, value: number): string => {
|
const formatSummaryValue = (field: SummaryFieldConfig, value: number): string => {
|
||||||
const decimals = field.decimals ?? 0;
|
const decimals = field.decimals ?? 0;
|
||||||
const formatted = value.toFixed(decimals);
|
|
||||||
|
|
||||||
switch (field.format) {
|
switch (field.format) {
|
||||||
case "currency":
|
case "currency":
|
||||||
return Number(formatted).toLocaleString() + "원";
|
return centralFormatNumber(value, decimals) + "원";
|
||||||
case "percent":
|
case "percent":
|
||||||
return formatted + "%";
|
return value.toFixed(decimals) + "%";
|
||||||
default:
|
default:
|
||||||
return Number(formatted).toLocaleString();
|
return centralFormatNumber(value, decimals);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -554,9 +554,9 @@ export function SimpleRepeaterTableComponent({
|
||||||
return (
|
return (
|
||||||
<div className="px-2 py-1">
|
<div className="px-2 py-1">
|
||||||
{column.type === "number"
|
{column.type === "number"
|
||||||
? typeof cellValue === "number"
|
? (cellValue !== null && cellValue !== undefined && cellValue !== ""
|
||||||
? cellValue.toLocaleString()
|
? centralFormatNumber(cellValue)
|
||||||
: cellValue || "0"
|
: "0")
|
||||||
: cellValue || "-"}
|
: cellValue || "-"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -565,12 +565,28 @@ export function SimpleRepeaterTableComponent({
|
||||||
// 편집 가능한 필드
|
// 편집 가능한 필드
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
case "number":
|
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 (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
value={cellValue || ""}
|
inputMode="decimal"
|
||||||
onChange={(e) => handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)}
|
value={numDisplay}
|
||||||
className="h-7 text-xs"
|
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";
|
} from "lucide-react";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
import { formatNumber as centralFormatNumber } from "@/lib/formatting";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { apiClient, getFullImageUrl } from "@/lib/api/client";
|
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"));
|
.replace("ss", String(date.getSeconds()).padStart(2, "0"));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 숫자 포맷팅 헬퍼 함수
|
// 숫자 포맷팅 헬퍼 함수 (공통 formatNumber 기반)
|
||||||
const formatNumberValue = useCallback((value: any, format: any): string => {
|
const formatNumberValue = useCallback((value: any, format: any): string => {
|
||||||
if (value === null || value === undefined || value === "") return "-";
|
if (value === null || value === undefined || value === "") return "-";
|
||||||
const num = typeof value === "number" ? value : parseFloat(String(value));
|
const num = typeof value === "number" ? value : parseFloat(String(value));
|
||||||
if (isNaN(num)) return String(value);
|
if (isNaN(num)) return String(value);
|
||||||
|
|
||||||
const options: Intl.NumberFormatOptions = {
|
let result: string;
|
||||||
minimumFractionDigits: format?.decimalPlaces ?? 0,
|
if (format?.thousandSeparator === false) {
|
||||||
maximumFractionDigits: format?.decimalPlaces ?? 10,
|
const dec = format?.decimalPlaces ?? 0;
|
||||||
useGrouping: format?.thousandSeparator ?? false,
|
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?.prefix) result = format.prefix + result;
|
||||||
if (format?.suffix) result = result + format.suffix;
|
if (format?.suffix) result = result + format.suffix;
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -1088,14 +1090,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD");
|
return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 숫자 포맷 적용
|
// 숫자 포맷 적용 (format 설정이 있거나 input_type이 number/decimal이면 자동 적용)
|
||||||
|
const isNumericByInputType = colInputType === "number" || colInputType === "decimal";
|
||||||
if (
|
if (
|
||||||
format?.type === "number" ||
|
format?.type === "number" ||
|
||||||
format?.type === "currency" ||
|
format?.type === "currency" ||
|
||||||
format?.thousandSeparator ||
|
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 { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core";
|
||||||
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
|
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
|
||||||
import { useTabId } from "@/contexts/TabIdContext";
|
import { useTabId } from "@/contexts/TabIdContext";
|
||||||
|
import { formatNumber as centralFormatNumber, formatCurrency as centralFormatCurrency } from "@/lib/formatting";
|
||||||
|
|
||||||
// 🖼️ 테이블 셀 이미지 썸네일 컴포넌트
|
// 🖼️ 테이블 셀 이미지 썸네일 컴포넌트
|
||||||
// objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용
|
// objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용
|
||||||
|
|
@ -4445,17 +4446,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 숫자 타입 포맷팅 (천단위 구분자 설정 확인)
|
// 숫자 타입 포맷팅 (공통 formatNumber 사용)
|
||||||
if (inputType === "number" || inputType === "decimal") {
|
if (inputType === "number" || inputType === "decimal") {
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
if (column.thousandSeparator !== false) {
|
||||||
if (!isNaN(numValue)) {
|
return centralFormatNumber(value);
|
||||||
// thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용
|
|
||||||
if (column.thousandSeparator !== false) {
|
|
||||||
return numValue.toLocaleString("ko-KR");
|
|
||||||
}
|
|
||||||
return String(numValue);
|
|
||||||
}
|
}
|
||||||
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||||
|
return isNaN(numValue) ? String(value) : String(numValue);
|
||||||
}
|
}
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
@ -4463,14 +4461,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
switch (column.format) {
|
switch (column.format) {
|
||||||
case "number":
|
case "number":
|
||||||
if (value !== null && value !== undefined && value !== "") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
if (column.thousandSeparator !== false) {
|
||||||
if (!isNaN(numValue)) {
|
return centralFormatNumber(value);
|
||||||
// thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용
|
|
||||||
if (column.thousandSeparator !== false) {
|
|
||||||
return numValue.toLocaleString("ko-KR");
|
|
||||||
}
|
|
||||||
return String(numValue);
|
|
||||||
}
|
}
|
||||||
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
||||||
|
return isNaN(numValue) ? String(value) : String(numValue);
|
||||||
}
|
}
|
||||||
return String(value);
|
return String(value);
|
||||||
case "date":
|
case "date":
|
||||||
|
|
@ -4487,12 +4482,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
return "-";
|
return "-";
|
||||||
case "currency":
|
case "currency":
|
||||||
if (typeof value === "number") {
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
// thousandSeparator가 false가 아닌 경우(기본값 true) 천단위 구분자 적용
|
return centralFormatCurrency(value);
|
||||||
if (column.thousandSeparator !== false) {
|
|
||||||
return `₩${value.toLocaleString()}`;
|
|
||||||
}
|
|
||||||
return `₩${value}`;
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
case "boolean":
|
case "boolean":
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue