Compare commits

...

6 Commits

Author SHA1 Message Date
kjs 3ea62df623 Merge pull request 'jskim-node' (#415) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/415
2026-03-13 16:02:30 +09:00
kjs 2f82247236 Merge branch 'main' into jskim-node 2026-03-13 16:02:19 +09:00
kjs a2040a228a docs: add document sync rule for component and DB changes
- Introduced a new document sync rule to ensure that related documentation is updated whenever components are added or modified, or when there are changes to the database structure.
- Specified the documents that must be updated, including the full-screen analysis and V2 component usage guide, along with detailed instructions on how to update them.
- This addition aims to enforce consistency and accuracy in documentation, facilitating better collaboration and adherence to development standards.

Made-with: Cursor
2026-03-13 16:02:02 +09:00
kjs 7a65ab0f85 docs: update full-screen analysis and V2 component usage guide
- Revised the full-screen analysis document to reflect the latest updates, including the purpose and core rules for screen development.
- Expanded the V2 component usage guide to include a comprehensive catalog of components, their configurations, and usage guidelines for LLM and chatbot applications.
- Added a summary of the system architecture and clarified the implementation methods for user business screens and admin menus.
- Enhanced the documentation to serve as a reference for AI agents and screen designers, ensuring adherence to the established guidelines.

These updates aim to improve clarity and usability for developers and designers working with the WACE ERP screen composition system.

Made-with: Cursor
2026-03-13 15:02:06 +09:00
kjs 429f1ba6ee feat: add item list mode configuration and screen code handling
- Introduced `itemListMode` to the process work standard configuration, allowing users to select between displaying all items or only registered items.
- Added `screenCode` to automatically set the screen ID when in registered mode.
- Updated the `ProcessWorkStandardComponent` to handle the new configuration and adjust item fetching logic accordingly.
- Enhanced the `ProcessWorkStandardConfigPanel` to include a select input for item list mode, improving user experience and configurability.

These changes aim to enhance the flexibility and usability of the process work standard component.

Made-with: Cursor
2026-03-13 14:01:09 +09:00
kjs 29b9cbdc90 Merge pull request 'jskim-node' (#414) from jskim-node into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/414
2026-03-13 11:47:07 +09:00
18 changed files with 2443 additions and 1065 deletions

View File

@ -0,0 +1,38 @@
---
description: 컴포넌트 추가/수정 또는 DB 구조 변경 시 관련 문서를 항상 최신화하도록 강제하는 규칙
globs:
- "frontend/lib/registry/components/**/*.tsx"
- "frontend/components/v2/**/*.tsx"
- "db/migrations/**/*.sql"
- "backend-node/src/types/ddl.ts"
---
# 컴포넌트 및 DB 구조 변경 시 문서 동기화 규칙
## 🚨 핵심 원칙 (절대 준수)
새로운 V2 컴포넌트를 생성하거나 기존 컴포넌트의 설정(overrides)을 변경할 때, 또는 DB 테이블 구조나 화면 생성 파이프라인이 변경될 때는 **반드시** 아래 두 문서를 함께 업데이트해야 합니다.
1. `docs/screen-implementation-guide/01_reference_guides/full-screen-analysis.md` (전체 레퍼런스)
2. `docs/screen-implementation-guide/01_reference_guides/v2-component-usage-guide.md` (실행 가이드)
## 📌 업데이트 대상 및 방법
### 1. V2 컴포넌트 신규 추가 또는 속성(Props/Overrides) 변경 시
- **`full-screen-analysis.md`**: `3. 컴포넌트 전체 설정 레퍼런스` 섹션에 해당 컴포넌트의 모든 설정값(타입, 기본값, 설명)을 표 형태로 추가/수정하세요.
- **`v2-component-usage-guide.md`**:
- `7. Step 6: screen_layouts_v2 INSERT`의 컴포넌트 url 매핑표에 추가하세요.
- `16. 컴포넌트 빠른 참조표`에 추가하세요.
- 필요한 경우 `8. 패턴별 layout_data 완전 예시`에 새로운 패턴을 추가하세요.
### 2. DB 테이블 구조 또는 화면 생성 로직 변경 시
- **`full-screen-analysis.md`**: `2. DB 테이블 스키마` 섹션의 테이블 구조(컬럼, 타입, 설명)를 최신화하세요.
- **`v2-component-usage-guide.md`**:
- `Step 1` ~ `Step 7`의 SQL 템플릿이 변경된 구조와 일치하는지 확인하고 수정하세요.
- 특히 `INSERT` 문의 컬럼 목록과 `VALUES` 형식이 정확한지 검증하세요.
## ⚠️ AI 에이전트 행동 지침
1. 사용자가 컴포넌트 코드를 수정해달라고 요청하면, 수정 완료 후 **"관련 가이드 문서도 업데이트할까요?"** 라고 반드시 물어보세요.
2. 사용자가 DB 마이그레이션 스크립트를 작성해달라고 하거나 핵심 시스템 테이블을 건드리면, 가이드 문서의 SQL 템플릿도 수정해야 하는지 확인하세요.
3. 가이드 문서 업데이트 시 JSON 예제 안에 `//` 같은 주석을 넣지 않도록 주의하세요 (DB 파싱 에러 방지).

View File

@ -1,331 +0,0 @@
# 화면 전체 분석 보고서
> **분석 대상**: `/Users/kimjuseok/Downloads/화면개발 8` 폴더 내 핵심 업무 화면
> **분석 기준**: 메뉴별 분류, 3개 이상 재활용 가능한 컴포넌트 식별
> **분석 일자**: 2026-01-30
---
## 1. 현재 사용 중인 V2 컴포넌트 목록
> **중요**: v2- 접두사가 붙은 컴포넌트만 사용합니다.
### 입력 컴포넌트
| ID | 이름 | 용도 |
|----|------|------|
| `v2-input` | V2 입력 | 텍스트, 숫자, 비밀번호, 이메일 등 입력 |
| `v2-select` | V2 선택 | 드롭다운, 콤보박스, 라디오, 체크박스 |
| `v2-date` | V2 날짜 | 날짜, 시간, 날짜범위 입력 |
### 표시 컴포넌트
| ID | 이름 | 용도 |
|----|------|------|
| `v2-text-display` | 텍스트 표시 | 라벨, 텍스트 표시 |
| `v2-card-display` | 카드 디스플레이 | 테이블 데이터를 카드 형태로 표시 |
| `v2-aggregation-widget` | 집계 위젯 | 합계, 평균, 개수 등 집계 표시 |
### 테이블/데이터 컴포넌트
| ID | 이름 | 용도 |
|----|------|------|
| `v2-table-list` | 테이블 리스트 | 데이터 테이블 표시, 페이지네이션, 정렬, 필터 |
| `v2-table-search-widget` | 검색 필터 | 화면 내 테이블 검색/필터/그룹 기능 |
| `v2-pivot-grid` | 피벗 그리드 | 다차원 데이터 분석 (피벗 테이블) |
### 레이아웃 컴포넌트
| 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. 화면 분류 (메뉴별)
### 01. 기준정보 (master-data)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 회사정보 | 회사정보.html | 검색+테이블 | ✅ 완전 |
| 부서정보 | 부서정보.html | 검색+테이블 | ✅ 완전 |
| 품목정보 | 품목정보.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 |
| BOM관리 | BOM관리.html | 분할패널+트리 | ⚠️ 트리뷰 미지원 |
| 공정정보관리 | 공정정보관리.html | 분할패널+테이블 | ✅ 완전 |
| 공정작업기준 | 공정작업기준관리.html | 검색+테이블 | ✅ 완전 |
| 품목라우팅 | 품목라우팅관리.html | 분할패널+테이블 | ✅ 완전 |
### 02. 영업관리 (sales)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 수주관리 | 수주관리.html | 분할패널+테이블 | ✅ 완전 |
| 견적관리 | 견적관리.html | 분할패널+테이블 | ✅ 완전 |
| 거래처관리 | 거래처관리.html | 분할패널+탭+그룹화 | ⚠️ 그룹화 미지원 |
| 판매품목정보 | 판매품목정보.html | 검색+테이블 | ✅ 완전 |
| 출하계획관리 | 출하계획관리.html | 검색+테이블 | ✅ 완전 |
### 03. 생산관리 (production)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 생산계획관리 | 생산계획관리.html | 분할패널+탭+타임라인 | ❌ 타임라인 미지원 |
| 생산관리 | 생산관리.html | 검색+테이블 | ✅ 완전 |
| 생산실적관리 | 생산실적관리.html | 검색+테이블 | ✅ 완전 |
| 작업지시 | 작업지시.html | 탭+그룹화테이블+분할패널 | ⚠️ 그룹화 미지원 |
| 공정관리 | 공정관리.html | 분할패널+테이블 | ✅ 완전 |
### 04. 구매관리 (purchase)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 발주관리 | 발주관리.html | 검색+테이블 | ✅ 완전 |
| 공급업체관리 | 공급업체관리.html | 검색+테이블 | ✅ 완전 |
| 구매입고 | pages/구매입고.html | 검색+테이블 | ✅ 완전 |
### 05. 설비관리 (equipment)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 설비정보 | 설비정보.html | 분할패널+카드+탭 | ✅ v2-card-display 활용 |
### 06. 물류관리 (logistics)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 창고관리 | 창고관리.html | 모바일앱스타일+iframe | ❌ 별도개발 필요 |
| 창고정보관리 | 창고정보관리.html | 검색+테이블 | ✅ 완전 |
| 입출고관리 | 입출고관리.html | 검색+테이블+그룹화 | ⚠️ 그룹화 미지원 |
| 재고현황 | 재고현황.html | 검색+테이블 | ✅ 완전 |
### 07. 품질관리 (quality)
| 화면명 | 파일명 | 패턴 | 구현 가능 |
|--------|--------|------|----------|
| 검사기준 | 검사기준.html | 검색+테이블 | ✅ 완전 |
| 검사정보관리 | 검사정보관리.html | 탭+테이블 | ✅ 완전 |
| 검사장비관리 | 검사장비관리.html | 검색+테이블 | ✅ 완전 |
| 불량관리 | 불량관리.html | 검색+테이블 | ✅ 완전 |
| 클레임관리 | 클레임관리.html | 검색+테이블 | ✅ 완전 |
---
## 3. 화면 UI 패턴 분석
### 패턴 A: 검색 + 테이블 (가장 기본)
**해당 화면**: 약 60% (15개 이상)
**사용 컴포넌트**:
- `v2-table-search-widget`: 검색 필터
- `v2-table-list`: 데이터 테이블
```
┌─────────────────────────────────────────┐
│ [검색필드들...] [조회] [엑셀] │ ← v2-table-search-widget
├─────────────────────────────────────────┤
│ 테이블 제목 [신규등록] [삭제] │
│ ────────────────────────────────────── │
│ □ | 코드 | 이름 | 상태 | 등록일 | │ ← v2-table-list
│ □ | A001 | 테스트| 사용 | 2026-01-30 | │
└─────────────────────────────────────────┘
```
### 패턴 B: 분할 패널 (마스터-디테일)
**해당 화면**: 약 25% (8개)
**사용 컴포넌트**:
- `v2-split-panel-layout`: 좌우 분할
- `v2-table-list`: 마스터/디테일 테이블
- `v2-tabs-widget`: 상세 탭 (선택)
```
┌──────────────────┬──────────────────────┐
│ 마스터 리스트 │ 상세 정보 / 탭 │
│ ─────────────── │ ┌────┬────┬────┐ │
│ □ A001 제품A │ │기본│이력│첨부│ │
│ □ A002 제품B ← │ └────┴────┴────┘ │
│ □ A003 제품C │ [테이블 or 폼] │
└──────────────────┴──────────────────────┘
```
### 패턴 C: 탭 + 테이블
**해당 화면**: 약 10% (3개)
**사용 컴포넌트**:
- `v2-tabs-widget`: 탭 전환
- `v2-table-list`: 탭별 테이블
```
┌─────────────────────────────────────────┐
│ [탭1] [탭2] [탭3] │
├─────────────────────────────────────────┤
│ [테이블 영역] │
└─────────────────────────────────────────┘
```
### 패턴 D: 특수 UI
**해당 화면**: 약 5% (2개)
- 생산계획관리: 타임라인/간트 차트 → **v2-timeline 미존재**
- 창고관리: 모바일 앱 스타일 → **별도 개발 필요**
---
## 4. 신규 컴포넌트 분석 (3개 이상 재활용 기준)
### 4.1 v2-grouped-table (그룹화 테이블)
**재활용 화면 수**: 5개 이상 ✅
| 화면 | 그룹화 기준 |
|------|------------|
| 품목정보 | 품목구분, 카테고리 |
| 거래처관리 | 거래처유형, 지역 |
| 작업지시 | 작업일자, 공정 |
| 입출고관리 | 입출고구분, 창고 |
| 견적관리 | 상태, 거래처 |
**기능 요구사항**:
- 특정 컬럼 기준 그룹핑
- 그룹 접기/펼치기
- 그룹 헤더에 집계 표시
- 다중 그룹핑 지원
**구현 복잡도**: 중
### 4.2 v2-tree-view (트리 뷰)
**재활용 화면 수**: 3개 ✅
| 화면 | 트리 용도 |
|------|----------|
| BOM관리 | BOM 구조 (정전개/역전개) |
| 부서정보 | 조직도 |
| 메뉴관리 | 메뉴 계층 |
**기능 요구사항**:
- 노드 접기/펼치기
- 드래그앤드롭 (선택)
- 정전개/역전개 전환
- 노드 선택 이벤트
**구현 복잡도**: 중상
### 4.3 v2-timeline-scheduler (타임라인)
**재활용 화면 수**: 1~2개 (기준 미달)
| 화면 | 용도 |
|------|------|
| 생산계획관리 | 간트 차트 |
| 설비 가동 현황 | 타임라인 |
**기능 요구사항**:
- 시간축 기반 배치
- 드래그로 일정 변경
- 공정별 색상 구분
- 줌 인/아웃
**구현 복잡도**: 상
> **참고**: 3개 미만이므로 우선순위 하향
---
## 5. 컴포넌트 커버리지
### 현재 V2 컴포넌트로 구현 가능
```
┌─────────────────────────────────────────────────┐
│ 17개 화면 (65%) │
│ - 기본 검색 + 테이블 패턴 │
│ - 분할 패널 │
│ - 탭 전환 │
│ - 카드 디스플레이 │
└─────────────────────────────────────────────────┘
```
### v2-grouped-table 개발 후
```
┌─────────────────────────────────────────────────┐
│ +5개 화면 (22개, 85%) │
│ - 품목정보, 거래처관리, 작업지시 │
│ - 입출고관리, 견적관리 │
└─────────────────────────────────────────────────┘
```
### v2-tree-view 개발 후
```
┌─────────────────────────────────────────────────┐
│ +2개 화면 (24개, 92%) │
│ - BOM관리, 부서정보(계층) │
└─────────────────────────────────────────────────┘
```
### 별도 개발 필요
```
┌─────────────────────────────────────────────────┐
│ 2개 화면 (8%) │
│ - 생산계획관리 (타임라인) │
│ - 창고관리 (모바일 앱 스타일) │
└─────────────────────────────────────────────────┘
```
---
## 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. 요약
| 항목 | 수치 |
|------|------|
| 전체 분석 화면 수 | 26개 |
| 현재 즉시 구현 가능 | 17개 (65%) |
| v2-grouped-table 추가 시 | 22개 (85%) |
| v2-tree-view 추가 시 | 24개 (92%) |
| 별도 개발 필요 | 2개 (8%) |
**핵심 결론**:
1. **현재 V2 컴포넌트**로 65% 화면 구현 가능
2. **v2-grouped-table** 1개 컴포넌트 개발로 85%까지 확대
3. **v2-tree-view** 추가로 92% 도달
4. 나머지 8%는 화면별 특수 UI (타임라인, 모바일 스타일)로 개별 개발 필요

View File

@ -1,631 +0,0 @@
# V2 공통 컴포넌트 사용 가이드
> **목적**: 다양한 회사에서 V2 컴포넌트를 활용하여 화면을 개발할 때 참고하는 범용 가이드
> **대상**: 화면 설계자, 개발자
> **버전**: 1.1.0
> **작성일**: 2026-02-23 (최종 업데이트)
---
## 1. V2 컴포넌트로 가능한 것 / 불가능한 것
### 1.1 가능한 화면 유형
| 화면 유형 | 설명 | 대표 예시 |
|-----------|------|----------|
| 마스터 관리 | 단일 테이블 CRUD | 회사정보, 부서정보, 코드관리 |
| 마스터-디테일 | 좌측 선택 → 우측 상세 | 공정관리, 품목라우팅, 견적관리 |
| 탭 기반 화면 | 탭별 다른 테이블/뷰 | 검사정보관리, 거래처관리 |
| 카드 뷰 | 이미지+정보 카드 형태 | 설비정보, 대시보드 |
| 피벗 분석 | 다차원 집계 | 매출분석, 재고현황 |
| 반복 컨테이너 | 데이터 수만큼 UI 반복 | 주문 상세, 항목 리스트 |
| 그룹화 테이블 | 그룹핑 기능 포함 테이블 | 카테고리별 집계, 부서별 현황 |
| 타임라인/스케줄 | 시간축 기반 일정 관리 | 생산일정, 작업스케줄 |
### 1.2 불가능한 화면 유형 (별도 개발 필요)
| 화면 유형 | 이유 | 해결 방안 |
|-----------|------|----------|
| 트리 뷰 (계층 구조) | 트리 컴포넌트 미존재 | `v2-tree-view` 개발 필요 |
| 드래그앤드롭 보드 | 칸반 스타일 UI 없음 | 별도 개발 |
| 모바일 앱 스타일 | 네이티브 앱 UI | 별도 개발 |
| 복잡한 차트 | 기본 집계 외 시각화 | 차트 라이브러리 연동 |
> **참고**: 그룹화 테이블(`v2-table-grouped`)과 타임라인 스케줄러(`v2-timeline-scheduler`)는 v1.1에서 추가되어 이제 지원됩니다.
---
## 2. V2 컴포넌트 전체 목록 (25개)
### 2.1 입력 컴포넌트 (4개)
| ID | 이름 | 용도 | 주요 옵션 |
|----|------|------|----------|
| `v2-input` | 입력 | 텍스트, 숫자, 비밀번호, 슬라이더, 컬러 | inputType(text/number/password/slider/color/button), format(email/tel/url/currency/biz_no), required, readonly, maxLength, min, max, step |
| `v2-select` | 선택 | 드롭다운, 콤보박스, 라디오, 체크, 태그, 토글, 스왑 | mode(dropdown/combobox/radio/check/tag/tagbox/toggle/swap), source(static/code/db/api/entity/category/distinct/select), searchable, multiple, cascading |
| `v2-date` | 날짜 | 날짜, 시간, 날짜시간 | dateType(date/time/datetime), format, range, minDate, maxDate, showToday |
| `v2-file-upload` | 파일 업로드 | 파일/이미지 업로드 | - |
### 2.2 표시 컴포넌트 (3개)
| ID | 이름 | 용도 | 주요 옵션 |
|----|------|------|----------|
| `v2-text-display` | 텍스트 표시 | 라벨, 제목, 설명 텍스트 | fontSize, fontWeight, color, textAlign |
| `v2-card-display` | 카드 디스플레이 | 테이블 데이터를 카드 형태로 표시 | cardsPerRow, cardSpacing, columnMapping(titleColumn/subtitleColumn/descriptionColumn/imageColumn), cardStyle(imagePosition/imageSize), dataSource(table/static/api) |
| `v2-aggregation-widget` | 집계 위젯 | 합계, 평균, 개수, 최대, 최소 | items, filters, layout |
### 2.3 테이블/데이터 컴포넌트 (4개)
| ID | 이름 | 용도 | 주요 옵션 |
|----|------|------|----------|
| `v2-table-list` | 테이블 리스트 | 데이터 조회/편집 테이블 | selectedTable, columns, pagination, filter, displayMode(table/card), checkbox, horizontalScroll, linkedFilters, excludeFilter, toolbar, tableStyle, autoLoad |
| `v2-table-search-widget` | 검색 필터 | 테이블 검색/필터/그룹 | autoSelectFirstTable, showTableSelector, title |
| `v2-pivot-grid` | 피벗 그리드 | 다차원 분석 (행/열/데이터 영역) | fields(area: row/column/data/filter, summaryType: sum/avg/count/min/max/countDistinct, groupInterval: year/quarter/month/week/day), dataSource(type: table/api/static, joinConfigs, filterConditions) |
| `v2-table-grouped` | 그룹화 테이블 | 그룹핑 기능이 포함된 테이블 | - |
### 2.4 레이아웃 컴포넌트 (7개)
| ID | 이름 | 용도 | 주요 옵션 |
|----|------|------|----------|
| `v2-split-panel-layout` | 분할 패널 | 마스터-디테일 좌우 분할 | splitRatio, resizable, minLeftWidth, minRightWidth, syncSelection, panel별: displayMode(list/table/custom), relation(type/foreignKey), editButton, addButton, deleteButton, additionalTabs |
| `v2-tabs-widget` | 탭 위젯 | 탭 전환, 탭 내 컴포넌트 | tabs(id/label/order/disabled/components), defaultTab, orientation(horizontal/vertical), allowCloseable, persistSelection |
| `v2-section-card` | 섹션 카드 | 제목+테두리 그룹화 | title, collapsible, padding |
| `v2-section-paper` | 섹션 페이퍼 | 배경색 그룹화 | backgroundColor, padding, shadow |
| `v2-divider-line` | 구분선 | 영역 구분 | orientation, thickness |
| `v2-repeat-container` | 리피터 컨테이너 | 데이터 수만큼 반복 렌더링 | dataSourceType, layout, gridColumns |
| `v2-repeater` | 리피터 | 반복 컨트롤 (inline/modal) | - |
### 2.5 액션/특수 컴포넌트 (7개)
| ID | 이름 | 용도 | 주요 옵션 |
|----|------|------|----------|
| `v2-button-primary` | 기본 버튼 | 저장, 삭제 등 액션 | text, actionType, variant |
| `v2-numbering-rule` | 채번규칙 | 자동 코드/번호 생성 | rule, prefix, format |
| `v2-category-manager` | 카테고리 관리자 | 카테고리 관리 UI | - |
| `v2-location-swap-selector` | 위치 교환 | 위치 선택/교환 | - |
| `v2-rack-structure` | 랙 구조 | 창고 랙 시각화 | - |
| `v2-media` | 미디어 | 이미지/동영상 표시 | - |
| `v2-timeline-scheduler` | 타임라인 스케줄러 | 시간축 기반 일정/작업 관리 | - |
---
## 3. 화면 패턴별 컴포넌트 조합
### 3.1 패턴 A: 기본 마스터 화면 (가장 흔함)
**적용 화면**: 코드관리, 사용자관리, 부서정보, 창고정보 등
```
┌─────────────────────────────────────────────────┐
│ v2-table-search-widget │
│ [검색필드1] [검색필드2] [조회] [엑셀] │
├─────────────────────────────────────────────────┤
│ v2-table-list │
│ 제목 [신규] [삭제] │
│ ─────────────────────────────────────────────── │
│ □ | 코드 | 이름 | 상태 | 등록일 | │
└─────────────────────────────────────────────────┘
```
**필수 컴포넌트**:
- `v2-table-search-widget` (1개)
- `v2-table-list` (1개)
**설정 포인트**:
- 테이블명 지정
- 검색 대상 컬럼 설정
- 컬럼 표시/숨김 설정
---
### 3.2 패턴 B: 마스터-디테일 화면
**적용 화면**: 공정관리, 견적관리, 수주관리, 품목라우팅 등
```
┌──────────────────┬──────────────────────────────┐
│ v2-table-list │ v2-table-list 또는 폼 │
│ (마스터) │ (디테일) │
│ ─────────────── │ │
│ □ A001 항목1 │ [상세 정보] │
│ □ A002 항목2 ← │ │
│ □ A003 항목3 │ │
└──────────────────┴──────────────────────────────┘
v2-split-panel-layout
```
**필수 컴포넌트**:
- `v2-split-panel-layout` (1개)
- `v2-table-list` (2개: 마스터, 디테일)
**설정 포인트**:
- `splitRatio`: 좌우 비율 (기본 30:70)
- `relation.type`: join / detail / custom
- `relation.foreignKey`: 연결 키 컬럼
---
### 3.3 패턴 C: 마스터-디테일 + 탭
**적용 화면**: 거래처관리, 품목정보, 설비정보 등
```
┌──────────────────┬──────────────────────────────┐
│ v2-table-list │ v2-tabs-widget │
│ (마스터) │ ┌────┬────┬────┐ │
│ │ │기본│이력│첨부│ │
│ □ A001 거래처1 │ └────┴────┴────┘ │
│ □ A002 거래처2 ← │ [탭별 컨텐츠] │
└──────────────────┴──────────────────────────────┘
```
**필수 컴포넌트**:
- `v2-split-panel-layout` (1개)
- `v2-table-list` (1개: 마스터)
- `v2-tabs-widget` (1개)
**설정 포인트**:
- 탭별 표시할 테이블/폼 설정
- 마스터 선택 시 탭 컨텐츠 연동
---
### 3.4 패턴 D: 카드 뷰
**적용 화면**: 설비정보, 대시보드, 상품 카탈로그 등
```
┌─────────────────────────────────────────────────┐
│ v2-table-search-widget │
├─────────────────────────────────────────────────┤
│ v2-card-display │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ [이미지]│ │ [이미지]│ │ [이미지]│ │
│ │ 제목 │ │ 제목 │ │ 제목 │ │
│ │ 설명 │ │ 설명 │ │ 설명 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────┘
```
**필수 컴포넌트**:
- `v2-table-search-widget` (1개)
- `v2-card-display` (1개)
**설정 포인트**:
- `cardsPerRow`: 한 행당 카드 수
- `columnMapping`: 제목, 부제목, 이미지, 상태 필드 매핑
- `cardStyle`: 이미지 위치, 크기
---
### 3.5 패턴 E: 피벗 분석
**적용 화면**: 매출분석, 재고현황, 생산실적 분석 등
```
┌─────────────────────────────────────────────────┐
│ v2-pivot-grid │
│ │ 2024년 │ 2025년 │ 2026년 │ 합계 │
│ ─────────────────────────────────────────────── │
│ 지역A │ 1,000 │ 1,200 │ 1,500 │ 3,700 │
│ 지역B │ 2,000 │ 2,500 │ 3,000 │ 7,500 │
│ 합계 │ 3,000 │ 3,700 │ 4,500 │ 11,200 │
└─────────────────────────────────────────────────┘
```
**필수 컴포넌트**:
- `v2-pivot-grid` (1개)
**설정 포인트**:
- `fields[].area`: row / column / data / filter
- `fields[].summaryType`: sum / avg / count / min / max
- `fields[].groupInterval`: 날짜 그룹화 (year/quarter/month)
---
## 4. 회사별 개발 시 핵심 체크포인트
### 4.1 테이블 설계 확인
**가장 먼저 확인**:
1. 회사에서 사용할 테이블 목록
2. 테이블 간 관계 (FK)
3. 조회 조건으로 쓸 컬럼
```
✅ 체크리스트:
□ 테이블명이 DB에 존재하는가?
□ company_code 컬럼이 있는가? (멀티테넌시)
□ 마스터-디테일 관계의 FK가 정의되어 있는가?
□ 검색 대상 컬럼에 인덱스가 있는가?
```
### 4.2 화면 패턴 판단
**질문을 통한 판단**:
| 질문 | 예 → 패턴 |
|------|----------|
| 단일 테이블만 조회/편집? | 패턴 A (기본 마스터) |
| 마스터 선택 시 디테일 표시? | 패턴 B (마스터-디테일) |
| 상세에 탭이 필요? | 패턴 C (마스터-디테일+탭) |
| 이미지+정보 카드 형태? | 패턴 D (카드 뷰) |
| 다차원 집계/분석? | 패턴 E (피벗) |
### 4.3 컴포넌트 설정 필수 항목
#### v2-table-list 필수 설정
```typescript
{
selectedTable: "테이블명", // 필수
columns: [ // 표시할 컬럼
{ columnName: "id", displayName: "ID", visible: true, sortable: true },
// ...
],
pagination: {
enabled: true,
pageSize: 20,
showSizeSelector: true,
showPageInfo: true
},
displayMode: "table", // "table" | "card"
checkbox: {
enabled: true,
multiple: true,
position: "left",
selectAll: true
},
horizontalScroll: { // 가로 스크롤 설정
enabled: true,
maxVisibleColumns: 8
},
linkedFilters: [], // 연결 필터 (다른 컴포넌트와 연동)
excludeFilter: {}, // 제외 필터
autoLoad: true, // 자동 데이터 로드
stickyHeader: false, // 헤더 고정
autoWidth: true // 자동 너비 조정
}
```
#### v2-split-panel-layout 필수 설정
```typescript
{
leftPanel: {
displayMode: "table", // "list" | "table" | "custom"
tableName: "마스터_테이블명",
columns: [], // 컬럼 설정
editButton: { // 수정 버튼 설정
enabled: true,
mode: "auto", // "auto" | "modal"
modalScreenId: "" // 모달 모드 시 화면 ID
},
addButton: { // 추가 버튼 설정
enabled: true,
mode: "auto",
modalScreenId: ""
},
deleteButton: { // 삭제 버튼 설정
enabled: true,
buttonLabel: "삭제",
confirmMessage: "삭제하시겠습니까?"
},
addModalColumns: [], // 추가 모달 전용 컬럼
additionalTabs: [] // 추가 탭 설정
},
rightPanel: {
displayMode: "table",
tableName: "디테일_테이블명",
relation: {
type: "detail", // "join" | "detail" | "custom"
foreignKey: "master_id", // 연결 키
leftColumn: "", // 좌측 연결 컬럼
rightColumn: "", // 우측 연결 컬럼
keys: [] // 복합 키
}
},
splitRatio: 30, // 좌측 비율 (0-100)
resizable: true, // 리사이즈 가능
minLeftWidth: 200, // 좌측 최소 너비
minRightWidth: 300, // 우측 최소 너비
syncSelection: true, // 선택 동기화
autoLoad: true // 자동 로드
}
```
#### v2-split-panel-layout 커스텀 모드 (NEW)
패널 내부에 자유롭게 컴포넌트를 배치할 수 있습니다. (v2-tabs-widget과 동일 구조)
```typescript
{
leftPanel: {
displayMode: "custom", // 커스텀 모드 활성화
components: [ // 내부 컴포넌트 배열
{
id: "btn-save",
componentType: "v2-button-primary",
label: "저장",
position: { x: 10, y: 10 },
size: { width: 100, height: 40 },
componentConfig: { buttonAction: "save" }
},
{
id: "tbl-list",
componentType: "v2-table-list",
label: "목록",
position: { x: 10, y: 60 },
size: { width: 400, height: 300 },
componentConfig: { selectedTable: "테이블명" }
}
]
},
rightPanel: {
displayMode: "table" // 기존 모드 유지
}
}
```
**디자인 모드 기능**:
- 컴포넌트 클릭 → 좌측 설정 패널에서 속성 편집
- 드래그 핸들(상단)로 이동
- 리사이즈 핸들(모서리)로 크기 조절
- 실제 컴포넌트 미리보기 렌더링
#### v2-card-display 필수 설정
```typescript
{
dataSource: "table",
columnMapping: {
title: "name", // 제목 필드
subtitle: "code", // 부제목 필드
image: "image_url", // 이미지 필드 (선택)
status: "status" // 상태 필드 (선택)
},
cardsPerRow: 3
}
```
---
## 5. 공통 컴포넌트 한계점
### 5.1 현재 불가능한 기능
| 기능 | 상태 | 대안 |
|------|------|------|
| 트리 뷰 (BOM, 조직도) | ❌ 미지원 | 테이블로 대체 or 별도 개발 |
| 드래그앤드롭 정렬 | ❌ 미지원 | 순서 컬럼으로 대체 |
| 인라인 편집 | ⚠️ 제한적 | 모달 편집으로 대체 |
| 복잡한 차트 | ❌ 미지원 | 외부 라이브러리 연동 |
> **v1.1 업데이트**: 그룹화 테이블(`v2-table-grouped`)과 타임라인 스케줄러(`v2-timeline-scheduler`)가 추가되어 해당 기능은 이제 지원됩니다.
### 5.2 권장하지 않는 조합
| 조합 | 이유 |
|------|------|
| 3단계 이상 중첩 분할 | 화면 복잡도 증가, 성능 저하 |
| 탭 안에 탭 | 사용성 저하 |
| 한 화면에 3개 이상 테이블 | 데이터 로딩 성능 |
| 피벗 + 상세 테이블 동시 | 데이터 과부하 |
---
## 6. 제어관리 (비즈니스 로직) - 별도 설정 필수
> **핵심**: V2 컴포넌트는 **UI만 담당**합니다. 비즈니스 로직은 **제어관리**에서 별도 설정해야 합니다.
### 6.1 UI vs 제어 분리 구조
```
┌─────────────────────────────────────────────────────────────────┐
│ 화면 구성 │
├─────────────────────────────┬───────────────────────────────────┤
│ UI 레이아웃 │ 제어관리 │
│ (screen_layouts_v2) │ (dataflow_diagrams) │
├─────────────────────────────┼───────────────────────────────────┤
│ • 컴포넌트 배치 │ • 버튼 클릭 시 액션 │
│ • 검색 필드 구성 │ • INSERT/UPDATE/DELETE 설정 │
│ • 테이블 컬럼 표시 │ • 조건부 실행 │
│ • 카드/탭 레이아웃 │ • 다중 행 처리 │
│ │ • 테이블 간 데이터 이동 │
└─────────────────────────────┴───────────────────────────────────┘
```
### 6.2 HTML에서 파악 가능/불가능
| 구분 | HTML에서 파악 | 이유 |
|------|--------------|------|
| 컴포넌트 배치 | ✅ 가능 | HTML 구조에서 보임 |
| 검색 필드 | ✅ 가능 | input 태그로 확인 |
| 테이블 컬럼 | ✅ 가능 | thead에서 확인 |
| **저장 테이블** | ❌ 불가능 | JS/백엔드에서 처리 |
| **버튼 액션** | ❌ 불가능 | 제어관리에서 설정 |
| **전/후 처리** | ❌ 불가능 | 제어관리에서 설정 |
| **다중 행 처리** | ❌ 불가능 | 제어관리에서 설정 |
| **테이블 간 관계** | ❌ 불가능 | DB/제어관리에서 설정 |
### 6.3 제어관리 설정 항목
#### 트리거 타입
- **버튼 클릭 전 (before)**: 클릭 직전 실행
- **버튼 클릭 후 (after)**: 클릭 완료 후 실행
#### 액션 타입
- **INSERT**: 새로운 데이터 삽입
- **UPDATE**: 기존 데이터 수정
- **DELETE**: 데이터 삭제
#### 조건 설정
```typescript
// 예: 선택된 행의 상태가 '대기'인 경우에만 실행
{
field: "status",
operator: "=",
value: "대기",
dataType: "string"
}
```
#### 필드 매핑
```typescript
// 예: 소스 테이블의 값을 타겟 테이블로 이동
{
sourceTable: "order_master",
sourceField: "order_no",
targetTable: "order_history",
targetField: "order_no"
}
```
### 6.4 제어관리 예시: 수주 확정 버튼
**시나리오**: 수주 목록에서 3건 선택 후 [확정] 버튼 클릭
```
┌─────────────────────────────────────────────────────────────────┐
│ [확정] 버튼 클릭 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 조건 체크: status = '대기' 인 행만 │
│ 2. UPDATE order_master SET status = '확정' WHERE id IN (선택) │
│ 3. INSERT order_history (수주이력 테이블에 기록) │
│ 4. 외부 시스템 호출 (ERP 연동) │
└─────────────────────────────────────────────────────────────────┘
```
**제어관리 설정**:
```json
{
"triggerType": "after",
"actions": [
{
"actionType": "update",
"targetTable": "order_master",
"conditions": [{ "field": "status", "operator": "=", "value": "대기" }],
"fieldMappings": [{ "targetField": "status", "defaultValue": "확정" }]
},
{
"actionType": "insert",
"targetTable": "order_history",
"fieldMappings": [
{ "sourceField": "order_no", "targetField": "order_no" },
{ "sourceField": "customer_name", "targetField": "customer_name" }
]
}
]
}
```
### 6.5 회사별 개발 시 제어관리 체크리스트
```
□ 버튼별 액션 정의
- 어떤 버튼이 있는가?
- 각 버튼 클릭 시 무슨 동작?
□ 저장/수정/삭제 대상 테이블
- 메인 테이블은?
- 이력 테이블은?
- 연관 테이블은?
□ 조건부 실행
- 특정 상태일 때만 실행?
- 특정 값 체크 필요?
□ 다중 행 처리
- 여러 행 선택 후 일괄 처리?
- 각 행별 개별 처리?
□ 외부 연동
- ERP/MES 등 외부 시스템 호출?
- API 연동 필요?
```
---
## 7. 회사별 커스터마이징 영역
### 7.1 컴포넌트로 처리되는 영역 (표준화)
| 영역 | 설명 |
|------|------|
| UI 레이아웃 | 컴포넌트 배치, 크기, 위치 |
| 검색 조건 | 화면 디자이너에서 설정 |
| 테이블 컬럼 | 표시/숨김, 순서, 너비 |
| 기본 CRUD | 조회, 저장, 삭제 자동 처리 |
| 페이지네이션 | 자동 처리 |
| 정렬/필터 | 자동 처리 |
### 7.2 회사별 개발 필요 영역
| 영역 | 설명 | 개발 방법 |
|------|------|----------|
| 비즈니스 로직 | 저장 전/후 검증, 계산 | 데이터플로우 또는 백엔드 API |
| 특수 UI | 간트, 트리, 차트 등 | 별도 컴포넌트 개발 |
| 외부 연동 | ERP, MES 등 연계 | 외부 호출 설정 |
| 리포트/인쇄 | 전표, 라벨 출력 | 리포트 컴포넌트 |
| 결재 프로세스 | 승인/반려 흐름 | 워크플로우 설정 |
---
## 8. 빠른 개발 가이드
### Step 1: 화면 분석
1. 어떤 테이블을 사용하는가?
2. 테이블 간 관계는?
3. 어떤 패턴인가? (A/B/C/D/E)
### Step 2: 컴포넌트 배치
1. 화면 디자이너에서 패턴에 맞는 컴포넌트 배치
2. 각 컴포넌트에 테이블/컬럼 설정
### Step 3: 연동 설정
1. 마스터-디테일 관계 설정 (FK)
2. 검색 조건 설정
3. 버튼 액션 설정
### Step 4: 테스트
1. 데이터 조회 확인
2. 마스터 선택 시 디테일 연동 확인
3. 저장/삭제 동작 확인
---
## 9. 요약
### V2 컴포넌트 커버리지
| 화면 유형 | 지원 여부 | 주요 컴포넌트 |
|-----------|----------|--------------|
| 기본 CRUD | ✅ 완전 | v2-table-list, v2-table-search-widget |
| 마스터-디테일 | ✅ 완전 | v2-split-panel-layout |
| 탭 화면 | ✅ 완전 | v2-tabs-widget |
| 카드 뷰 | ✅ 완전 | v2-card-display |
| 피벗 분석 | ✅ 완전 | v2-pivot-grid |
| 그룹화 테이블 | ✅ 지원 | v2-table-grouped |
| 타임라인/스케줄 | ✅ 지원 | v2-timeline-scheduler |
| 파일 업로드 | ✅ 지원 | v2-file-upload |
| 트리 뷰 | ❌ 미지원 | 개발 필요 |
### 개발 시 핵심 원칙
1. **테이블 먼저**: DB 테이블 구조 확인이 최우선
2. **패턴 판단**: 5가지 패턴 중 어디에 해당하는지 판단
3. **표준 조합**: 검증된 컴포넌트 조합 사용
4. **한계 인식**: 불가능한 UI는 조기에 식별하여 별도 개발 계획
5. **멀티테넌시**: 모든 테이블에 company_code 필터링 필수
6. **제어관리 필수**: UI 완성 후 버튼별 비즈니스 로직 설정 필수
### UI vs 제어 구분
| 영역 | 담당 | 설정 위치 |
|------|------|----------|
| 화면 레이아웃 | V2 컴포넌트 | 화면 디자이너 |
| 비즈니스 로직 | 제어관리 | dataflow_diagrams |
| 외부 연동 | 외부호출 설정 | external_call_configs |
**HTML에서 배낄 수 있는 것**: UI 구조만
**별도 설정 필요한 것**: 저장 테이블, 버튼 액션, 조건 처리, 다중 행 처리

View File

@ -0,0 +1,952 @@
# WACE 화면 시스템 - DB 스키마 & 컴포넌트 설정 전체 레퍼런스
> **최종 업데이트**: 2026-03-13
> **용도**: AI 챗봇이 화면 생성 시 참조하는 DB 스키마, 컴포넌트 전체 설정 사전
> **관련 문서**: `v2-component-usage-guide.md` (SQL 템플릿, 실행 예시)
---
## 1. 시스템 아키텍처
### 렌더링 파이프라인
```
[DB] screen_definitions + screen_layouts_v2
→ [Backend API] GET /api/screens/:screenId
→ [layoutV2Converter] V2 JSON → Legacy 변환 (기본값 + overrides 병합)
→ [ResponsiveGridRenderer] → DynamicComponentRenderer
→ [ComponentRegistry] → 실제 React 컴포넌트
```
### 테이블 관계도
```
비즈니스 테이블 ←── table_labels (라벨)
←── table_type_columns (컬럼 타입, company_code='*')
←── column_labels (한글 라벨)
screen_definitions ←── screen_layouts_v2 (layout_data JSON)
menu_info (메뉴 트리, menu_url → /screen/{screen_code})
[선택] dataflow_diagrams (비즈니스 로직)
[선택] numbering_rules + numbering_rule_parts (채번)
[선택] table_column_category_values (카테고리)
```
---
## 2. DB 테이블 스키마
### 2.1 비즈니스 테이블 필수 구조
> **[최우선 규칙] 비즈니스 테이블에 NOT NULL / UNIQUE 제약조건 절대 금지!**
>
> 멀티테넌시 환경에서 회사별로 필수값/유니크 규칙이 다를 수 있으므로,
> 제약조건은 DB 레벨이 아닌 **`table_type_columns`의 메타데이터(`is_nullable`, `is_unique`)로 논리적 제어**한다.
> DB에 직접 NOT NULL/UNIQUE/CHECK/FOREIGN KEY를 걸면 멀티테넌시가 깨진다.
>
> **허용**: `id` PRIMARY KEY, `DEFAULT` 값만 DB 레벨 설정
> **금지**: 비즈니스 컬럼에 `NOT NULL`, `UNIQUE`, `CHECK`, `FOREIGN KEY`
```sql
CREATE TABLE "{테이블명}" (
"id" varchar(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
"created_date" timestamp DEFAULT now(),
"updated_date" timestamp DEFAULT now(),
"writer" varchar(500) DEFAULT NULL,
"company_code" varchar(500),
-- 모든 비즈니스 컬럼은 varchar(500), NOT NULL/UNIQUE 제약조건 금지
);
```
### 2.2 table_labels
| 컬럼 | 타입 | 설명 |
|------|------|------|
| table_name | varchar PK | 테이블명 |
| table_label | varchar | 한글 라벨 |
| description | text | 설명 |
| use_log_table | varchar(1) | 'Y'/'N' |
### 2.3 table_type_columns
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | serial PK | 자동 증가 |
| table_name | varchar | UNIQUE(+column_name+company_code) |
| column_name | varchar | 컬럼명 |
| company_code | varchar | `'*'` = 전체 공통 |
| input_type | varchar | text/number/date/code/entity/select/checkbox/radio/textarea/category/numbering |
| detail_settings | text | JSON (code/entity/select 상세) |
| is_nullable | varchar | `'Y'`/`'N'` (논리적 필수값 제어) |
| display_order | integer | -5~-1: 기본, 0~: 비즈니스 |
| column_label | varchar | 컬럼 한글 라벨 |
| description | text | 컬럼 설명 |
| is_visible | boolean | 화면 표시 여부 (기본 true) |
| code_category | varchar | input_type=code일 때 코드 카테고리 |
| code_value | varchar | 코드 값 |
| reference_table | varchar | input_type=entity일 때 참조 테이블 |
| reference_column | varchar | 참조 컬럼 |
| display_column | varchar | 참조 표시 컬럼 |
| is_unique | varchar | `'Y'`/`'N'` (논리적 유니크 제어) |
| category_ref | varchar | 카테고리 참조 |
### 2.4 screen_definitions
| 컬럼 | 타입 | 설명 |
|------|------|------|
| screen_id | serial PK | 자동 증가 |
| screen_name | varchar NOT NULL | 화면명 |
| screen_code | varchar | **조건부 UNIQUE** (`WHERE is_active <> 'D'`) |
| table_name | varchar | 메인 테이블명 |
| company_code | varchar NOT NULL | 회사 코드 |
| description | text | 화면 설명 |
| is_active | char(1) | `'Y'`/`'N'`/`'D'` (D=삭제) |
| layout_metadata | jsonb | 레이아웃 메타데이터 |
| created_date | timestamp | 생성일시 |
| created_by | varchar | 생성자 |
| updated_date | timestamp | 수정일시 |
| updated_by | varchar | 수정자 |
| deleted_date | timestamp | 삭제일시 |
| deleted_by | varchar | 삭제자 |
| delete_reason | text | 삭제 사유 |
| db_source_type | varchar | `'internal'` (기본) / `'external'` |
| db_connection_id | integer | 외부 DB 연결 ID |
| data_source_type | varchar | `'database'` (기본) / `'rest_api'` |
| rest_api_connection_id | integer | REST API 연결 ID |
| rest_api_endpoint | varchar | REST API 엔드포인트 |
| rest_api_json_path | varchar | JSON 응답 경로 (기본 `'data'`) |
| source_screen_id | integer | 원본 화면 ID (복사본일 때) |
> **screen_code UNIQUE 주의**: `is_active = 'D'`(삭제)인 화면은 UNIQUE 대상에서 제외된다. 삭제된 화면과 같은 코드로 새 화면을 만들 수 있지만, 활성 상태(`'Y'`/`'N'`)에서는 중복 불가.
### 2.5 screen_layouts_v2
| 컬럼 | 타입 | 설명 |
|------|------|------|
| layout_id | serial PK | 자동 증가 |
| screen_id | integer FK | UNIQUE(+company_code+layer_id) |
| company_code | varchar NOT NULL | 회사 코드 |
| layout_data | jsonb NOT NULL | 전체 레이아웃 JSON (기본 `'{}'`) |
| created_at | timestamptz | 생성일시 |
| updated_at | timestamptz | 수정일시 |
| layer_id | integer | 1=기본 레이어 (기본값 1) |
| layer_name | varchar | 레이어명 (기본 `'기본 레이어'`) |
| condition_config | jsonb | 레이어 조건부 표시 설정 |
### 2.6 menu_info
| 컬럼 | 타입 | 설명 |
|------|------|------|
| objid | numeric PK | BIGINT 고유값 |
| menu_type | numeric | 0=화면, 1=폴더 |
| parent_obj_id | numeric | 부모 메뉴 objid |
| menu_name_kor | varchar | 메뉴명 (한글) |
| menu_name_eng | varchar | 메뉴명 (영문) |
| seq | numeric | 정렬 순서 |
| menu_url | varchar | `/screen/{screen_code}` |
| menu_desc | varchar | 메뉴 설명 |
| writer | varchar | 작성자 |
| regdate | timestamp | 등록일시 |
| status | varchar | 상태 (`'active'` 등) |
| company_code | varchar | 회사 코드 (기본 `'*'`) |
| screen_code | varchar | 연결 화면 코드 |
| system_name | varchar | 시스템명 |
| lang_key | varchar | 다국어 키 |
| lang_key_desc | varchar | 다국어 설명 키 |
| menu_code | varchar | 메뉴 코드 |
| source_menu_objid | bigint | 원본 메뉴 objid (복사본일 때) |
| screen_group_id | integer | 화면 그룹 ID |
| menu_icon | varchar | 메뉴 아이콘 |
---
## 3. 컴포넌트 전체 설정 레퍼런스 (32개)
> 아래 설정은 layout_data JSON의 각 컴포넌트 `overrides` 안에 들어가는 값이다.
> 기본값과 다른 부분만 overrides에 지정하면 된다.
---
### 3.1 v2-table-list (데이터 테이블)
**용도**: DB 테이블 데이터를 테이블/카드 형태로 조회/편집. 가장 핵심적인 컴포넌트.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| tableName | string | - | 조회할 DB 테이블명 |
| selectedTable | string | - | tableName 별칭 |
| displayMode | `"table"\|"card"` | `"table"` | 테이블 모드 또는 카드 모드 |
| autoLoad | boolean | `true` | 화면 로드 시 자동으로 데이터 조회 |
| isReadOnly | boolean | false | 읽기 전용 (편집 불가) |
| columns | ColumnConfig[] | `[]` | 표시할 컬럼 설정 배열 |
| title | string | - | 테이블 상단 제목 |
| showHeader | boolean | `true` | 테이블 헤더 행 표시 |
| showFooter | boolean | `true` | 테이블 푸터 표시 |
| height | string | `"auto"` | 높이 모드 (`"auto"`, `"fixed"`, `"viewport"`) |
| fixedHeight | number | - | height="fixed"일 때 고정 높이(px) |
| autoWidth | boolean | `true` | 컬럼 너비 자동 계산 |
| stickyHeader | boolean | `false` | 스크롤 시 헤더 고정 |
**checkbox (체크박스 설정)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| enabled | boolean | `true` | 체크박스 사용 여부 |
| multiple | boolean | `true` | 다중 선택 허용 |
| position | `"left"\|"right"` | `"left"` | 체크박스 위치 |
| selectAll | boolean | `true` | 전체 선택 버튼 표시 |
**pagination (페이지네이션)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| enabled | boolean | `true` | 페이지네이션 사용 |
| pageSize | number | `20` | 한 페이지당 행 수 |
| showSizeSelector | boolean | `true` | 페이지 크기 변경 드롭다운 |
| showPageInfo | boolean | `true` | "1-20 / 100건" 같은 정보 표시 |
| pageSizeOptions | number[] | `[10,20,50,100]` | 선택 가능한 페이지 크기 |
**horizontalScroll (가로 스크롤)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| enabled | boolean | `true` | 가로 스크롤 사용 |
| maxVisibleColumns | number | `8` | 스크롤 없이 보이는 최대 컬럼 수 |
| minColumnWidth | number | `100` | 컬럼 최소 너비(px) |
| maxColumnWidth | number | `300` | 컬럼 최대 너비(px) |
**tableStyle (스타일)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| theme | string | `"default"` | 테마 (`default`/`striped`/`bordered`/`minimal`) |
| headerStyle | string | `"default"` | 헤더 스타일 (`default`/`dark`/`light`) |
| rowHeight | string | `"normal"` | 행 높이 (`compact`/`normal`/`comfortable`) |
| alternateRows | boolean | `true` | 짝수/홀수 행 색상 교차 |
| hoverEffect | boolean | `true` | 마우스 호버 시 행 강조 |
| borderStyle | string | `"light"` | 테두리 (`none`/`light`/`heavy`) |
**toolbar (툴바 버튼)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| showEditMode | boolean | `false` | 즉시저장/배치저장 모드 전환 버튼 |
| showExcel | boolean | `false` | Excel 내보내기 버튼 |
| showPdf | boolean | `false` | PDF 내보내기 버튼 |
| showSearch | boolean | `false` | 테이블 내 검색 |
| showRefresh | boolean | `false` | 상단 새로고침 버튼 |
| showPaginationRefresh | boolean | `true` | 하단 새로고침 버튼 |
**filter (필터)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| enabled | boolean | `true` | 필터 기능 사용 |
| filters | array | `[]` | 사전 정의 필터 목록 |
**ColumnConfig (columns 배열 요소)**:
| 설정 | 타입 | 설명 |
|------|------|------|
| columnName | string | DB 컬럼명 |
| displayName | string | 화면 표시명 |
| visible | boolean | 표시 여부 |
| sortable | boolean | 정렬 가능 여부 |
| searchable | boolean | 검색 가능 여부 |
| editable | boolean | 인라인 편집 가능 여부 |
| width | number | 컬럼 너비(px) |
| align | `"left"\|"center"\|"right"` | 텍스트 정렬 |
| format | string | 포맷 (`text`/`number`/`date`/`currency`/`boolean`) |
| hidden | boolean | 숨김 (데이터는 로드하되 표시 안 함) |
| fixed | `"left"\|"right"\|false` | 컬럼 고정 위치 |
| thousandSeparator | boolean | 숫자 천 단위 콤마 |
| isEntityJoin | boolean | 엔티티 조인 사용 여부 |
| entityJoinInfo | object | 조인 정보 (`sourceTable`, `sourceColumn`, `referenceTable`, `joinAlias`) |
**cardConfig (displayMode="card"일 때)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| idColumn | string | `"id"` | ID 컬럼 |
| titleColumn | string | `"name"` | 카드 제목 컬럼 |
| subtitleColumn | string | - | 부제목 컬럼 |
| descriptionColumn | string | - | 설명 컬럼 |
| imageColumn | string | - | 이미지 URL 컬럼 |
| cardsPerRow | number | `3` | 행당 카드 수 |
| cardSpacing | number | `16` | 카드 간격(px) |
| showActions | boolean | `true` | 카드 액션 버튼 표시 |
---
### 3.2 v2-split-panel-layout (마스터-디테일 분할)
**용도**: 좌측 마스터 테이블 선택 → 우측 디테일 테이블 연동. 가장 복잡한 컴포넌트.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| splitRatio | number | `30` | 좌측 패널 비율(0~100) |
| resizable | boolean | `true` | 사용자가 분할선 드래그로 비율 변경 가능 |
| minLeftWidth | number | `200` | 좌측 최소 너비(px) |
| minRightWidth | number | `300` | 우측 최소 너비(px) |
| autoLoad | boolean | `true` | 화면 로드 시 자동 데이터 조회 |
| syncSelection | boolean | `true` | 좌측 선택 시 우측 자동 갱신 |
**leftPanel / rightPanel 공통 설정**:
| 설정 | 타입 | 설명 |
|------|------|------|
| title | string | 패널 제목 |
| tableName | string | DB 테이블명 |
| displayMode | `"list"\|"table"\|"custom"` | `list`: 리스트, `table`: 테이블, `custom`: 자유 배치 |
| columns | array | 컬럼 설정 (`name`, `label`, `width`, `sortable`, `align`, `isEntityJoin`, `joinInfo`) |
| showSearch | boolean | 패널 내 검색 바 표시 |
| showAdd | boolean | 추가 버튼 표시 |
| showEdit | boolean | 수정 버튼 표시 |
| showDelete | boolean | 삭제 버튼 표시 |
| addButton | object | `{ enabled, mode("auto"/"modal"), modalScreenId }` |
| editButton | object | `{ enabled, mode("auto"/"modal"), modalScreenId, buttonLabel }` |
| deleteButton | object | `{ enabled, buttonLabel, confirmMessage }` |
| addModalColumns | array | 추가 모달 전용 컬럼 (`name`, `label`, `required`) |
| dataFilter | object | `{ enabled, filters, matchType("all"/"any") }` |
| tableConfig | object | `{ showCheckbox, showRowNumber, rowHeight, headerHeight, striped, bordered, hoverable, stickyHeader }` |
| components | array | displayMode="custom"일 때 내부 컴포넌트 배열 |
**rightPanel 전용 설정**:
| 설정 | 타입 | 설명 |
|------|------|------|
| relation | object | 마스터-디테일 연결 관계 |
| relation.type | `"detail"\|"join"` | detail: FK 관계, join: 테이블 JOIN |
| relation.leftColumn | string | 좌측(마스터) 연결 컬럼 (보통 `"id"`) |
| relation.rightColumn | string | 우측(디테일) 연결 컬럼 (FK) |
| relation.foreignKey | string | FK 컬럼명 (rightColumn과 동일) |
| relation.keys | array | 복합키 `[{ leftColumn, rightColumn }]` |
| additionalTabs | array | 우측 패널에 탭 추가 (각 탭은 rightPanel과 동일 구조 + `tabId`, `label`) |
| addConfig | object | `{ targetTable, autoFillColumns, leftPanelColumn, targetColumn }` |
| deduplication | object | `{ enabled, groupByColumn, keepStrategy, sortColumn }` |
| summaryColumnCount | number | 요약 표시 컬럼 수 |
---
### 3.3 v2-table-search-widget (검색 바)
**용도**: 테이블 상단에 배치하여 검색/필터 기능 제공. 대상 테이블 컬럼을 자동 감지.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| autoSelectFirstTable | boolean | `true` | 화면 내 첫 번째 테이블 자동 연결 |
| showTableSelector | boolean | `true` | 테이블 선택 드롭다운 표시 |
| title | string | `"테이블 검색"` | 검색 바 제목 |
| filterMode | `"dynamic"\|"preset"` | `"dynamic"` | dynamic: 자동 필터, preset: 고정 필터 |
| presetFilters | array | `[]` | 고정 필터 목록 (`{ columnName, columnLabel, filterType, width }`) |
| targetPanelPosition | `"left"\|"right"\|"auto"` | `"left"` | split-panel에서 대상 패널 위치 |
---
### 3.4 v2-input (텍스트/숫자 입력)
**용도**: 텍스트, 숫자, 비밀번호, textarea, 슬라이더, 컬러, 버튼 등 단일 값 입력.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| inputType | string | `"text"` | 입력 유형: `text`/`number`/`password`/`slider`/`color`/`button`/`textarea` |
| format | string | `"none"` | 포맷 검증: `none`/`email`/`tel`/`url`/`currency`/`biz_no` |
| placeholder | string | `""` | 입력 힌트 텍스트 |
| required | boolean | `false` | 필수 입력 표시 |
| readonly | boolean | `false` | 읽기 전용 |
| disabled | boolean | `false` | 비활성화 |
| maxLength | number | - | 최대 입력 글자 수 |
| minLength | number | - | 최소 입력 글자 수 |
| pattern | string | - | 정규식 패턴 검증 |
| showCounter | boolean | `false` | 글자 수 카운터 표시 |
| min | number | - | 최소값 (number/slider) |
| max | number | - | 최대값 (number/slider) |
| step | number | - | 증감 단위 (number/slider) |
| buttonText | string | - | 버튼 텍스트 (inputType=button) |
| tableName | string | - | 바인딩 테이블명 |
| columnName | string | - | 바인딩 컬럼명 |
---
### 3.5 v2-select (선택)
**용도**: 드롭다운, 콤보박스, 라디오, 체크박스, 태그, 토글 등 선택형 입력.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| mode | string | `"dropdown"` | 선택 모드: `dropdown`/`combobox`/`radio`/`check`/`tag`/`tagbox`/`toggle`/`swap` |
| source | string | `"distinct"` | 데이터 소스: `static`/`code`/`db`/`api`/`entity`/`category`/`distinct`/`select` |
| options | array | `[]` | source=static일 때 옵션 목록 `[{ label, value }]` |
| codeGroup | string | - | source=code일 때 코드 그룹 |
| codeCategory | string | - | source=code일 때 코드 카테고리 |
| table | string | - | source=db일 때 테이블명 |
| valueColumn | string | - | source=db일 때 값 컬럼 |
| labelColumn | string | - | source=db일 때 표시 컬럼 |
| entityTable | string | - | source=entity일 때 엔티티 테이블 |
| entityValueField | string | - | source=entity일 때 값 필드 |
| entityLabelField | string | - | source=entity일 때 표시 필드 |
| searchable | boolean | `true` | 검색 가능 (combobox에서 기본 활성) |
| multiple | boolean | `false` | 다중 선택 허용 |
| maxSelect | number | - | 최대 선택 수 |
| allowClear | boolean | - | 선택 해제 허용 |
| placeholder | string | `"선택하세요"` | 힌트 텍스트 |
| required | boolean | `false` | 필수 선택 |
| readonly | boolean | `false` | 읽기 전용 |
| disabled | boolean | `false` | 비활성화 |
| cascading | object | - | 연쇄 선택 (상위 select 값에 따라 하위 옵션 변경) |
| hierarchical | boolean | - | 계층 구조 (부모-자식 관계) |
| parentField | string | - | 부모 필드명 |
---
### 3.6 v2-date (날짜)
**용도**: 날짜, 시간, 날짜시간, 날짜범위, 월, 연도 입력.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dateType | string | `"date"` | 날짜 유형: `date`/`datetime`/`time`/`daterange`/`month`/`year` |
| format | string | `"YYYY-MM-DD"` | 표시/저장 형식 |
| placeholder | string | `"날짜 선택"` | 힌트 텍스트 |
| required | boolean | `false` | 필수 입력 |
| readonly | boolean | `false` | 읽기 전용 |
| disabled | boolean | `false` | 비활성화 |
| showTime | boolean | `false` | 시간 선택 표시 (datetime) |
| use24Hours | boolean | `true` | 24시간 형식 |
| range | boolean | - | 범위 선택 (시작~종료) |
| minDate | string | - | 선택 가능 최소 날짜 (ISO 8601) |
| maxDate | string | - | 선택 가능 최대 날짜 |
| showToday | boolean | - | 오늘 버튼 표시 |
---
### 3.7 v2-button-primary (액션 버튼)
**용도**: 저장, 삭제, 조회, 커스텀 등 액션 버튼. 제어관리(dataflow)와 연결 가능.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| text | string | `"저장"` | 버튼 텍스트 |
| actionType | string | `"button"` | 버튼 타입: `button`/`submit`/`reset` |
| variant | string | `"primary"` | 스타일: `primary`/`secondary`/`danger` |
| size | string | `"md"` | 크기: `sm`/`md`/`lg` |
| disabled | boolean | `false` | 비활성화 |
| action | object | - | 액션 설정 |
| action.type | string | `"save"` | 액션 유형: `save`/`delete`/`edit`/`copy`/`navigate`/`modal`/`control`/`custom` |
| action.successMessage | string | `"저장되었습니다."` | 성공 시 토스트 메시지 |
| action.errorMessage | string | `"오류가 발생했습니다."` | 실패 시 토스트 메시지 |
| webTypeConfig | object | - | 제어관리 연결 설정 |
| webTypeConfig.enableDataflowControl | boolean | - | 제어관리 활성화 |
| webTypeConfig.dataflowConfig | object | - | 제어관리 설정 |
| webTypeConfig.dataflowConfig.controlMode | string | - | `"relationship"`/`"flow"`/`"none"` |
| webTypeConfig.dataflowConfig.relationshipConfig | object | - | `{ relationshipId, executionTiming("before"/"after"/"replace") }` |
| webTypeConfig.dataflowConfig.flowConfig | object | - | `{ flowId, executionTiming }` |
---
### 3.8 v2-table-grouped (그룹화 테이블)
**용도**: 특정 컬럼 기준으로 데이터를 그룹화. 그룹별 접기/펼치기, 집계 표시.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| selectedTable | string | `""` | DB 테이블명 |
| columns | array | `[]` | 컬럼 설정 (v2-table-list와 동일) |
| showCheckbox | boolean | `false` | 체크박스 표시 |
| checkboxMode | `"single"\|"multi"` | `"multi"` | 체크박스 모드 |
| isReadOnly | boolean | `false` | 읽기 전용 |
| rowClickable | boolean | `true` | 행 클릭 가능 |
| showExpandAllButton | boolean | `true` | 전체 펼치기/접기 버튼 |
| groupHeaderStyle | string | `"default"` | 그룹 헤더 스타일 (`default`/`compact`/`card`) |
| emptyMessage | string | `"데이터가 없습니다."` | 빈 데이터 메시지 |
| height | string\|number | `"auto"` | 높이 |
| maxHeight | number | `600` | 최대 높이(px) |
| pagination.enabled | boolean | `false` | 페이지네이션 사용 |
| pagination.pageSize | number | `10` | 페이지 크기 |
**groupConfig (그룹화 설정)**:
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| groupByColumn | string | `""` | **필수**. 그룹화 기준 컬럼 |
| groupLabelFormat | string | `"{value}"` | 그룹 라벨 포맷 |
| defaultExpanded | boolean | `true` | 초기 펼침 여부 |
| sortDirection | `"asc"\|"desc"` | `"asc"` | 그룹 정렬 방향 |
| summary.showCount | boolean | `true` | 그룹별 건수 표시 |
| summary.sumColumns | string[] | `[]` | 합계 표시할 컬럼 목록 |
| summary.avgColumns | string[] | - | 평균 표시 컬럼 |
| summary.maxColumns | string[] | - | 최대값 표시 컬럼 |
| summary.minColumns | string[] | - | 최소값 표시 컬럼 |
---
### 3.9 v2-pivot-grid (피벗 분석)
**용도**: 다차원 데이터 분석. 행/열/데이터/필터 영역에 필드를 배치하여 집계.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| fields | array | `[]` | **필수**. 피벗 필드 배열 |
| dataSource | object | - | 데이터 소스 (`type`, `tableName`, `joinConfigs`, `filterConditions`) |
| allowSortingBySummary | boolean | - | 집계값 기준 정렬 허용 |
| allowFiltering | boolean | - | 필터링 허용 |
| allowExpandAll | boolean | - | 전체 확장/축소 허용 |
| wordWrapEnabled | boolean | - | 텍스트 줄바꿈 |
| height | string\|number | - | 높이 |
| totals.showRowGrandTotals | boolean | - | 행 총합계 표시 |
| totals.showColumnGrandTotals | boolean | - | 열 총합계 표시 |
| chart.enabled | boolean | - | 차트 연동 표시 |
| chart.type | string | - | 차트 타입 (`bar`/`line`/`area`/`pie`/`stackedBar`) |
**fields 배열 요소**:
| 설정 | 타입 | 설명 |
|------|------|------|
| field | string | DB 컬럼명 |
| caption | string | 표시 라벨 |
| area | `"row"\|"column"\|"data"\|"filter"` | **필수**. 배치 영역 |
| summaryType | string | area=data일 때: `sum`/`count`/`avg`/`min`/`max`/`countDistinct` |
| groupInterval | string | 날짜 그룹화: `year`/`quarter`/`month`/`week`/`day` |
| sortBy | string | 정렬 기준: `value`/`caption` |
| sortOrder | string | 정렬 방향: `asc`/`desc`/`none` |
---
### 3.10 v2-card-display (카드 뷰)
**용도**: 테이블 데이터를 카드 형태로 표시. 이미지+제목+설명 구조.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSource | string | `"table"` | 데이터 소스: `table`/`static` |
| tableName | string | - | DB 테이블명 |
| cardsPerRow | number | `3` | 행당 카드 수 (1~6) |
| cardSpacing | number | `16` | 카드 간격(px) |
| columnMapping | object | `{}` | 필드 매핑 (`title`, `subtitle`, `description`, `image`, `status`) |
| cardStyle.showTitle | boolean | `true` | 제목 표시 |
| cardStyle.showSubtitle | boolean | `true` | 부제목 표시 |
| cardStyle.showDescription | boolean | `true` | 설명 표시 |
| cardStyle.showImage | boolean | `false` | 이미지 표시 |
| cardStyle.showActions | boolean | `true` | 액션 버튼 표시 |
---
### 3.11 v2-timeline-scheduler (간트차트)
**용도**: 시간축 기반 일정/계획 시각화. 드래그/리사이즈로 일정 편집.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| selectedTable | string | - | 스케줄 데이터 테이블 |
| resourceTable | string | `"equipment_mng"` | 리소스(설비/작업자) 테이블 |
| scheduleType | string | `"PRODUCTION"` | 스케줄 유형: `PRODUCTION`/`MAINTENANCE`/`SHIPPING`/`WORK_ASSIGN` |
| defaultZoomLevel | string | `"day"` | 초기 줌: `day`/`week`/`month` |
| editable | boolean | `true` | 편집 가능 |
| draggable | boolean | `true` | 드래그 이동 허용 |
| resizable | boolean | `true` | 기간 리사이즈 허용 |
| rowHeight | number | `50` | 행 높이(px) |
| headerHeight | number | `60` | 헤더 높이(px) |
| resourceColumnWidth | number | `150` | 리소스 컬럼 너비(px) |
| cellWidth.day | number | `60` | 일 단위 셀 너비 |
| cellWidth.week | number | `120` | 주 단위 셀 너비 |
| cellWidth.month | number | `40` | 월 단위 셀 너비 |
| showConflicts | boolean | `true` | 시간 겹침 충돌 표시 |
| showProgress | boolean | `true` | 진행률 바 표시 |
| showTodayLine | boolean | `true` | 오늘 날짜 표시선 |
| showToolbar | boolean | `true` | 상단 툴바 표시 |
| showAddButton | boolean | `true` | 추가 버튼 |
| height | number | `500` | 높이(px) |
**fieldMapping (필수)**:
| 설정 | 기본값 | 설명 |
|------|--------|------|
| id | `"schedule_id"` | 스케줄 PK 필드 |
| resourceId | `"resource_id"` | 리소스 FK 필드 |
| title | `"schedule_name"` | 제목 필드 |
| startDate | `"start_date"` | 시작일 필드 |
| endDate | `"end_date"` | 종료일 필드 |
| status | - | 상태 필드 |
| progress | - | 진행률 필드 (0~100) |
**resourceFieldMapping**:
| 설정 | 기본값 | 설명 |
|------|--------|------|
| id | `"equipment_code"` | 리소스 PK |
| name | `"equipment_name"` | 리소스 표시명 |
| group | - | 리소스 그룹 |
**statusColors (상태별 색상)**:
| 상태 | 기본 색상 |
|------|----------|
| planned | `"#3b82f6"` (파랑) |
| in_progress | `"#f59e0b"` (주황) |
| completed | `"#10b981"` (초록) |
| delayed | `"#ef4444"` (빨강) |
| cancelled | `"#6b7280"` (회색) |
---
### 3.12 v2-tabs-widget (탭)
**용도**: 탭 전환. 각 탭 내부에 컴포넌트 배치 가능.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| tabs | array | `[{id:"tab-1",label:"탭1",...}]` | 탭 배열 |
| defaultTab | string | `"tab-1"` | 기본 활성 탭 ID |
| orientation | string | `"horizontal"` | 탭 방향: `horizontal`/`vertical` |
| variant | string | `"default"` | 스타일: `default`/`pills`/`underline` |
| allowCloseable | boolean | `false` | 탭 닫기 버튼 표시 |
| persistSelection | boolean | `false` | 탭 선택 상태 localStorage 저장 |
**tabs 배열 요소**: `{ id, label, order, disabled, icon, components[] }`
**components 요소**: `{ id, componentType, label, position, size, componentConfig }`
---
### 3.13 v2-aggregation-widget (집계 카드)
**용도**: 합계, 평균, 개수 등 집계값을 카드 형태로 표시.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSourceType | string | `"table"` | 데이터 소스: `table`/`component`/`selection` |
| tableName | string | - | 테이블명 |
| items | array | `[]` | 집계 항목 배열 |
| layout | string | `"horizontal"` | 배치: `horizontal`/`vertical` |
| showLabels | boolean | `true` | 라벨 표시 |
| showIcons | boolean | `true` | 아이콘 표시 |
| gap | string | `"16px"` | 항목 간격 |
| autoRefresh | boolean | `false` | 자동 새로고침 |
| refreshOnFormChange | boolean | `true` | 폼 변경 시 새로고침 |
**items 요소**: `{ id, columnName, columnLabel, type("sum"/"avg"/"count"/"max"/"min"), format, decimalPlaces, prefix, suffix }`
---
### 3.14 v2-status-count (상태별 건수)
**용도**: 상태별 건수를 카드 형태로 표시. 대시보드/현황 화면용.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| title | string | `"상태 현황"` | 제목 |
| tableName | string | `""` | 대상 테이블 |
| statusColumn | string | `"status"` | 상태 컬럼명 |
| relationColumn | string | `""` | 관계 컬럼 (필터용) |
| items | array | - | 상태 항목 `[{ value, label, color }]` |
| showTotal | boolean | - | 합계 표시 |
| cardSize | string | `"md"` | 카드 크기: `sm`/`md`/`lg` |
---
### 3.15 v2-text-display (텍스트 표시)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| text | string | `"텍스트를 입력하세요"` | 표시 텍스트 |
| fontSize | string | `"14px"` | 폰트 크기 |
| fontWeight | string | `"normal"` | 폰트 굵기 |
| color | string | `"#212121"` | 텍스트 색상 |
| textAlign | string | `"left"` | 정렬: `left`/`center`/`right` |
| backgroundColor | string | - | 배경색 |
| padding | string | - | 패딩 |
---
### 3.16 v2-numbering-rule (자동 채번)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| ruleConfig | object | - | 채번 규칙 설정 |
| maxRules | number | `6` | 최대 파트 수 |
| readonly | boolean | `false` | 읽기 전용 |
| showPreview | boolean | `true` | 미리보기 표시 |
| showRuleList | boolean | `true` | 규칙 목록 표시 |
| cardLayout | string | `"vertical"` | 레이아웃: `vertical`/`horizontal` |
---
### 3.17 v2-file-upload (파일 업로드)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| placeholder | string | `"파일을 선택하세요"` | 힌트 텍스트 |
| multiple | boolean | `true` | 다중 업로드 |
| accept | string | `"*/*"` | 허용 파일 형식 (예: `"image/*"`, `".pdf,.xlsx"`) |
| maxSize | number | `10485760` | 최대 파일 크기(bytes, 기본 10MB) |
| maxFiles | number | - | 최대 파일 수 |
| showPreview | boolean | - | 미리보기 표시 |
| showFileList | boolean | - | 파일 목록 표시 |
| allowDelete | boolean | - | 삭제 허용 |
| allowDownload | boolean | - | 다운로드 허용 |
---
### 3.18 v2-section-card (그룹 컨테이너 - 테두리)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| title | string | `"섹션 제목"` | 제목 |
| description | string | `""` | 설명 |
| showHeader | boolean | `true` | 헤더 표시 |
| padding | string | `"md"` | 패딩: `none`/`sm`/`md`/`lg` |
| backgroundColor | string | `"default"` | 배경: `default`/`muted`/`transparent` |
| borderStyle | string | `"solid"` | 테두리: `solid`/`dashed`/`none` |
| collapsible | boolean | `false` | 접기/펼치기 가능 |
| defaultOpen | boolean | `true` | 기본 펼침 |
---
### 3.19 v2-section-paper (그룹 컨테이너 - 배경색)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| backgroundColor | string | `"default"` | 배경: `default`/`muted`/`accent`/`primary`/`custom` |
| customColor | string | - | custom일 때 색상 |
| showBorder | boolean | `false` | 테두리 표시 |
| padding | string | `"md"` | 패딩: `none`/`sm`/`md`/`lg` |
| roundedCorners | string | `"md"` | 모서리: `none`/`sm`/`md`/`lg` |
| shadow | string | `"none"` | 그림자: `none`/`sm`/`md` |
---
### 3.20 v2-divider-line (구분선)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| orientation | string | - | 방향 (가로/세로) |
| thickness | number | - | 두께 |
---
### 3.21 v2-split-line (캔버스 분할선)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| resizable | boolean | `true` | 드래그 리사이즈 허용 |
| lineColor | string | `"#e2e8f0"` | 분할선 색상 |
| lineWidth | number | `4` | 분할선 두께(px) |
---
### 3.22 v2-repeat-container (반복 렌더링)
**용도**: 데이터 수만큼 내부 컴포넌트를 반복 렌더링. 카드 리스트 등에 사용.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSourceType | string | `"manual"` | 소스: `table-list`/`v2-repeater`/`externalData`/`manual` |
| dataSourceComponentId | string | - | 연결할 컴포넌트 ID |
| tableName | string | - | 테이블명 |
| layout | string | `"vertical"` | 배치: `vertical`/`horizontal`/`grid` |
| gridColumns | number | `2` | grid일 때 컬럼 수 |
| gap | string | `"16px"` | 아이템 간격 |
| showBorder | boolean | `true` | 카드 테두리 |
| showShadow | boolean | `false` | 카드 그림자 |
| borderRadius | string | `"8px"` | 모서리 둥글기 |
| backgroundColor | string | `"#ffffff"` | 배경색 |
| padding | string | `"16px"` | 패딩 |
| showItemTitle | boolean | `false` | 아이템 제목 표시 |
| itemTitleTemplate | string | `""` | 제목 템플릿 (예: `"{order_no} - {item}"`) |
| emptyMessage | string | `"데이터가 없습니다"` | 빈 상태 메시지 |
| clickable | boolean | `false` | 클릭 가능 |
| selectionMode | string | `"single"` | 선택 모드: `single`/`multiple` |
| usePaging | boolean | `false` | 페이징 사용 |
| pageSize | number | `10` | 페이지 크기 |
---
### 3.23 v2-repeater (반복 데이터 관리)
**용도**: 인라인/모달 모드로 반복 데이터(주문 상세 등) 관리. 행 추가/삭제/편집.
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| renderMode | string | `"inline"` | 모드: `inline` (인라인 편집) / `modal` (모달로 선택 추가) |
| mainTableName | string | - | 저장 대상 테이블 |
| foreignKeyColumn | string | - | 마스터 연결 FK 컬럼 |
| foreignKeySourceColumn | string | - | 마스터 PK 컬럼 |
| columns | array | `[]` | 컬럼 설정 |
| dataSource.tableName | string | - | 데이터 테이블 |
| dataSource.foreignKey | string | - | FK 컬럼 |
| dataSource.sourceTable | string | - | 모달용 소스 테이블 |
| modal.size | string | `"md"` | 모달 크기: `sm`/`md`/`lg`/`xl`/`full` |
| modal.title | string | - | 모달 제목 |
| modal.searchFields | string[] | - | 검색 필드 |
| features.showAddButton | boolean | `true` | 추가 버튼 |
| features.showDeleteButton | boolean | `true` | 삭제 버튼 |
| features.inlineEdit | boolean | `false` | 인라인 편집 |
| features.showRowNumber | boolean | `false` | 행 번호 표시 |
| calculationRules | array | - | 자동 계산 규칙 (예: 수량*단가=금액) |
---
### 3.24 v2-approval-step (결재 스테퍼)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| targetTable | string | `""` | 결재 대상 테이블 |
| targetRecordIdField | string | `""` | 레코드 ID 필드 |
| displayMode | string | `"horizontal"` | 표시 방향: `horizontal`/`vertical` |
| showComment | boolean | `true` | 결재 코멘트 표시 |
| showTimestamp | boolean | `true` | 결재 시간 표시 |
| showDept | boolean | `true` | 부서 표시 |
| compact | boolean | `false` | 컴팩트 모드 |
---
### 3.25 v2-bom-tree (BOM 트리)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| detailTable | string | `"bom_detail"` | BOM 디테일 테이블 |
| foreignKey | string | `"bom_id"` | BOM 마스터 FK |
| parentKey | string | `"parent_detail_id"` | 트리 부모 키 (자기참조) |
---
### 3.26 v2-bom-item-editor (BOM 편집)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| detailTable | string | `"bom_detail"` | BOM 디테일 테이블 |
| sourceTable | string | `"item_info"` | 품목 소스 테이블 |
| foreignKey | string | `"bom_id"` | BOM 마스터 FK |
| parentKey | string | `"parent_detail_id"` | 트리 부모 키 |
| itemCodeField | string | `"item_number"` | 품목 코드 필드 |
| itemNameField | string | `"item_name"` | 품목명 필드 |
| itemTypeField | string | `"type"` | 품목 유형 필드 |
| itemUnitField | string | `"unit"` | 품목 단위 필드 |
---
### 3.27 v2-category-manager (카테고리 관리)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| tableName | string | - | 대상 테이블 |
| columnName | string | - | 카테고리 컬럼 |
| menuObjid | number | - | 연결 메뉴 OBJID |
| viewMode | string | `"tree"` | 뷰 모드: `tree`/`list` |
| showViewModeToggle | boolean | `true` | 뷰 모드 토글 표시 |
| defaultExpandLevel | number | `1` | 기본 트리 펼침 레벨 |
| showInactiveItems | boolean | `false` | 비활성 항목 표시 |
| leftPanelWidth | number | `15` | 좌측 패널 너비 |
---
### 3.28 v2-media (미디어)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| mediaType | string | `"file"` | 미디어 타입: `file`/`image`/`video`/`audio` |
| multiple | boolean | `false` | 다중 업로드 |
| preview | boolean | `true` | 미리보기 |
| maxSize | number | `10` | 최대 크기(MB) |
| accept | string | `"*/*"` | 허용 형식 |
| showFileList | boolean | `true` | 파일 목록 |
| dragDrop | boolean | `true` | 드래그앤드롭 |
---
### 3.29 v2-location-swap-selector (위치 교환)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSource.type | string | `"static"` | 소스: `static`/`table`/`code` |
| dataSource.tableName | string | - | 장소 테이블 |
| dataSource.valueField | string | `"location_code"` | 값 필드 |
| dataSource.labelField | string | `"location_name"` | 표시 필드 |
| dataSource.staticOptions | array | - | 정적 옵션 `[{value, label}]` |
| departureField | string | `"departure"` | 출발지 저장 필드 |
| destinationField | string | `"destination"` | 도착지 저장 필드 |
| departureLabel | string | `"출발지"` | 출발지 라벨 |
| destinationLabel | string | `"도착지"` | 도착지 라벨 |
| showSwapButton | boolean | `true` | 교환 버튼 표시 |
| variant | string | `"card"` | UI: `card`/`inline`/`minimal` |
---
### 3.30 v2-rack-structure (창고 랙)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| maxConditions | number | `10` | 최대 조건 수 |
| maxRows | number | `99` | 최대 열 수 |
| maxLevels | number | `20` | 최대 단 수 |
| codePattern | string | `"{warehouseCode}-{floor}{zone}-{row:02d}-{level}"` | 위치 코드 패턴 |
| namePattern | string | `"{zone}구역-{row:02d}열-{level}단"` | 위치 이름 패턴 |
| showTemplates | boolean | `true` | 템플릿 표시 |
| showPreview | boolean | `true` | 미리보기 |
| showStatistics | boolean | `true` | 통계 카드 |
| readonly | boolean | `false` | 읽기 전용 |
---
### 3.31 v2-process-work-standard (공정 작업기준)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSource.itemTable | string | `"item_info"` | 품목 테이블 |
| dataSource.routingVersionTable | string | `"item_routing_version"` | 라우팅 버전 테이블 |
| dataSource.routingDetailTable | string | `"item_routing_detail"` | 라우팅 디테일 테이블 |
| dataSource.processTable | string | `"process_mng"` | 공정 테이블 |
| splitRatio | number | `30` | 좌우 분할 비율 |
| leftPanelTitle | string | `"품목 및 공정 선택"` | 좌측 패널 제목 |
| readonly | boolean | `false` | 읽기 전용 |
| itemListMode | string | `"all"` | 품목 모드: `all`/`registered` |
---
### 3.32 v2-item-routing (품목 라우팅)
| 설정 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| dataSource.itemTable | string | `"item_info"` | 품목 테이블 |
| dataSource.routingVersionTable | string | `"item_routing_version"` | 라우팅 버전 테이블 |
| dataSource.routingDetailTable | string | `"item_routing_detail"` | 라우팅 디테일 테이블 |
| dataSource.processTable | string | `"process_mng"` | 공정 테이블 |
| splitRatio | number | `40` | 좌우 분할 비율 |
| leftPanelTitle | string | `"품목 목록"` | 좌측 제목 |
| rightPanelTitle | string | `"공정 순서"` | 우측 제목 |
| readonly | boolean | `false` | 읽기 전용 |
| autoSelectFirstVersion | boolean | `true` | 첫 버전 자동 선택 |
| itemListMode | string | `"all"` | 품목 모드: `all`/`registered` |
---
## 4. 패턴 의사결정 트리
```
Q1. 시간축 기반 일정/간트차트? → v2-timeline-scheduler
Q2. 다차원 피벗 분석? → v2-pivot-grid
Q3. 그룹별 접기/펼치기? → v2-table-grouped
Q4. 카드 형태 표시? → v2-card-display
Q5. 마스터-디테일?
├ 우측 멀티 탭? → v2-split-panel-layout + additionalTabs
└ 단일 디테일? → v2-split-panel-layout
Q6. 단일 테이블? → v2-table-search-widget + v2-table-list
```
---
## 5. 관계(relation) 레퍼런스
| 관계 유형 | 설정 |
|----------|------|
| 단순 FK | `{ type:"detail", leftColumn:"id", rightColumn:"{FK}", foreignKey:"{FK}" }` |
| 복합 키 | `{ type:"detail", keys:[{ leftColumn:"a", rightColumn:"b" }] }` |
| JOIN | `{ type:"join", leftColumn:"{col}", rightColumn:"{col}" }` |
## 6. 엔티티 조인
FK 컬럼에 참조 테이블의 이름을 표시:
**table_type_columns**: `input_type='entity'`, `detail_settings='{"referenceTable":"X","referenceColumn":"id","displayColumn":"name"}'`
**layout_data columns**: `{ name:"fk_col", isEntityJoin:true, joinInfo:{ sourceTable:"A", sourceColumn:"fk_col", referenceTable:"X", joinAlias:"name" } }`

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

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

View File

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

View File

@ -17,25 +17,37 @@ interface ProcessWorkStandardComponentProps {
formData?: Record<string, any>; formData?: Record<string, any>;
isPreview?: boolean; isPreview?: boolean;
tableName?: string; tableName?: string;
screenId?: number | string;
} }
export function ProcessWorkStandardComponent({ export function ProcessWorkStandardComponent({
config: configProp, config: configProp,
isPreview, isPreview,
screenId,
}: ProcessWorkStandardComponentProps) { }: ProcessWorkStandardComponentProps) {
const resolvedConfig = useMemo(() => {
const merged = {
...configProp,
};
if (merged.itemListMode === "registered" && !merged.screenCode && screenId) {
merged.screenCode = `screen_${screenId}`;
}
return merged;
}, [configProp, screenId]);
const config: ProcessWorkStandardConfig = useMemo( const config: ProcessWorkStandardConfig = useMemo(
() => ({ () => ({
...defaultConfig, ...defaultConfig,
...configProp, ...resolvedConfig,
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource }, dataSource: { ...defaultConfig.dataSource, ...resolvedConfig?.dataSource },
phases: configProp?.phases?.length phases: resolvedConfig?.phases?.length
? configProp.phases ? resolvedConfig.phases
: defaultConfig.phases, : defaultConfig.phases,
detailTypes: configProp?.detailTypes?.length detailTypes: resolvedConfig?.detailTypes?.length
? configProp.detailTypes ? resolvedConfig.detailTypes
: defaultConfig.detailTypes, : defaultConfig.detailTypes,
}), }),
[configProp] [resolvedConfig]
); );
const { const {
@ -46,7 +58,8 @@ export function ProcessWorkStandardComponent({
selectedDetailsByPhase, selectedDetailsByPhase,
selection, selection,
loading, loading,
fetchItems, isRegisteredMode,
loadItems,
selectItem, selectItem,
selectProcess, selectProcess,
fetchWorkItemDetails, fetchWorkItemDetails,
@ -112,8 +125,8 @@ export function ProcessWorkStandardComponent({
); );
const handleInit = useCallback(() => { const handleInit = useCallback(() => {
fetchItems(); loadItems();
}, [fetchItems]); }, [loadItems]);
const splitRatio = config.splitRatio || 30; const splitRatio = config.splitRatio || 30;
@ -144,7 +157,7 @@ export function ProcessWorkStandardComponent({
items={items} items={items}
routings={routings} routings={routings}
selection={selection} selection={selection}
onSearch={(keyword) => fetchItems(keyword)} onSearch={(keyword) => loadItems(keyword)}
onSelectItem={selectItem} onSelectItem={selectItem}
onSelectProcess={selectProcess} onSelectProcess={selectProcess}
onInit={handleInit} onInit={handleInit}

View File

@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ProcessWorkStandardConfig, WorkPhaseDefinition, DetailTypeDefinition } from "./types"; import { ProcessWorkStandardConfig, WorkPhaseDefinition, DetailTypeDefinition } from "./types";
import { defaultConfig } from "./config"; import { defaultConfig } from "./config";
@ -81,6 +82,30 @@ export function ProcessWorkStandardConfigPanel({
<div className="space-y-5 p-4"> <div className="space-y-5 p-4">
<h3 className="text-sm font-semibold"> </h3> <h3 className="text-sm font-semibold"> </h3>
{/* 품목 목록 모드 */}
<section className="space-y-3">
<p className="text-xs font-medium text-muted-foreground"> </p>
<div>
<Select
value={config.itemListMode || "all"}
onValueChange={(v) => update({ itemListMode: v as "all" | "registered" })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
<SelectItem value="registered"> </SelectItem>
</SelectContent>
</Select>
<p className="mt-1 text-[10px] text-muted-foreground">
{config.itemListMode === "registered"
? "품목별 라우팅 탭에서 등록한 품목만 표시됩니다. screenCode는 화면 ID 기준으로 자동 설정됩니다."
: "모든 품목을 표시합니다."}
</p>
</div>
</section>
{/* 데이터 소스 설정 */} {/* 데이터 소스 설정 */}
<section className="space-y-3"> <section className="space-y-3">
<p className="text-xs font-medium text-muted-foreground"> </p> <p className="text-xs font-medium text-muted-foreground"> </p>

View File

@ -9,7 +9,7 @@ export class ProcessWorkStandardRenderer extends AutoRegisteringComponentRendere
static componentDefinition = V2ProcessWorkStandardDefinition; static componentDefinition = V2ProcessWorkStandardDefinition;
render(): React.ReactElement { render(): React.ReactElement {
const { formData, isPreview, config, tableName } = this.props as Record< const { formData, isPreview, config, tableName, screenId } = this.props as Record<
string, string,
unknown unknown
>; >;
@ -20,6 +20,7 @@ export class ProcessWorkStandardRenderer extends AutoRegisteringComponentRendere
formData={formData as Record<string, unknown>} formData={formData as Record<string, unknown>}
tableName={tableName as string} tableName={tableName as string}
isPreview={isPreview as boolean} isPreview={isPreview as boolean}
screenId={screenId as number | string}
/> />
); );
} }

View File

@ -31,4 +31,6 @@ export const defaultConfig: ProcessWorkStandardConfig = {
splitRatio: 30, splitRatio: 30,
leftPanelTitle: "품목 및 공정 선택", leftPanelTitle: "품목 및 공정 선택",
readonly: false, readonly: false,
itemListMode: "all",
screenCode: "",
}; };

View File

@ -32,7 +32,9 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
processName: null, processName: null,
}); });
// 품목 목록 조회 const isRegisteredMode = config.itemListMode === "registered";
// 품목 목록 조회 (전체 모드)
const fetchItems = useCallback( const fetchItems = useCallback(
async (search?: string) => { async (search?: string) => {
try { try {
@ -59,6 +61,53 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
[config.dataSource] [config.dataSource]
); );
// 등록 품목 조회 (등록 모드)
const fetchRegisteredItems = useCallback(
async (search?: string) => {
const screenCode = config.screenCode;
if (!screenCode) {
console.warn("screenCode가 설정되지 않았습니다");
setItems([]);
return;
}
try {
setLoading(true);
const ds = config.dataSource;
const params = new URLSearchParams({
tableName: ds.itemTable,
nameColumn: ds.itemNameColumn,
codeColumn: ds.itemCodeColumn,
routingTable: ds.routingVersionTable,
routingFkColumn: ds.routingFkColumn,
...(search ? { search } : {}),
});
const res = await apiClient.get(
`${API_BASE}/registered-items/${encodeURIComponent(screenCode)}?${params}`
);
if (res.data?.success) {
setItems(res.data.data || []);
}
} catch (err) {
console.error("등록 품목 조회 실패", err);
} finally {
setLoading(false);
}
},
[config.dataSource, config.screenCode]
);
// 모드에 따라 적절한 함수 호출
const loadItems = useCallback(
async (search?: string) => {
if (isRegisteredMode) {
await fetchRegisteredItems(search);
} else {
await fetchItems(search);
}
},
[isRegisteredMode, fetchItems, fetchRegisteredItems]
);
// 라우팅 + 공정 조회 // 라우팅 + 공정 조회
const fetchRoutings = useCallback( const fetchRoutings = useCallback(
async (itemCode: string) => { async (itemCode: string) => {
@ -340,7 +389,10 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
selection, selection,
loading, loading,
saving, saving,
isRegisteredMode,
fetchItems, fetchItems,
fetchRegisteredItems,
loadItems,
selectItem, selectItem,
selectProcess, selectProcess,
fetchWorkItems, fetchWorkItems,

View File

@ -37,6 +37,10 @@ export interface ProcessWorkStandardConfig {
splitRatio?: number; splitRatio?: number;
leftPanelTitle?: string; leftPanelTitle?: string;
readonly?: boolean; readonly?: boolean;
/** 품목 목록 모드: all=전체, registered=등록된 품목만 */
itemListMode?: "all" | "registered";
/** 등록 모드 시 화면 코드 (자동 설정됨) */
screenCode?: string;
} }
// ============================================================ // ============================================================
@ -121,6 +125,7 @@ export interface ProcessWorkStandardComponentProps {
formData?: Record<string, any>; formData?: Record<string, any>;
isPreview?: boolean; isPreview?: boolean;
tableName?: string; tableName?: string;
screenId?: number | string;
} }
// 선택 상태 // 선택 상태

View File

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

View File

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