jskim-node #415

Merged
kjs merged 4 commits from jskim-node into main 2026-03-13 16:02:32 +09:00
9 changed files with 1383 additions and 908 deletions
Showing only changes of commit 7a65ab0f85 - Show all commits

View File

@ -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 | 회사 코드 |

View File

@ -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" }}
/>
);
});

View File

@ -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,

View File

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

View File

@ -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 형식

View File

@ -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"
/>
);

View File

@ -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 });
}
// 카테고리 매핑 찾기 (여러 키 형태 시도)

View File

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