feature/v2-unified-renewal #379
|
|
@ -5,7 +5,7 @@ services:
|
|||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
dockerfile: ../docker/dev/frontend.Dockerfile
|
||||
container_name: pms-frontend-win
|
||||
ports:
|
||||
- "9771:3000"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,974 @@
|
|||
# 품목정보 (Item Info)
|
||||
|
||||
> Screen ID: /screens/140
|
||||
> 메뉴 경로: 기준정보 > 품목정보
|
||||
> 테이블: `item_info`
|
||||
|
||||
## 1. 테이블 선택 및 화면 구조
|
||||
|
||||
### 1.1 사용 테이블
|
||||
|
||||
| 테이블명 | 용도 | 비고 |
|
||||
|----------|------|------|
|
||||
| `item_info` | 품목 기본정보 | 주 테이블 |
|
||||
|
||||
### 1.2 테이블 컬럼 정의 (실제 DB 기준)
|
||||
|
||||
| 컬럼명 | 표시명 | 타입 | 필수 | 설명 |
|
||||
|--------|--------|------|------|------|
|
||||
| `id` | ID | varchar(500) | PK | UUID 자동 생성 |
|
||||
| `item_number` | 품번코드 | varchar(500) | | 품목 고유 코드 |
|
||||
| `item_name` | 품명 | varchar(500) | | 품목명 |
|
||||
| `status` | 상태 | varchar(500) | | 정상, 품절, 대기, 단종 |
|
||||
| `size` | 규격 | varchar(500) | | 규격 정보 |
|
||||
| `material` | 재질 | varchar(500) | | 재질 정보 |
|
||||
| `inventory_unit` | 재고단위 | varchar(500) | | EA, kg, L, Sheet, Box |
|
||||
| `weight` | 중량 | varchar(500) | | 중량 값 |
|
||||
| `unit` | 단위 | varchar(500) | | g, kg, kg/L, t |
|
||||
| `image` | 이미지 | varchar(500) | | 품목 이미지 경로 |
|
||||
| `division` | 구분 | varchar(500) | | 원자재, 중간재, 완제품, 포장재 (카테고리 코드) |
|
||||
| `type` | 유형 | varchar(500) | | 용도별 유형 |
|
||||
| `meno` | 메모 | varchar(500) | | 비고 (오타: memo) |
|
||||
| `selling_price` | 판매가 | varchar(500) | | 기본값 '0' |
|
||||
| `standard_price` | 기준가 | varchar(500) | | 기본값 '0' |
|
||||
| `currency_code` | 통화코드 | varchar(500) | | 기본값 'KRW' |
|
||||
| `writer` | 등록자 | varchar(500) | | 작성자 ID |
|
||||
| `company_code` | 회사코드 | varchar(500) | | 멀티테넌시 |
|
||||
| `created_date` | 등록일 | timestamp | | 자동 생성 |
|
||||
| `updated_date` | 수정일 | timestamp | | 자동 갱신 |
|
||||
|
||||
### 1.3 화면 구조 개요
|
||||
|
||||
- **화면 유형**: 목록형 (단일 테이블 CRUD)
|
||||
- **주요 기능**:
|
||||
- 품목 조회/검색/필터링
|
||||
- 품목 등록/수정/삭제
|
||||
- 그룹핑 (Group By)
|
||||
- 코드 변경/합병
|
||||
- 엑셀 업로드
|
||||
- 컬럼 표시/숨기기 설정
|
||||
|
||||
---
|
||||
|
||||
## 2. 컴포넌트 배치도
|
||||
|
||||
### 2.1 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ [검색 영역] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ v2-table-search-widget │ │
|
||||
│ │ ┌───────────┐ ┌───────────────┐ ┌───────────────┐ ┌─────────┐ │ │
|
||||
│ │ │ 상태 │ │ 품번코드 │ │ 품명 │ │ [검색] │ │ │
|
||||
│ │ │ (select) │ │ (text) │ │ (text) │ │ │ │ │
|
||||
│ │ └───────────┘ └───────────────┘ └───────────────┘ └─────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [테이블 헤더 + 액션 버튼] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [코드변경][업로드][다운로드] [등록][복사][수정][삭제] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [데이터 테이블] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ v2-table-list │ │
|
||||
│ │ ┌──┬────┬────────┬────────┬──────┬──────┬────────┬─────┬─────┬────────┐ │ │
|
||||
│ │ │☐ │상태│품번코드│품명 │규격 │재질 │재고단위│중량 │단위 │구분 │ │ │
|
||||
│ │ ├──┼────┼────────┼────────┼──────┼──────┼────────┼─────┼─────┼────────┤ │ │
|
||||
│ │ │☐ │정상│R_001 │테스트A │100mm │SUS304│EA │1.5 │kg │원자재 │ │ │
|
||||
│ │ │☐ │대기│R_002 │테스트B │200mm │AL │kg │2.0 │kg │완제품 │ │ │
|
||||
│ │ └──┴────┴────────┴────────┴──────┴──────┴────────┴─────┴─────┴────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 컴포넌트 목록
|
||||
|
||||
| 컴포넌트 타입 | 역할 |
|
||||
|---------------|------|
|
||||
| v2-table-search-widget | 검색 필터 |
|
||||
| v2-table-list | 품목 데이터 테이블 |
|
||||
| v2-button-primary | 코드변경 |
|
||||
| v2-button-primary | 업로드 (엑셀) |
|
||||
| v2-button-primary | 다운로드 (엑셀) |
|
||||
| v2-button-primary | 등록 (모달 열기) |
|
||||
| v2-button-primary | 복사 (모달 열기) |
|
||||
| v2-button-primary | 수정 (모달 열기) |
|
||||
| v2-button-primary | 삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 디자이너 설정 가이드
|
||||
|
||||
### 3.1 v2-table-search-widget (검색 필터) 설정
|
||||
|
||||
1. 좌측 컴포넌트 패널에서 `v2-table-search-widget` 드래그하여 화면 상단에 배치
|
||||
2. 대상 테이블로 아래에 배치할 테이블 리스트 선택
|
||||
|
||||
> 💡 **참고**: 검색 필터는 사용자가 런타임에서 원하는 필드를 직접 추가/삭제하여 사용할 수 있습니다. 별도의 필드 설정이 필요 없습니다.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 v2-table-list (품목 테이블) 설정
|
||||
|
||||
#### Step 1: 컴포넌트 추가
|
||||
1. 좌측 컴포넌트 패널에서 `v2-table-list` 드래그하여 검색 필터 아래에 배치
|
||||
|
||||
#### Step 2: 데이터 소스 설정
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 테이블 선택 | `item_info` |
|
||||
| 자동 컬럼 생성 | ✅ 체크 (테이블 컬럼 자동 로드) |
|
||||
|
||||
#### Step 3: 컬럼 설정
|
||||
|
||||
**[컬럼 설정]** 패널에서 표시할 컬럼 선택 및 순서 조정:
|
||||
|
||||
| 순서 | 컬럼 | 표시명 | 너비 | 정렬 | 표시 | 특수 설정 |
|
||||
|------|------|--------|------|------|------|-----------|
|
||||
| 1 | `status` | 상태 | 80 | 중앙 | ✅ | 뱃지 스타일 (색상별) |
|
||||
| 2 | `item_number` | 품번코드 | 140 | 좌측 | ✅ | |
|
||||
| 3 | `item_name` | 품명 | 200 | 좌측 | ✅ | 굵게 표시 |
|
||||
| 4 | `size` | 규격 | 150 | 좌측 | ✅ | |
|
||||
| 5 | `material` | 재질 | 150 | 좌측 | ✅ | |
|
||||
| 6 | `inventory_unit` | 재고단위 | 100 | 중앙 | ✅ | |
|
||||
| 7 | `weight` | 중량 | 80 | 우측 | ✅ | |
|
||||
| 8 | `unit` | 단위 | 80 | 중앙 | ✅ | |
|
||||
| 9 | `image` | 이미지 | 80 | 중앙 | ✅ | 이미지 미리보기 |
|
||||
| 10 | `division` | 구분 | 100 | 중앙 | ✅ | 카테고리 표시 |
|
||||
| 11 | `type` | 유형 | 100 | 중앙 | ✅ | |
|
||||
| 12 | `selling_price` | 판매가 | 100 | 우측 | ☐ | 숫자 포맷 |
|
||||
| 13 | `standard_price` | 기준가 | 100 | 우측 | ☐ | 숫자 포맷 |
|
||||
| 14 | `meno` | 메모 | 180 | 좌측 | ☐ | |
|
||||
| 15 | `writer` | 등록자 | 100 | 좌측 | ☐ | 읽기 전용 |
|
||||
| 16 | `created_date` | 등록일 | 120 | 중앙 | ☐ | 읽기 전용 |
|
||||
| 17 | `updated_date` | 수정일 | 120 | 중앙 | ☐ | 읽기 전용 |
|
||||
|
||||
#### Step 4: 기능 설정
|
||||
|
||||
| 설정 항목 | 설정 값 | 설명 |
|
||||
|-----------|---------|------|
|
||||
| 체크박스 | ✅ 사용 | 다중 선택 활성화 |
|
||||
| 페이지네이션 | ✅ 사용 | |
|
||||
| 페이지 크기 | 20 | 기본 표시 행 수 |
|
||||
| 정렬 | ✅ 사용 | 컬럼 헤더 클릭 정렬 |
|
||||
| 컬럼 리사이즈 | ✅ 사용 | 컬럼 너비 조정 |
|
||||
| 그룹핑 | ✅ 사용 | Group By 기능 |
|
||||
|
||||
#### Step 5: 그룹핑 옵션 설정
|
||||
|
||||
Group By 드롭다운에 표시할 컬럼 선택:
|
||||
- ✅ `status` (상태)
|
||||
- ✅ `division` (구분)
|
||||
- ✅ `type` (유형)
|
||||
- ✅ `inventory_unit` (재고단위)
|
||||
- ✅ `writer` (등록자)
|
||||
|
||||
---
|
||||
|
||||
### 3.3 버튼 설정
|
||||
|
||||
#### 좌측 버튼 그룹
|
||||
|
||||
##### 코드변경 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `코드변경` |
|
||||
| 액션 타입 | `code_merge` |
|
||||
| 스타일 | `secondary` |
|
||||
| 선택 필수 | ✅ 체크 (복수 선택) |
|
||||
| 병합 대상 컬럼 | `item_number` |
|
||||
| 데이터플로우 연결 | 품번코드 통합 (flow_id: 18) |
|
||||
|
||||
##### 업로드 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `업로드` |
|
||||
| 액션 타입 | `excel_upload` |
|
||||
| 스타일 | `secondary` |
|
||||
| 대상 테이블 | `item_info` |
|
||||
|
||||
##### 다운로드 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `다운로드` |
|
||||
| 액션 타입 | `excel_download` |
|
||||
| 스타일 | `secondary` |
|
||||
| 대상 | 현재 테이블 리스트 |
|
||||
|
||||
#### 우측 버튼 그룹
|
||||
|
||||
##### 등록 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `등록` |
|
||||
| 액션 타입 | `modal` |
|
||||
| 스타일 | `default` |
|
||||
| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) |
|
||||
| 모달 제목 | 품목 등록 |
|
||||
| 모달 사이즈 | `md` |
|
||||
|
||||
##### 복사 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `복사` |
|
||||
| 액션 타입 | `copy` |
|
||||
| 스타일 | `default` |
|
||||
| 선택 필수 | ✅ 체크 (1개만) |
|
||||
| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) |
|
||||
| 동작 | 선택된 데이터를 복사하여 신규 등록 폼에 채움 |
|
||||
|
||||
##### 수정 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `수정` |
|
||||
| 액션 타입 | `edit` |
|
||||
| 스타일 | `default` |
|
||||
| 선택 필수 | ✅ 체크 (1개만) |
|
||||
| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) |
|
||||
| 동작 | 선택된 데이터 수정 모드로 폼 열기 |
|
||||
|
||||
##### 삭제 버튼
|
||||
|
||||
| 설정 항목 | 설정 값 |
|
||||
|-----------|---------|
|
||||
| 라벨 | `삭제` |
|
||||
| 액션 타입 | `delete` |
|
||||
| 스타일 | `default` |
|
||||
| 선택 필수 | ✅ 체크 (복수 선택 가능) |
|
||||
| 확인 메시지 | 선택한 품목을 삭제하시겠습니까? |
|
||||
| 삭제 후 동작 | 테이블 새로고침 |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 품목 등록/수정 화면 (모달용 화면)
|
||||
|
||||
> 📌 **별도 화면 생성 필요**: 등록/복사/수정 버튼에 연결할 모달 화면을 새로 생성합니다.
|
||||
>
|
||||
> 💡 **동일 화면 공유**: 등록, 복사, 수정 버튼 모두 동일한 폼 화면을 사용합니다.
|
||||
> - **등록**: 빈 폼으로 열림
|
||||
> - **복사**: 선택된 데이터가 채워진 상태로 열림 (신규 등록)
|
||||
> - **수정**: 선택된 데이터가 채워진 상태로 열림 (기존 데이터 업데이트)
|
||||
|
||||
#### Step 1: 새 화면 생성
|
||||
|
||||
1. 화면 관리에서 **[+ 새 화면]** 클릭
|
||||
2. 화면 정보 입력:
|
||||
- 화면명: `품목 등록/수정`
|
||||
- 테이블: `item_info`
|
||||
- 화면 유형: `모달`
|
||||
|
||||
#### Step 2: 폼 필드 배치
|
||||
|
||||
**모달 레이아웃 배치도**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 품목 등록/수정 [✕] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 품번코드 * │ │ 품명 * │ │
|
||||
│ │ [____________________] │ │ [____________________] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 규격 │ │ 재질 │ │
|
||||
│ │ [____________________] │ │ [____________________] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ 재고단위 * │ │ 중량 │ │ 중량단위 │ │
|
||||
│ │ [EA ▼] │ │ [_______] │ │ [kg ▼] │ │
|
||||
│ └─────────────────────────┘ └───────────┘ └───────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 구분 * │ │ 유형 │ │
|
||||
│ │ [원자재 ▼] │ │ [반도체용 ▼] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 판매가 │ │ 기준가 │ │
|
||||
│ │ [____________________] │ │ [____________________] │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 메모 │ │
|
||||
│ │ [__________________________________________________]│ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ 상태 * │ │
|
||||
│ │ [정상 ▼] │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ [취소] [💾 저장] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**필드 목록**:
|
||||
|
||||
| 순서 | 필드 (컬럼명) | 라벨 | 입력 타입 | 필수 | 비고 |
|
||||
|------|---------------|------|-----------|------|------|
|
||||
| 1 | `item_number` | 품번코드 | text | ✅ | |
|
||||
| 2 | `item_name` | 품명 | text | ✅ | |
|
||||
| 3 | `size` | 규격 | text | | |
|
||||
| 4 | `material` | 재질 | text | | |
|
||||
| 5 | `inventory_unit` | 재고단위 | select | ✅ | 옵션: EA, kg, L, Sheet, Box |
|
||||
| 6 | `weight` | 중량 | number | | |
|
||||
| 7 | `unit` | 중량단위 | select | | 옵션: g, kg, kg/L, t |
|
||||
| 8 | `division` | 구분 | category | ✅ | 품목 구분 카테고리 |
|
||||
| 9 | `type` | 유형 | select | | 옵션: 반도체용, 태양광용, 산업용, 의료용, 건축용, 사출용, 화장품용 |
|
||||
| 10 | `selling_price` | 판매가 | number | | |
|
||||
| 11 | `standard_price` | 기준가 | number | | |
|
||||
| 12 | `meno` | 메모 | text | | |
|
||||
| 13 | `status` | 상태 | select | ✅ | 옵션: 정상, 품절, 대기, 단종 |
|
||||
|
||||
#### Step 3: 버튼 배치
|
||||
|
||||
| 버튼 | 액션 타입 | 스타일 | 설정 |
|
||||
|------|-----------|--------|------|
|
||||
| 저장 | `저장` | primary | 저장 후 모달 닫기, 부모 화면 테이블 새로고침 |
|
||||
| 취소 | `모달 닫기` | secondary | |
|
||||
|
||||
#### Step 4: 버튼에 화면 연결
|
||||
|
||||
1. 메인 화면(품목정보)으로 돌아가기
|
||||
2. **등록 버튼** 선택 → 설정 패널에서:
|
||||
- 액션 타입: `modal`
|
||||
- 연결 화면: `품목 등록/수정` 선택
|
||||
- 모달 제목: `품목 등록`
|
||||
3. **복사 버튼** 선택 → 설정 패널에서:
|
||||
- 액션 타입: `copy`
|
||||
- 연결 화면: `품목 등록/수정` 선택
|
||||
- 선택 필수: ✅ 체크
|
||||
- 동작: 선택된 데이터를 복사하여 폼에 채움 (신규 등록)
|
||||
4. **수정 버튼** 선택 → 설정 패널에서:
|
||||
- 액션 타입: `edit`
|
||||
- 연결 화면: `품목 등록/수정` 선택
|
||||
- 선택 필수: ✅ 체크
|
||||
- 동작: 선택된 데이터를 수정 모드로 폼에 채움
|
||||
|
||||
> 💡 **참고**: 컬럼별 스타일(뱃지 색상, 카테고리 표시 등)은 컴포넌트 기본 스타일을 따릅니다. 필요시 테이블 관리에서 컬럼별 상세 설정을 조정할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 컴포넌트 연동 설정
|
||||
|
||||
### 4.1 이벤트 흐름
|
||||
|
||||
```
|
||||
[검색 입력]
|
||||
│
|
||||
▼
|
||||
v2-table-search-widget
|
||||
│ onFilterChange
|
||||
▼
|
||||
v2-table-list (자동 재조회)
|
||||
│
|
||||
▼
|
||||
[데이터 표시]
|
||||
|
||||
|
||||
[등록/복사/수정 버튼 클릭]
|
||||
│
|
||||
▼
|
||||
[모달 열기] → [폼 입력] → [저장]
|
||||
│ │
|
||||
│ ▼
|
||||
│ refreshTable 이벤트
|
||||
│ │
|
||||
└────────────────────────┘
|
||||
│
|
||||
▼
|
||||
v2-table-list (재조회)
|
||||
```
|
||||
|
||||
### 4.2 연동 설정
|
||||
|
||||
| 소스 컴포넌트 | 이벤트/액션 | 대상 컴포넌트 | 동작 |
|
||||
|---------------|-------------|---------------|------|
|
||||
| 검색 위젯 | onFilterChange | 테이블 리스트 | 필터 적용, 재조회 |
|
||||
| 등록 버튼 | click | 모달 | 빈 폼으로 모달 열기 |
|
||||
| 복사 버튼 | click | 모달 | 선택 데이터가 채워진 폼 열기 (신규) |
|
||||
| 수정 버튼 | click | 모달 | 선택 데이터가 채워진 폼 열기 (수정) |
|
||||
| 삭제 버튼 | click | 테이블 리스트 | 선택 항목 삭제 |
|
||||
| 모달 저장 | afterSave | 테이블 리스트 | refreshTable |
|
||||
|
||||
### 4.3 TableOptionsContext 연동
|
||||
|
||||
```
|
||||
v2-table-search-widget ──── TableOptionsContext ──── v2-table-list
|
||||
│ │ │
|
||||
│ registeredTables에서 │ │
|
||||
│ item-table 참조 │ │
|
||||
│ │ │
|
||||
└── onFilterChange() ───────┼──────────────────────┘
|
||||
│
|
||||
▼
|
||||
필터 조건 전달 & 재조회
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 사용자 사용 예시 시나리오
|
||||
|
||||
### 시나리오 1: 품목 조회
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 화면 진입 | 전체 품목 목록 표시 |
|
||||
| 2 | 상태 필터를 "정상"으로 선택 | 자동 필터링 |
|
||||
| 3 | 품명에 "폴리머" 입력 후 검색 | 품명에 "폴리머" 포함된 품목 표시 |
|
||||
| 4 | Group by에서 "구분" 선택 | division별 그룹핑 |
|
||||
|
||||
### 시나리오 2: 품목 등록
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | [등록] 버튼 클릭 | 빈 폼 모달 표시 |
|
||||
| 2 | 데이터 입력 (품번코드, 품명, 규격 등) | 입력 필드 채움 |
|
||||
| 3 | [저장] 버튼 클릭 | 저장 완료, 모달 닫힘, 목록 갱신 |
|
||||
|
||||
### 시나리오 3: 품목 복사
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 테이블에서 복사할 행 체크박스 선택 | 행 선택 표시 |
|
||||
| 2 | [복사] 버튼 클릭 | 선택된 데이터가 채워진 폼 모달 표시 |
|
||||
| 3 | 필요시 데이터 수정 (품번코드 등) | 필드 값 변경 |
|
||||
| 4 | [저장] 버튼 클릭 | 신규 등록 완료, 목록 갱신 |
|
||||
|
||||
### 시나리오 4: 품목 수정
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 테이블에서 행 체크박스 선택 | 행 선택 표시 |
|
||||
| 2 | [수정] 버튼 클릭 | 수정 모달 표시 (기존 데이터 로드) |
|
||||
| 3 | 데이터 수정 | 필드 값 변경 |
|
||||
| 4 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 |
|
||||
|
||||
### 시나리오 5: 품목 삭제
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 삭제할 행 체크박스 선택 (다중 가능) | 행 선택 표시 |
|
||||
| 2 | [삭제] 버튼 클릭 | 삭제 확인 다이얼로그 표시 |
|
||||
| 3 | 확인 | 삭제 완료, 목록 갱신 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 체크리스트
|
||||
|
||||
### 기본 기능
|
||||
- [ ] 데이터 조회가 정상 동작하는가?
|
||||
- [ ] 검색 필터 (상태, 품번코드, 품명)가 정상 동작하는가?
|
||||
- [ ] 신규 등록이 정상 동작하는가?
|
||||
- [ ] 복사 기능이 정상 동작하는가?
|
||||
- [ ] 수정이 정상 동작하는가?
|
||||
- [ ] 삭제가 정상 동작하는가?
|
||||
- [ ] 코드변경이 정상 동작하는가?
|
||||
- [ ] 엑셀 업로드가 정상 동작하는가?
|
||||
- [ ] 엑셀 다운로드가 정상 동작하는가?
|
||||
|
||||
### 테이블 기능
|
||||
- [ ] 페이지네이션이 정상 동작하는가?
|
||||
- [ ] 정렬이 정상 동작하는가?
|
||||
- [ ] 컬럼 너비 조정이 정상 동작하는가?
|
||||
- [ ] 체크박스 선택이 정상 동작하는가?
|
||||
|
||||
### 검색 위젯 연동
|
||||
- [ ] v2-table-search-widget과 v2-table-list 연동이 정상 동작하는가?
|
||||
- [ ] 필터 변경 시 자동 재조회가 동작하는가?
|
||||
- [ ] 초기화 버튼이 정상 동작하는가?
|
||||
|
||||
### 그룹핑 기능
|
||||
- [ ] Group by 선택 시 그룹핑이 정상 동작하는가?
|
||||
- [ ] 다중 그룹핑이 정상 동작하는가?
|
||||
|
||||
---
|
||||
|
||||
## 7. 참고 사항
|
||||
|
||||
### 관련 테이블
|
||||
- `customer_item_mapping` - 거래처별 품목 매핑
|
||||
- `supplier_item_mapping` - 공급업체별 품목 매핑
|
||||
- `item_inspection_info` - 품목 검사 정보
|
||||
- `item_routing_version` - 품목별 공정 버전
|
||||
- `item_routing_detail` - 품목별 공정 상세
|
||||
|
||||
### 특이 사항
|
||||
- `division` 컬럼은 카테고리 코드 (예: CATEGORY_191259)로 저장됨
|
||||
- `meno` 컬럼은 오타로 보임 (원래 memo)
|
||||
- `selling_price`, `standard_price`는 varchar로 저장됨 (숫자 형식 문자열)
|
||||
- `company_code`는 멀티테넌시용 회사 코드
|
||||
|
||||
---
|
||||
|
||||
## 8. DB INSERT용 JSON 설정 (screen_layouts_v2 방식)
|
||||
|
||||
> 📌 실제 화면 저장은 `screen_definitions` + `screen_layouts_v2` 테이블을 사용합니다.
|
||||
> `screen_layouts_v2`는 전체 레이아웃을 하나의 JSON (`layout_data`)으로 저장합니다.
|
||||
|
||||
### 8.1 테이블 구조
|
||||
|
||||
#### screen_definitions
|
||||
|
||||
| 컬럼명 | 타입 | 필수 | 기본값 | 설명 |
|
||||
|--------|------|------|--------|------|
|
||||
| `screen_id` | integer | PK | 자동 생성 (시퀀스) | 화면 고유 ID |
|
||||
| `screen_name` | varchar(100) | ✅ | - | 화면명 |
|
||||
| `screen_code` | varchar(50) | ✅ | **자동 생성** | `{company_code}_{순번}` 형식 |
|
||||
| `table_name` | varchar(100) | | - | 기본 테이블명 |
|
||||
| `company_code` | varchar(50) | ✅ | - | 회사 코드 |
|
||||
| `description` | text | | - | 화면 설명 |
|
||||
| `is_active` | char(1) | | `'Y'` | Y=활성, N=비활성, D=삭제 |
|
||||
| `created_date` | timestamp | | `CURRENT_TIMESTAMP` | 생성일시 |
|
||||
| `db_source_type` | varchar(10) | | `'internal'` | internal/external |
|
||||
| `data_source_type` | varchar(20) | | `'database'` | database/rest_api |
|
||||
|
||||
#### screen_layouts_v2
|
||||
|
||||
| 컬럼명 | 타입 | 필수 | 기본값 | 설명 |
|
||||
|--------|------|------|--------|------|
|
||||
| `layout_id` | integer | PK | 자동 생성 (시퀀스) | 레이아웃 고유 ID |
|
||||
| `screen_id` | integer | ✅ | - | 화면 ID (FK) |
|
||||
| `company_code` | varchar(20) | ✅ | - | 회사 코드 |
|
||||
| `layout_data` | jsonb | ✅ | `'{}'` | 전체 레이아웃 JSON |
|
||||
| `created_at` | timestamp | | `now()` | 생성일시 |
|
||||
| `updated_at` | timestamp | | `now()` | 수정일시 |
|
||||
|
||||
### 8.2 화면 정의 (screen_definitions)
|
||||
|
||||
> ⚠️ `screen_code`는 API 호출 시 자동 생성됩니다. (`{company_code}_{순번}` 형식)
|
||||
|
||||
**필수 입력 필드:**
|
||||
|
||||
```json
|
||||
{
|
||||
"screenName": "품목정보",
|
||||
"tableName": "item_info",
|
||||
"companyCode": "COMPANY_7",
|
||||
"description": "품목 기본정보 관리 화면"
|
||||
}
|
||||
```
|
||||
|
||||
**전체 필드 (자동 생성 포함):**
|
||||
|
||||
```json
|
||||
{
|
||||
"screen_id": 140,
|
||||
"screen_name": "품목정보",
|
||||
"screen_code": "COMPANY_7_3",
|
||||
"table_name": "item_info",
|
||||
"company_code": "COMPANY_7",
|
||||
"description": "품목 기본정보 관리 화면",
|
||||
"is_active": "Y",
|
||||
"db_source_type": "internal",
|
||||
"data_source_type": "database",
|
||||
"created_date": "2025-01-29T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 레이아웃 데이터 (screen_layouts_v2.layout_data)
|
||||
|
||||
> 전체 레이아웃을 하나의 JSON으로 저장
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_search",
|
||||
"url": "@/lib/registry/components/v2-table-search-widget",
|
||||
"size": { "width": 1920, "height": 80 },
|
||||
"position": { "x": 0, "y": 20, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-table-search-widget",
|
||||
"label": "검색 필터",
|
||||
"webTypeConfig": {}
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_table",
|
||||
"url": "@/lib/registry/components/v2-table-list",
|
||||
"size": { "width": 1920, "height": 930 },
|
||||
"position": { "x": 0, "y": 150, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-table-list",
|
||||
"label": "테이블 리스트",
|
||||
"filter": { "enabled": true, "filters": [] },
|
||||
"height": "auto",
|
||||
"actions": { "actions": [], "bulkActions": false, "showActions": false },
|
||||
"columns": [
|
||||
{ "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "status" },
|
||||
{ "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "item_number", "searchable": true, "displayName": "item_number" },
|
||||
{ "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "item_name", "searchable": true, "displayName": "item_name" },
|
||||
{ "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "size", "searchable": true, "displayName": "size" },
|
||||
{ "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "material" },
|
||||
{ "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "inventory_unit", "searchable": true, "displayName": "inventory_unit" },
|
||||
{ "align": "left", "order": 6, "format": "text", "visible": true, "sortable": true, "columnName": "weight", "searchable": true, "displayName": "weight" },
|
||||
{ "align": "left", "order": 7, "format": "text", "visible": true, "sortable": true, "columnName": "unit", "searchable": true, "displayName": "unit" },
|
||||
{ "align": "left", "order": 8, "format": "text", "visible": true, "sortable": true, "columnName": "division", "searchable": true, "displayName": "division" },
|
||||
{ "align": "left", "order": 9, "format": "text", "visible": true, "sortable": true, "columnName": "type", "searchable": true, "displayName": "type" },
|
||||
{ "align": "left", "order": 10, "format": "text", "visible": true, "sortable": true, "columnName": "writer", "searchable": true, "displayName": "writer" }
|
||||
],
|
||||
"autoLoad": true,
|
||||
"checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true },
|
||||
"pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true },
|
||||
"showFooter": true,
|
||||
"showHeader": true,
|
||||
"tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true },
|
||||
"displayMode": "table",
|
||||
"stickyHeader": false,
|
||||
"selectedTable": "item_info",
|
||||
"webTypeConfig": {},
|
||||
"horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 100, "maxVisibleColumns": 8 }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_code_merge",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 88, "height": 40 },
|
||||
"position": { "x": 10, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "코드변경",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "code_merge",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다.",
|
||||
"mergeColumnName": "item_number"
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": {
|
||||
"variant": "default",
|
||||
"actionType": "custom",
|
||||
"dataflowConfig": {
|
||||
"flowConfig": { "flowId": 18, "flowName": "품번코드 통합", "contextData": {}, "executionTiming": "after" },
|
||||
"selectedDiagramId": 18
|
||||
}
|
||||
}
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_upload",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 88, "height": 40 },
|
||||
"position": { "x": 110, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "업로드",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "excel_upload",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다."
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_download",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 88, "height": 40 },
|
||||
"position": { "x": 210, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "다운로드",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "excel_download",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다."
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_register",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1550, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "등록",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "modal",
|
||||
"modalSize": "md",
|
||||
"modalTitle": "품목 등록",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다.",
|
||||
"targetScreenId": "{{modal_screen_id}}"
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_copy",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1640, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "복사",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "copy",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다.",
|
||||
"targetScreenId": "{{modal_screen_id}}"
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_edit",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1730, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "수정",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "edit",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다.",
|
||||
"targetScreenId": "{{modal_screen_id}}"
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_delete",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1820, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "삭제",
|
||||
"type": "v2-button-primary",
|
||||
"label": "기본 버튼",
|
||||
"action": {
|
||||
"type": "delete",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다.",
|
||||
"successMessage": "저장되었습니다."
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button",
|
||||
"webTypeConfig": { "variant": "default", "actionType": "custom" }
|
||||
},
|
||||
"displayOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 모달 화면 (품목 등록/수정)
|
||||
|
||||
#### 화면 정의 (필수 입력)
|
||||
|
||||
```json
|
||||
{
|
||||
"screenName": "품목 등록/수정",
|
||||
"tableName": "item_info",
|
||||
"companyCode": "COMPANY_7",
|
||||
"description": "품목 등록/수정 폼 화면"
|
||||
}
|
||||
```
|
||||
|
||||
#### 레이아웃 데이터 (screen_layouts_v2.layout_data)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp_item_number",
|
||||
"url": "@/lib/registry/components/v2-text-input",
|
||||
"size": { "width": 300, "height": 60 },
|
||||
"position": { "x": 20, "y": 20, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-text-input",
|
||||
"label": "품번코드",
|
||||
"fieldName": "item_number",
|
||||
"placeholder": "품번코드를 입력하세요",
|
||||
"required": true
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
{
|
||||
"id": "comp_item_name",
|
||||
"url": "@/lib/registry/components/v2-text-input",
|
||||
"size": { "width": 300, "height": 60 },
|
||||
"position": { "x": 340, "y": 20, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-text-input",
|
||||
"label": "품명",
|
||||
"fieldName": "item_name",
|
||||
"placeholder": "품명을 입력하세요",
|
||||
"required": true
|
||||
},
|
||||
"displayOrder": 1
|
||||
},
|
||||
{
|
||||
"id": "comp_status",
|
||||
"url": "@/lib/registry/components/v2-select-basic",
|
||||
"size": { "width": 300, "height": 60 },
|
||||
"position": { "x": 20, "y": 100, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-select-basic",
|
||||
"label": "상태",
|
||||
"fieldName": "status",
|
||||
"options": ["정상", "품절", "대기", "단종"]
|
||||
},
|
||||
"displayOrder": 2
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_save",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 400, "y": 500, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "저장",
|
||||
"type": "v2-button-primary",
|
||||
"label": "저장 버튼",
|
||||
"action": {
|
||||
"type": "save",
|
||||
"closeModalAfterSave": true,
|
||||
"refreshParentTable": true,
|
||||
"successMessage": "저장되었습니다.",
|
||||
"errorMessage": "저장 중 오류가 발생했습니다."
|
||||
},
|
||||
"variant": "primary",
|
||||
"actionType": "button"
|
||||
},
|
||||
"displayOrder": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 API 호출 방식
|
||||
|
||||
> 📌 실제 화면 생성은 API를 통해 진행됩니다. `screen_code`는 서버에서 자동 생성됩니다.
|
||||
|
||||
#### Step 1: 화면 코드 자동 생성 API
|
||||
|
||||
```http
|
||||
GET /api/screens/generate-code?companyCode=COMPANY_7
|
||||
```
|
||||
|
||||
**응답:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { "screenCode": "COMPANY_7_4" }
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: 화면 생성 API
|
||||
|
||||
```http
|
||||
POST /api/screens
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"screenName": "품목정보",
|
||||
"screenCode": "COMPANY_7_4",
|
||||
"tableName": "item_info",
|
||||
"companyCode": "COMPANY_7",
|
||||
"description": "품목 기본정보 관리 화면"
|
||||
}
|
||||
```
|
||||
|
||||
**응답:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"screenId": 141,
|
||||
"screenCode": "COMPANY_7_4",
|
||||
"screenName": "품목정보"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3: 레이아웃 저장 API
|
||||
|
||||
```http
|
||||
PUT /api/screens/141/layout-v2
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"layoutData": {
|
||||
"version": "2.0",
|
||||
"components": [ /* 8.2의 components 배열 */ ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 SQL 직접 INSERT (참고용)
|
||||
|
||||
> ⚠️ 일반적으로 API를 사용하지만, 대량 마이그레이션 시 직접 SQL 사용 가능
|
||||
|
||||
```sql
|
||||
-- Step 1: 화면 정의 (screen_code는 수동 지정 필요)
|
||||
INSERT INTO screen_definitions (
|
||||
screen_name, screen_code, table_name, company_code, description
|
||||
) VALUES (
|
||||
'품목정보', 'COMPANY_7_4', 'item_info', 'COMPANY_7', '품목 기본정보 관리 화면'
|
||||
) RETURNING screen_id;
|
||||
|
||||
-- Step 2: 레이아웃 저장 (screen_id 사용)
|
||||
INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data)
|
||||
VALUES (
|
||||
141, -- 위에서 반환된 screen_id
|
||||
'COMPANY_7',
|
||||
'{"version": "2.0", "components": [...]}'::jsonb
|
||||
);
|
||||
```
|
||||
|
||||
### 8.6 주의사항
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| `screen_code` | API 사용 시 `generateScreenCode` 먼저 호출, 형식: `{company_code}_{순번}` |
|
||||
| `screen_id` | 화면 생성 후 반환되는 값, 레이아웃 저장 시 필요 |
|
||||
| `component.id` | 고유 ID (UUID 또는 `comp_` prefix), 중복 불가 |
|
||||
| `component.url` | `@/lib/registry/components/v2-xxx` 형식 |
|
||||
| `{{modal_screen_id}}` | 모달 화면 먼저 생성 후 실제 ID로 치환 |
|
||||
| `version` | 반드시 `"2.0"` 사용 |
|
||||
| UNIQUE 제약 | `screen_layouts_v2`는 `(screen_id, company_code)` 조합이 유니크 |
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# 화면 구현 가이드
|
||||
|
||||
V2 컴포넌트를 활용한 ERP 화면 구현 가이드입니다.
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
```
|
||||
screen-implementation-guide/
|
||||
├── 01_master-data/ # 기준정보
|
||||
│ ├── company-info.md # 회사정보
|
||||
│ ├── department.md # 부서관리
|
||||
│ ├── item-info.md # 품목정보
|
||||
│ └── options.md # 옵션설정
|
||||
│
|
||||
├── 02_sales/ # 영업관리
|
||||
│ ├── quotation.md # 견적관리
|
||||
│ ├── order.md # 수주관리
|
||||
│ ├── customer.md # 거래처관리
|
||||
│ ├── sales-item.md # 판매품목정보
|
||||
│ └── options.md # 영업옵션설정
|
||||
│
|
||||
├── 03_production/ # 생산관리
|
||||
│ ├── production-plan.md # 생산계획
|
||||
│ ├── work-order.md # 작업지시
|
||||
│ ├── production-result.md # 생산실적
|
||||
│ ├── process-info.md # 공정정보관리
|
||||
│ ├── bom.md # BOM관리
|
||||
│ └── options.md # 생산옵션설정
|
||||
│
|
||||
├── 04_purchase/ # 구매관리
|
||||
│ ├── purchase-order.md # 발주관리
|
||||
│ ├── purchase-item.md # 구매품목정보
|
||||
│ ├── supplier.md # 공급업체관리
|
||||
│ ├── receiving.md # 입고관리
|
||||
│ └── options.md # 구매옵션설정
|
||||
│
|
||||
├── 05_equipment/ # 설비관리
|
||||
│ ├── equipment-info.md # 설비정보
|
||||
│ └── options.md # 설비옵션설정
|
||||
│
|
||||
├── 06_logistics/ # 물류관리
|
||||
│ ├── logistics-info.md # 물류정보관리
|
||||
│ ├── inout.md # 입출고관리
|
||||
│ ├── inventory.md # 재고현황
|
||||
│ ├── warehouse.md # 창고정보관리
|
||||
│ ├── shipping.md # 출고관리
|
||||
│ └── options.md # 물류옵션설정
|
||||
│
|
||||
├── 07_quality/ # 품질관리
|
||||
│ ├── inspection-info.md # 검사정보관리
|
||||
│ ├── item-inspection.md # 품목검사정보
|
||||
│ └── options.md # 품질옵션설정
|
||||
│
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 문서 작성 형식
|
||||
|
||||
각 화면별 문서는 다음 구조로 작성됩니다:
|
||||
|
||||
### 1. 테이블 선택 및 화면 구조
|
||||
- 사용할 데이터베이스 테이블
|
||||
- 테이블 간 관계 (FK, 조인)
|
||||
- 화면 전체 레이아웃
|
||||
|
||||
### 2. 컴포넌트 배치도
|
||||
- ASCII 다이어그램으로 컴포넌트 배치
|
||||
- 각 영역별 사용 컴포넌트 명시
|
||||
|
||||
### 3. 각 컴포넌트별 설정
|
||||
- 컴포넌트 타입
|
||||
- 상세 설정 (config)
|
||||
- 연동 설정
|
||||
|
||||
### 4. 사용자 사용 예시 시나리오
|
||||
- 테스트 시나리오
|
||||
- 기대 동작
|
||||
- 검증 포인트
|
||||
|
||||
## 메뉴별 Screen ID 매핑
|
||||
|
||||
| 메뉴 | Screen ID | 상태 |
|
||||
|------|-----------|------|
|
||||
| **기준정보** | | |
|
||||
| 회사정보 | /screens/138 | 활성화 |
|
||||
| 부서관리 | /screens/1487 | 활성화 |
|
||||
| 품목정보 | /screens/140 | 활성화 |
|
||||
| 옵션설정 | /screens/1421 | 활성화 |
|
||||
| **영업관리** | | |
|
||||
| 견적관리 | - | 활성화 |
|
||||
| 수주관리 | /screens/156 | 활성화 |
|
||||
| 거래처관리 | - | 활성화 |
|
||||
| 판매품목정보 | - | 활성화 |
|
||||
| 영업옵션설정 | /screens/1552 | 활성화 |
|
||||
| **생산관리** | | |
|
||||
| 생산계획 | - | 활성화 |
|
||||
| 작업지시 | - | 활성화 |
|
||||
| 생산실적 | - | 활성화 |
|
||||
| 공정정보관리 | /screens/1599 | 활성화 |
|
||||
| BOM관리 | - | 활성화 |
|
||||
| 생산옵션설정 | /screens/1606 | 활성화 |
|
||||
| **구매관리** | | |
|
||||
| 발주관리 | /screens/1244 | 활성화 |
|
||||
| 구매품목정보 | /screens/1061 | 활성화 |
|
||||
| 공급업체관리 | /screens/1053 | 활성화 |
|
||||
| 입고관리 | /screens/1064 | 활성화 |
|
||||
| 구매옵션설정 | /screens/1057 | 활성화 |
|
||||
| **설비관리** | | |
|
||||
| 설비정보 | /screens/1253 | 활성화 |
|
||||
| 설비옵션설정 | /screens/1264 | 활성화 |
|
||||
| **물류관리** | | |
|
||||
| 물류정보관리 | /screens/1556 | 활성화 |
|
||||
| 입출고관리 | - | 활성화 |
|
||||
| 재고현황 | /screens/1587 | 활성화 |
|
||||
| 창고정보관리 | /screens/1562 | 활성화 |
|
||||
| 출고관리 | /screens/2296 | 활성화 |
|
||||
| 물류옵션설정 | /screens/1559 | 활성화 |
|
||||
| **품질관리** | | |
|
||||
| 검사정보관리 | /screens/1616 | 활성화 |
|
||||
| 품목검사정보 | /screens/2089 | 활성화 |
|
||||
| 품질옵션설정 | /screens/1622 | 활성화 |
|
||||
|
||||
## 참고 문서
|
||||
|
||||
- [V2 컴포넌트 분석 가이드](../V2_컴포넌트_분석_가이드.md)
|
||||
- [V2 컴포넌트 연동 가이드](../V2_컴포넌트_연동_가이드.md)
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
# [화면명]
|
||||
|
||||
> Screen ID: /screens/XXX
|
||||
> 메뉴 경로: [L2 메뉴] > [L3 메뉴]
|
||||
|
||||
## 1. 테이블 선택 및 화면 구조
|
||||
|
||||
### 1.1 사용 테이블
|
||||
|
||||
| 테이블명 | 용도 | 비고 |
|
||||
|----------|------|------|
|
||||
| `table_name` | 마스터 데이터 | 주 테이블 |
|
||||
| `detail_table` | 디테일 데이터 | FK: master_id |
|
||||
|
||||
### 1.2 테이블 관계
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ master_table │ │ detail_table │
|
||||
├─────────────────┤ ├─────────────────┤
|
||||
│ id (PK) │──1:N──│ master_id (FK) │
|
||||
│ name │ │ id (PK) │
|
||||
│ ... │ │ ... │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 화면 구조 개요
|
||||
|
||||
- **화면 유형**: [목록형 / 마스터-디테일 / 단일 폼 / 복합]
|
||||
- **주요 기능**: [CRUD / 조회 / 집계 등]
|
||||
|
||||
---
|
||||
|
||||
## 2. 컴포넌트 배치도
|
||||
|
||||
### 2.1 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [검색 영역] v2-table-search-widget │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [메인 테이블] v2-table-list │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ [버튼 영역] v2-button-primary (신규, 저장, 삭제) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 컴포넌트 목록
|
||||
|
||||
| 컴포넌트 ID | 컴포넌트 타입 | 역할 |
|
||||
|-------------|---------------|------|
|
||||
| `search-widget` | v2-table-search-widget | 검색 필터 |
|
||||
| `main-table` | v2-table-list | 데이터 목록 |
|
||||
| `btn-new` | v2-button-primary | 신규 등록 |
|
||||
| `btn-save` | v2-button-primary | 저장 |
|
||||
| `btn-delete` | v2-button-primary | 삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 각 컴포넌트별 설정
|
||||
|
||||
### 3.1 v2-table-search-widget
|
||||
|
||||
```json
|
||||
{
|
||||
"targetTableId": "main-table",
|
||||
"searchFields": [
|
||||
{
|
||||
"field": "name",
|
||||
"label": "이름",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"label": "상태",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{ "value": "active", "label": "활성" },
|
||||
{ "value": "inactive", "label": "비활성" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 v2-table-list
|
||||
|
||||
```json
|
||||
{
|
||||
"tableName": "master_table",
|
||||
"columns": [
|
||||
{
|
||||
"field": "id",
|
||||
"headerName": "ID",
|
||||
"width": 80,
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"field": "name",
|
||||
"headerName": "이름",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"headerName": "상태",
|
||||
"width": 100
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"checkbox": true,
|
||||
"pagination": true,
|
||||
"sorting": true
|
||||
},
|
||||
"pagination": {
|
||||
"pageSize": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 v2-button-primary (저장)
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "저장",
|
||||
"actionType": "save",
|
||||
"variant": "default",
|
||||
"afterSaveActions": ["refreshTable"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 컴포넌트 연동 설정
|
||||
|
||||
### 4.1 이벤트 흐름
|
||||
|
||||
```
|
||||
[검색 입력]
|
||||
│
|
||||
▼
|
||||
v2-table-search-widget
|
||||
│ onFilterChange
|
||||
▼
|
||||
v2-table-list (자동 재조회)
|
||||
│
|
||||
▼
|
||||
[데이터 표시]
|
||||
```
|
||||
|
||||
### 4.2 연동 설정
|
||||
|
||||
| 소스 컴포넌트 | 이벤트/액션 | 대상 컴포넌트 | 동작 |
|
||||
|---------------|-------------|---------------|------|
|
||||
| search-widget | onFilterChange | main-table | 필터 적용 |
|
||||
| btn-save | click | main-table | refreshTable |
|
||||
|
||||
---
|
||||
|
||||
## 5. 사용자 사용 예시 시나리오
|
||||
|
||||
### 시나리오 1: 데이터 조회
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 화면 진입 | 전체 목록 표시 |
|
||||
| 2 | 검색어 입력 | 필터링된 결과 표시 |
|
||||
| 3 | 정렬 클릭 | 정렬 순서 변경 |
|
||||
|
||||
### 시나리오 2: 데이터 등록
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | [신규] 버튼 클릭 | 등록 모달/폼 표시 |
|
||||
| 2 | 데이터 입력 | 입력 필드 채움 |
|
||||
| 3 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 |
|
||||
|
||||
### 시나리오 3: 데이터 수정
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 행 더블클릭 | 수정 모달/폼 표시 |
|
||||
| 2 | 데이터 수정 | 필드 값 변경 |
|
||||
| 3 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 |
|
||||
|
||||
### 시나리오 4: 데이터 삭제
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|------|-------------|-----------|
|
||||
| 1 | 행 체크박스 선택 | 선택 표시 |
|
||||
| 2 | [삭제] 버튼 클릭 | 삭제 확인 다이얼로그 |
|
||||
| 3 | 확인 | 삭제 완료, 목록 갱신 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 체크리스트
|
||||
|
||||
- [ ] 데이터 조회가 정상 동작하는가?
|
||||
- [ ] 검색 필터가 정상 동작하는가?
|
||||
- [ ] 신규 등록이 정상 동작하는가?
|
||||
- [ ] 수정이 정상 동작하는가?
|
||||
- [ ] 삭제가 정상 동작하는가?
|
||||
- [ ] 페이지네이션이 정상 동작하는가?
|
||||
- [ ] 정렬이 정상 동작하는가?
|
||||
|
||||
---
|
||||
|
||||
## 7. 참고 사항
|
||||
|
||||
- 관련 화면: [관련 화면명](./related-screen.md)
|
||||
- 특이 사항: 없음
|
||||
|
|
@ -8,6 +8,11 @@
|
|||
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
|
||||
*
|
||||
* RepeaterTable 및 ItemSelectionModal 재사용
|
||||
*
|
||||
* 데이터 전달 인터페이스:
|
||||
* - DataProvidable: 선택된 데이터 제공
|
||||
* - DataReceivable: 외부에서 데이터 수신
|
||||
* - repeaterDataChange 이벤트 발행
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
|
|
@ -28,6 +33,13 @@ import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/Re
|
|||
import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal";
|
||||
import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types";
|
||||
|
||||
// 데이터 전달 인터페이스
|
||||
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
|
||||
// V2 이벤트 시스템
|
||||
import { V2_EVENTS, dispatchV2Event } from "@/types/component-events";
|
||||
|
||||
// 전역 UnifiedRepeater 등록 (buttonActions에서 사용)
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -55,6 +67,9 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
[propConfig],
|
||||
);
|
||||
|
||||
// ScreenContext (데이터 전달 인터페이스 등록용)
|
||||
const screenContext = useScreenContextOptional();
|
||||
|
||||
// 상태
|
||||
const [data, setData] = useState<any[]>(initialData || []);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
|
|
@ -101,6 +116,123 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
};
|
||||
}, [config.dataSource?.tableName]);
|
||||
|
||||
// ============================================================
|
||||
// DataProvidable 인터페이스 구현
|
||||
// 다른 컴포넌트에서 이 리피터의 데이터를 가져갈 수 있게 함
|
||||
// ============================================================
|
||||
const dataProvider: DataProvidable = useMemo(() => ({
|
||||
componentId: parentId || config.fieldName || "unified-repeater",
|
||||
componentType: "unified-repeater",
|
||||
|
||||
// 선택된 행 데이터 반환
|
||||
getSelectedData: () => {
|
||||
return Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean);
|
||||
},
|
||||
|
||||
// 전체 데이터 반환
|
||||
getAllData: () => {
|
||||
return [...data];
|
||||
},
|
||||
|
||||
// 선택 초기화
|
||||
clearSelection: () => {
|
||||
setSelectedRows(new Set());
|
||||
},
|
||||
}), [parentId, config.fieldName, data, selectedRows]);
|
||||
|
||||
// ============================================================
|
||||
// DataReceivable 인터페이스 구현
|
||||
// 외부에서 이 리피터로 데이터를 전달받을 수 있게 함
|
||||
// ============================================================
|
||||
const dataReceiver: DataReceivable = useMemo(() => ({
|
||||
componentId: parentId || config.fieldName || "unified-repeater",
|
||||
componentType: "repeater",
|
||||
|
||||
// 데이터 수신 (append, replace, merge 모드 지원)
|
||||
receiveData: async (incomingData: any[], receiverConfig: DataReceiverConfig) => {
|
||||
if (!incomingData || incomingData.length === 0) return;
|
||||
|
||||
// 매핑 규칙 적용
|
||||
const mappedData = incomingData.map((item, index) => {
|
||||
const newRow: any = { _id: `received_${Date.now()}_${index}` };
|
||||
|
||||
if (receiverConfig.mappingRules && receiverConfig.mappingRules.length > 0) {
|
||||
receiverConfig.mappingRules.forEach((rule) => {
|
||||
const sourceValue = item[rule.sourceField];
|
||||
newRow[rule.targetField] = sourceValue !== undefined ? sourceValue : rule.defaultValue;
|
||||
});
|
||||
} else {
|
||||
// 매핑 규칙 없으면 그대로 복사
|
||||
Object.assign(newRow, item);
|
||||
}
|
||||
|
||||
return newRow;
|
||||
});
|
||||
|
||||
// 모드에 따라 데이터 처리
|
||||
switch (receiverConfig.mode) {
|
||||
case "replace":
|
||||
setData(mappedData);
|
||||
onDataChange?.(mappedData);
|
||||
break;
|
||||
case "merge":
|
||||
// 중복 제거 후 병합 (id 또는 _id 기준)
|
||||
const existingIds = new Set(data.map((row) => row.id || row._id));
|
||||
const newItems = mappedData.filter((row) => !existingIds.has(row.id || row._id));
|
||||
const mergedData = [...data, ...newItems];
|
||||
setData(mergedData);
|
||||
onDataChange?.(mergedData);
|
||||
break;
|
||||
case "append":
|
||||
default:
|
||||
const appendedData = [...data, ...mappedData];
|
||||
setData(appendedData);
|
||||
onDataChange?.(appendedData);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// 현재 데이터 반환
|
||||
getData: () => {
|
||||
return [...data];
|
||||
},
|
||||
}), [parentId, config.fieldName, data, onDataChange]);
|
||||
|
||||
// ============================================================
|
||||
// ScreenContext에 DataProvider/DataReceiver 등록
|
||||
// ============================================================
|
||||
useEffect(() => {
|
||||
if (screenContext && (parentId || config.fieldName)) {
|
||||
const componentId = parentId || config.fieldName || "unified-repeater";
|
||||
|
||||
screenContext.registerDataProvider(componentId, dataProvider);
|
||||
screenContext.registerDataReceiver(componentId, dataReceiver);
|
||||
|
||||
return () => {
|
||||
screenContext.unregisterDataProvider(componentId);
|
||||
screenContext.unregisterDataReceiver(componentId);
|
||||
};
|
||||
}
|
||||
}, [screenContext, parentId, config.fieldName, dataProvider, dataReceiver]);
|
||||
|
||||
// ============================================================
|
||||
// repeaterDataChange 이벤트 발행
|
||||
// 데이터 변경 시 다른 컴포넌트(aggregation-widget 등)에 알림
|
||||
// ============================================================
|
||||
const prevDataLengthRef = useRef(data.length);
|
||||
useEffect(() => {
|
||||
// 데이터가 변경되었을 때만 이벤트 발행
|
||||
if (typeof window !== "undefined" && data.length !== prevDataLengthRef.current) {
|
||||
dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, {
|
||||
componentId: parentId || config.fieldName || "unified-repeater",
|
||||
tableName: config.dataSource?.tableName || "",
|
||||
data: data,
|
||||
selectedData: Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean),
|
||||
});
|
||||
prevDataLengthRef.current = data.length;
|
||||
}
|
||||
}, [data, selectedRows, parentId, config.fieldName, config.dataSource?.tableName]);
|
||||
|
||||
// 저장 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleSaveEvent = async (event: CustomEvent) => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import { cn } from "@/lib/utils";
|
|||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
// V2 이벤트 시스템
|
||||
import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events";
|
||||
|
||||
interface AggregationWidgetComponentProps extends ComponentRendererProps {
|
||||
config?: AggregationWidgetConfig;
|
||||
// 외부에서 데이터를 직접 전달받을 수 있음
|
||||
|
|
@ -269,19 +272,19 @@ export function AggregationWidgetComponent({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataSourceType, selectedRowsKey]);
|
||||
|
||||
// ============================================================
|
||||
// 전역 선택 이벤트 수신 (dataSourceType === "selection"일 때)
|
||||
// V2 표준 이벤트만 사용 (중복 이벤트 제거됨)
|
||||
// ============================================================
|
||||
useEffect(() => {
|
||||
if (dataSourceType !== "selection" || isDesignMode) return;
|
||||
|
||||
// 테이블리스트에서 발생하는 선택 이벤트 수신
|
||||
// tableListDataChange 이벤트의 data가 선택된 행들임
|
||||
const handleTableListDataChange = (event: CustomEvent) => {
|
||||
const { data: eventData, selectedRows: eventSelectedRows } = event.detail || {};
|
||||
// data가 선택된 행 데이터 배열
|
||||
// 테이블리스트 데이터 변경 이벤트 (V2 표준)
|
||||
const handleTableListDataChange = (event: CustomEvent<TableListDataChangeDetail>) => {
|
||||
const { data: eventData } = event.detail || {};
|
||||
const rows = eventData || [];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
// 필터 적용
|
||||
const filteredData = applyFilters(
|
||||
rows,
|
||||
filtersRef.current || [],
|
||||
|
|
@ -293,8 +296,8 @@ export function AggregationWidgetComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// 리피터에서 발생하는 이벤트
|
||||
const handleRepeaterDataChange = (event: CustomEvent) => {
|
||||
// 리피터 데이터 변경 이벤트 (V2 표준)
|
||||
const handleRepeaterDataChange = (event: CustomEvent<RepeaterDataChangeDetail>) => {
|
||||
const { data: eventData, selectedData } = event.detail || {};
|
||||
const rows = selectedData || eventData || [];
|
||||
|
||||
|
|
@ -310,38 +313,13 @@ export function AggregationWidgetComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// 일반 선택 이벤트
|
||||
const handleSelectionChange = (event: CustomEvent) => {
|
||||
const { selectedRows: eventSelectedRows, selectedData, checkedRows, selectedItems } = event.detail || {};
|
||||
const rows = selectedData || eventSelectedRows || checkedRows || selectedItems || [];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const filteredData = applyFilters(
|
||||
rows,
|
||||
filtersRef.current || [],
|
||||
filterLogic,
|
||||
formDataRef.current,
|
||||
selectedRowsRef.current
|
||||
);
|
||||
setData(filteredData);
|
||||
}
|
||||
};
|
||||
|
||||
// 다양한 선택 이벤트 수신
|
||||
window.addEventListener("tableListDataChange" as any, handleTableListDataChange);
|
||||
window.addEventListener("repeaterDataChange" as any, handleRepeaterDataChange);
|
||||
window.addEventListener("selectionChange" as any, handleSelectionChange);
|
||||
window.addEventListener("tableSelectionChange" as any, handleSelectionChange);
|
||||
window.addEventListener("rowSelectionChange" as any, handleSelectionChange);
|
||||
window.addEventListener("checkboxSelectionChange" as any, handleSelectionChange);
|
||||
// V2 표준 이벤트만 구독 (중복 이벤트 제거)
|
||||
const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange);
|
||||
const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("tableListDataChange" as any, handleTableListDataChange);
|
||||
window.removeEventListener("repeaterDataChange" as any, handleRepeaterDataChange);
|
||||
window.removeEventListener("selectionChange" as any, handleSelectionChange);
|
||||
window.removeEventListener("tableSelectionChange" as any, handleSelectionChange);
|
||||
window.removeEventListener("rowSelectionChange" as any, handleSelectionChange);
|
||||
window.removeEventListener("checkboxSelectionChange" as any, handleSelectionChange);
|
||||
unsubscribeTableList();
|
||||
unsubscribeRepeater();
|
||||
};
|
||||
}, [dataSourceType, isDesignMode, filterLogic]);
|
||||
|
||||
|
|
@ -362,14 +340,17 @@ export function AggregationWidgetComponent({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalDataKey, filterLogic]);
|
||||
|
||||
// ============================================================
|
||||
// 컴포넌트 데이터 변경 이벤트 리스닝 (dataSourceType === "component"일 때)
|
||||
// V2 표준 이벤트만 사용
|
||||
// ============================================================
|
||||
useEffect(() => {
|
||||
if (dataSourceType !== "component" || !dataSourceComponentId || isDesignMode) return;
|
||||
|
||||
const handleDataChange = (event: CustomEvent) => {
|
||||
// 테이블 리스트 데이터 변경
|
||||
const handleTableListDataChange = (event: CustomEvent<TableListDataChangeDetail>) => {
|
||||
const { componentId, data: eventData } = event.detail || {};
|
||||
if (componentId === dataSourceComponentId && Array.isArray(eventData)) {
|
||||
// 필터 적용
|
||||
const filteredData = applyFilters(
|
||||
eventData,
|
||||
filtersRef.current || [],
|
||||
|
|
@ -381,33 +362,31 @@ export function AggregationWidgetComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// 선택 변경 이벤트 (체크박스 선택 등)
|
||||
const handleSelectionChange = (event: CustomEvent) => {
|
||||
const { componentId, selectedData } = event.detail || {};
|
||||
if (componentId === dataSourceComponentId && Array.isArray(selectedData)) {
|
||||
// 선택된 데이터만 집계
|
||||
const filteredData = applyFilters(
|
||||
selectedData,
|
||||
filtersRef.current || [],
|
||||
filterLogic,
|
||||
formDataRef.current,
|
||||
selectedRowsRef.current
|
||||
);
|
||||
setData(filteredData);
|
||||
// 리피터 데이터 변경
|
||||
const handleRepeaterDataChange = (event: CustomEvent<RepeaterDataChangeDetail>) => {
|
||||
const { componentId, data: eventData, selectedData } = event.detail || {};
|
||||
if (componentId === dataSourceComponentId) {
|
||||
const rows = selectedData || eventData || [];
|
||||
if (Array.isArray(rows)) {
|
||||
const filteredData = applyFilters(
|
||||
rows,
|
||||
filtersRef.current || [],
|
||||
filterLogic,
|
||||
formDataRef.current,
|
||||
selectedRowsRef.current
|
||||
);
|
||||
setData(filteredData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 리피터 데이터 변경 이벤트
|
||||
window.addEventListener("repeaterDataChange" as any, handleDataChange);
|
||||
// 테이블 리스트 데이터 변경 이벤트
|
||||
window.addEventListener("tableListDataChange" as any, handleDataChange);
|
||||
// 선택 변경 이벤트
|
||||
window.addEventListener("selectionChange" as any, handleSelectionChange);
|
||||
// V2 표준 이벤트만 구독
|
||||
const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange);
|
||||
const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("repeaterDataChange" as any, handleDataChange);
|
||||
window.removeEventListener("tableListDataChange" as any, handleDataChange);
|
||||
window.removeEventListener("selectionChange" as any, handleSelectionChange);
|
||||
unsubscribeTableList();
|
||||
unsubscribeRepeater();
|
||||
};
|
||||
}, [dataSourceType, dataSourceComponentId, isDesignMode, filterLogic]);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import { cn } from "@/lib/utils";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import DynamicComponentRenderer from "@/lib/registry/DynamicComponentRenderer";
|
||||
|
||||
// V2 이벤트 시스템
|
||||
import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events";
|
||||
|
||||
interface RepeatContainerComponentProps extends ComponentRendererProps {
|
||||
config?: RepeatContainerConfig;
|
||||
// 외부에서 데이터를 직접 전달받을 수 있음
|
||||
|
|
@ -136,7 +139,10 @@ export function RepeatContainerComponent({
|
|||
}
|
||||
}, [externalData]);
|
||||
|
||||
// 컴포넌트 데이터 변경 이벤트 리스닝 (componentId 또는 tableName으로 매칭)
|
||||
// ============================================================
|
||||
// 컴포넌트 데이터 변경 이벤트 리스닝 (V2 표준 이벤트)
|
||||
// componentId 또는 tableName으로 매칭
|
||||
// ============================================================
|
||||
useEffect(() => {
|
||||
if (isDesignMode) return;
|
||||
|
||||
|
|
@ -147,19 +153,12 @@ export function RepeatContainerComponent({
|
|||
effectiveTableName,
|
||||
});
|
||||
|
||||
// dataSourceComponentId가 없어도 테이블명으로 매칭 가능
|
||||
const handleDataChange = (event: CustomEvent) => {
|
||||
const { componentId, tableName: eventTableName, data: eventData } = event.detail || {};
|
||||
|
||||
console.log("📩 리피터 컨테이너 이벤트 수신:", {
|
||||
eventType: event.type,
|
||||
fromComponentId: componentId,
|
||||
fromTableName: eventTableName,
|
||||
dataCount: Array.isArray(eventData) ? eventData.length : 0,
|
||||
myDataSourceComponentId: dataSourceComponentId,
|
||||
myEffectiveTableName: effectiveTableName,
|
||||
});
|
||||
|
||||
// 공통 데이터 처리 함수
|
||||
const processIncomingData = (
|
||||
componentId: string | undefined,
|
||||
eventTableName: string | undefined,
|
||||
eventData: any[]
|
||||
) => {
|
||||
// 1. 명시적으로 dataSourceComponentId가 설정된 경우 해당 컴포넌트만 매칭
|
||||
if (dataSourceComponentId) {
|
||||
if (componentId === dataSourceComponentId && Array.isArray(eventData)) {
|
||||
|
|
@ -167,8 +166,6 @@ export function RepeatContainerComponent({
|
|||
setData(eventData);
|
||||
setCurrentPage(1);
|
||||
setSelectedIndices([]);
|
||||
} else {
|
||||
console.log("⚠️ 리피터: 컴포넌트 ID 불일치로 무시", { expected: dataSourceComponentId, received: componentId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -179,17 +176,28 @@ export function RepeatContainerComponent({
|
|||
setData(eventData);
|
||||
setCurrentPage(1);
|
||||
setSelectedIndices([]);
|
||||
} else if (effectiveTableName) {
|
||||
console.log("⚠️ 리피터: 테이블명 불일치로 무시", { expected: effectiveTableName, received: eventTableName });
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("repeaterDataChange" as any, handleDataChange);
|
||||
window.addEventListener("tableListDataChange" as any, handleDataChange);
|
||||
// 테이블 리스트 데이터 변경 이벤트 (V2 표준)
|
||||
const handleTableListDataChange = (event: CustomEvent<TableListDataChangeDetail>) => {
|
||||
const { componentId, tableName: eventTableName, data: eventData } = event.detail || {};
|
||||
processIncomingData(componentId, eventTableName, eventData);
|
||||
};
|
||||
|
||||
// 리피터 데이터 변경 이벤트 (V2 표준)
|
||||
const handleRepeaterDataChange = (event: CustomEvent<RepeaterDataChangeDetail>) => {
|
||||
const { componentId, tableName: eventTableName, data: eventData } = event.detail || {};
|
||||
processIncomingData(componentId, eventTableName, eventData);
|
||||
};
|
||||
|
||||
// V2 표준 이벤트 구독
|
||||
const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange);
|
||||
const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("repeaterDataChange" as any, handleDataChange);
|
||||
window.removeEventListener("tableListDataChange" as any, handleDataChange);
|
||||
unsubscribeTableList();
|
||||
unsubscribeRepeater();
|
||||
};
|
||||
}, [component?.id, dataSourceType, dataSourceComponentId, effectiveTableName, isDesignMode]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* V2 컴포넌트 간 통신 이벤트 타입 정의
|
||||
*
|
||||
* 모든 V2 컴포넌트는 이 파일에 정의된 이벤트 타입을 사용해야 합니다.
|
||||
* 이벤트 발행/구독 시 타입 안전성을 보장합니다.
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 이벤트 상세 데이터 타입 (event.detail)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 테이블 리스트 데이터 변경 이벤트
|
||||
* 발행: v2-table-list
|
||||
* 구독: v2-aggregation-widget, v2-repeat-container
|
||||
*/
|
||||
export interface TableListDataChangeDetail {
|
||||
componentId: string;
|
||||
tableName: string;
|
||||
data: any[];
|
||||
selectedRows: string[] | number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리피터 데이터 변경 이벤트
|
||||
* 발행: v2-unified-repeater
|
||||
* 구독: v2-aggregation-widget, v2-repeat-container
|
||||
*/
|
||||
export interface RepeaterDataChangeDetail {
|
||||
componentId: string;
|
||||
tableName: string;
|
||||
data: any[];
|
||||
selectedData?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 저장 전 이벤트
|
||||
* 발행: buttonActions, UnifiedFormContext
|
||||
* 구독: v2-unified-repeater, simple-repeater-table, modal-repeater-table 등
|
||||
*/
|
||||
export interface BeforeFormSaveDetail {
|
||||
formData: Record<string, any>;
|
||||
skipDefaultSave?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 저장 후 이벤트
|
||||
* 발행: UnifiedFormContext
|
||||
* 구독: 저장 결과 처리 컴포넌트들
|
||||
*/
|
||||
export interface AfterFormSaveDetail {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리피터 저장 이벤트 (마스터-디테일 FK 연결용)
|
||||
* 발행: InteractiveScreenViewerDynamic
|
||||
* 구독: v2-unified-repeater
|
||||
*/
|
||||
export interface RepeaterSaveDetail {
|
||||
parentId?: string | number;
|
||||
masterRecordId: string | number;
|
||||
mainFormData: Record<string, any>;
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 새로고침 이벤트
|
||||
* 발행: v2-button-primary, buttonActions
|
||||
* 구독: v2-table-list, v2-split-panel-layout
|
||||
*/
|
||||
export interface RefreshTableDetail {
|
||||
tableName?: string;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 디스플레이 새로고침 이벤트
|
||||
* 발행: buttonActions, InteractiveScreenViewerDynamic
|
||||
* 구독: v2-card-display
|
||||
*/
|
||||
export interface RefreshCardDisplayDetail {
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 간 데이터 전달 이벤트
|
||||
* 발행: buttonActions
|
||||
* 구독: v2-unified-repeater
|
||||
*/
|
||||
export interface ComponentDataTransferDetail {
|
||||
sourceComponentId: string;
|
||||
targetComponentId: string;
|
||||
data: any[];
|
||||
mode: "append" | "replace" | "merge";
|
||||
mappingRules?: Array<{
|
||||
sourceField: string;
|
||||
targetField: string;
|
||||
defaultValue?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할 패널 간 데이터 전달 이벤트
|
||||
* 발행: buttonActions
|
||||
* 구독: v2-unified-repeater, repeater-field-group
|
||||
*/
|
||||
export interface SplitPanelDataTransferDetail {
|
||||
sourcePosition: "left" | "right";
|
||||
targetPosition: "left" | "right";
|
||||
data: any[];
|
||||
mode: "append" | "replace" | "merge";
|
||||
mappingRules?: Array<{
|
||||
sourceField: string;
|
||||
targetField: string;
|
||||
defaultValue?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 연관 데이터 버튼 선택 이벤트
|
||||
* 발행: related-data-buttons
|
||||
* 구독: v2-table-list
|
||||
*/
|
||||
export interface RelatedButtonSelectDetail {
|
||||
targetTable: string;
|
||||
filterColumn: string;
|
||||
filterValue: any;
|
||||
selectedData?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 제어 이벤트
|
||||
*/
|
||||
export interface EditModalDetail {
|
||||
screenId?: number;
|
||||
recordId?: string | number;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 이벤트 이름 상수
|
||||
// ============================================================
|
||||
|
||||
export const V2_EVENTS = {
|
||||
// 데이터 변경 이벤트
|
||||
TABLE_LIST_DATA_CHANGE: "tableListDataChange",
|
||||
REPEATER_DATA_CHANGE: "repeaterDataChange",
|
||||
|
||||
// 폼 저장 이벤트
|
||||
BEFORE_FORM_SAVE: "beforeFormSave",
|
||||
AFTER_FORM_SAVE: "afterFormSave",
|
||||
REPEATER_SAVE: "repeaterSave",
|
||||
|
||||
// UI 갱신 이벤트
|
||||
REFRESH_TABLE: "refreshTable",
|
||||
REFRESH_CARD_DISPLAY: "refreshCardDisplay",
|
||||
|
||||
// 데이터 전달 이벤트
|
||||
COMPONENT_DATA_TRANSFER: "componentDataTransfer",
|
||||
SPLIT_PANEL_DATA_TRANSFER: "splitPanelDataTransfer",
|
||||
|
||||
// 모달 제어 이벤트
|
||||
OPEN_EDIT_MODAL: "openEditModal",
|
||||
CLOSE_EDIT_MODAL: "closeEditModal",
|
||||
SAVE_SUCCESS_IN_MODAL: "saveSuccessInModal",
|
||||
|
||||
// 연관 데이터 버튼 이벤트
|
||||
RELATED_BUTTON_SELECT: "related-button-select",
|
||||
RELATED_BUTTON_REGISTER: "related-button-register",
|
||||
RELATED_BUTTON_UNREGISTER: "related-button-unregister",
|
||||
} as const;
|
||||
|
||||
// ============================================================
|
||||
// Window EventMap 확장 (타입 안전한 이벤트 리스너)
|
||||
// ============================================================
|
||||
|
||||
declare global {
|
||||
interface WindowEventMap {
|
||||
// 데이터 변경 이벤트
|
||||
[V2_EVENTS.TABLE_LIST_DATA_CHANGE]: CustomEvent<TableListDataChangeDetail>;
|
||||
[V2_EVENTS.REPEATER_DATA_CHANGE]: CustomEvent<RepeaterDataChangeDetail>;
|
||||
|
||||
// 폼 저장 이벤트
|
||||
[V2_EVENTS.BEFORE_FORM_SAVE]: CustomEvent<BeforeFormSaveDetail>;
|
||||
[V2_EVENTS.AFTER_FORM_SAVE]: CustomEvent<AfterFormSaveDetail>;
|
||||
[V2_EVENTS.REPEATER_SAVE]: CustomEvent<RepeaterSaveDetail>;
|
||||
|
||||
// UI 갱신 이벤트
|
||||
[V2_EVENTS.REFRESH_TABLE]: CustomEvent<RefreshTableDetail>;
|
||||
[V2_EVENTS.REFRESH_CARD_DISPLAY]: CustomEvent<RefreshCardDisplayDetail>;
|
||||
|
||||
// 데이터 전달 이벤트
|
||||
[V2_EVENTS.COMPONENT_DATA_TRANSFER]: CustomEvent<ComponentDataTransferDetail>;
|
||||
[V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER]: CustomEvent<SplitPanelDataTransferDetail>;
|
||||
|
||||
// 연관 데이터 버튼 이벤트
|
||||
[V2_EVENTS.RELATED_BUTTON_SELECT]: CustomEvent<RelatedButtonSelectDetail>;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 유틸리티 함수
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 타입 안전한 이벤트 발행 함수
|
||||
*/
|
||||
export function dispatchV2Event<K extends keyof WindowEventMap>(
|
||||
eventName: K,
|
||||
detail: WindowEventMap[K] extends CustomEvent<infer D> ? D : never
|
||||
): void {
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 타입 안전한 이벤트 구독 함수
|
||||
*/
|
||||
export function subscribeV2Event<K extends keyof WindowEventMap>(
|
||||
eventName: K,
|
||||
handler: (event: WindowEventMap[K]) => void
|
||||
): () => void {
|
||||
if (typeof window === "undefined") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
window.addEventListener(eventName, handler as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener(eventName, handler as EventListener);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 내보내기
|
||||
// ============================================================
|
||||
|
||||
export type V2EventName = typeof V2_EVENTS[keyof typeof V2_EVENTS];
|
||||
Binary file not shown.
|
|
@ -0,0 +1,116 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
REM 스크립트가 있는 디렉토리에서 루트로 이동
|
||||
cd /d "%~dp0\..\.."
|
||||
|
||||
REM 시작 시간 기록
|
||||
set START_TIME=%DATE% %TIME%
|
||||
|
||||
echo ============================================
|
||||
echo WACE 솔루션 - 전체 서비스 시작 (병렬 최적화)
|
||||
echo ============================================
|
||||
echo [시작 시간] %START_TIME%
|
||||
echo.
|
||||
|
||||
REM Docker Desktop 실행 확인
|
||||
echo [1/5] Docker Desktop 상태 확인 중...
|
||||
docker --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Docker Desktop이 실행되지 않았습니다!
|
||||
echo Docker Desktop을 먼저 실행해주세요.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [OK] Docker Desktop이 실행 중입니다.
|
||||
echo.
|
||||
|
||||
REM 기존 컨테이너 정리
|
||||
echo [2/5] 기존 컨테이너 정리 중...
|
||||
docker rm -f pms-backend-win pms-frontend-win 2>nul
|
||||
docker network rm pms-network 2>nul
|
||||
docker network create pms-network 2>nul
|
||||
echo [OK] 컨테이너 정리 완료
|
||||
echo.
|
||||
|
||||
REM 병렬 빌드 (docker-compose 자체가 병렬 처리)
|
||||
echo [3/5] 이미지 빌드 중... (백엔드 + 프론트엔드 병렬)
|
||||
echo 이 작업은 시간이 걸릴 수 있습니다...
|
||||
echo.
|
||||
|
||||
REM 백엔드 빌드
|
||||
docker-compose -f docker-compose.backend.win.yml build
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] 백엔드 빌드 실패!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [OK] 백엔드 빌드 완료
|
||||
echo.
|
||||
|
||||
REM 프론트엔드 빌드
|
||||
docker-compose -f docker-compose.frontend.win.yml build
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] 프론트엔드 빌드 실패!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [OK] 프론트엔드 빌드 완료
|
||||
echo.
|
||||
|
||||
REM 기존 컨테이너 정리 후 서비스 시작
|
||||
echo [4/5] 서비스 시작 중...
|
||||
docker-compose -f docker-compose.backend.win.yml down -v 2>nul
|
||||
docker-compose -f docker-compose.frontend.win.yml down -v 2>nul
|
||||
|
||||
REM 백엔드 시작
|
||||
echo 백엔드 서비스 시작...
|
||||
docker-compose -f docker-compose.backend.win.yml up -d
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] 백엔드 시작 실패!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 프론트엔드 시작
|
||||
echo 프론트엔드 서비스 시작...
|
||||
docker-compose -f docker-compose.frontend.win.yml up -d
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] 프론트엔드 시작 실패!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [OK] 서비스 시작 완료
|
||||
echo.
|
||||
|
||||
REM 안정화 대기
|
||||
echo [5/5] 서비스 안정화 대기 중... (10초)
|
||||
timeout /t 10 /nobreak >nul
|
||||
echo.
|
||||
|
||||
echo ============================================
|
||||
echo [완료] 모든 서비스가 시작되었습니다!
|
||||
echo ============================================
|
||||
echo.
|
||||
echo [DATABASE] PostgreSQL: http://39.117.244.52:11132
|
||||
echo [BACKEND] Node.js API: http://localhost:8080/api
|
||||
echo [FRONTEND] Next.js: http://localhost:9771
|
||||
echo.
|
||||
echo [서비스 상태 확인]
|
||||
echo docker-compose -f docker-compose.backend.win.yml ps
|
||||
echo docker-compose -f docker-compose.frontend.win.yml ps
|
||||
echo.
|
||||
echo [로그 확인]
|
||||
echo 백엔드: docker-compose -f docker-compose.backend.win.yml logs -f
|
||||
echo 프론트엔드: docker-compose -f docker-compose.frontend.win.yml logs -f
|
||||
echo.
|
||||
echo [서비스 중지]
|
||||
echo scripts\dev\stop-all.bat
|
||||
echo.
|
||||
|
||||
set END_TIME=%DATE% %TIME%
|
||||
echo [종료 시간] %END_TIME%
|
||||
echo ============================================
|
||||
|
||||
pause
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
# WACE 솔루션 - 전체 서비스 시작 (병렬 최적화) - PowerShell 버전
|
||||
# 실행 방법: powershell -ExecutionPolicy Bypass -File .\scripts\dev\start-all-parallel.ps1
|
||||
|
||||
# UTF-8 출력 설정
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# 스크립트 위치에서 루트 디렉토리로 이동
|
||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
Set-Location (Join-Path $scriptPath "..\..")
|
||||
|
||||
# 시작 시간 기록
|
||||
$startTime = Get-Date
|
||||
$startTimeFormatted = $startTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "WACE 솔루션 - 전체 서비스 시작 (병렬 최적화)" -ForegroundColor Cyan
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "[시작 시간] $startTimeFormatted" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Docker Desktop 실행 확인
|
||||
Write-Host "[1/5] Docker Desktop 상태 확인 중..." -ForegroundColor White
|
||||
$dockerCheck = docker --version 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Docker Desktop이 실행되지 않았습니다!" -ForegroundColor Red
|
||||
Write-Host " Docker Desktop을 먼저 실행해주세요." -ForegroundColor Red
|
||||
Read-Host "계속하려면 Enter를 누르세요"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] Docker Desktop이 실행 중입니다." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 기존 컨테이너 정리
|
||||
Write-Host "[2/5] 기존 컨테이너 정리 중..." -ForegroundColor White
|
||||
docker rm -f pms-backend-win pms-frontend-win 2>$null | Out-Null
|
||||
docker network rm pms-network 2>$null | Out-Null
|
||||
docker network create pms-network 2>$null | Out-Null
|
||||
Write-Host "[OK] 컨테이너 정리 완료" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 병렬 빌드 시작
|
||||
$parallelStart = Get-Date
|
||||
Write-Host "[3/5] 이미지 빌드 중... (백엔드 + 프론트엔드 병렬)" -ForegroundColor White
|
||||
Write-Host " 이 작업은 시간이 걸릴 수 있습니다..." -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# 병렬 빌드 실행
|
||||
$backendBuildJob = Start-Job -ScriptBlock {
|
||||
param($workDir)
|
||||
Set-Location $workDir
|
||||
$output = docker-compose -f docker-compose.backend.win.yml build 2>&1
|
||||
return @{
|
||||
Success = $LASTEXITCODE -eq 0
|
||||
Output = $output
|
||||
}
|
||||
} -ArgumentList $PWD.Path
|
||||
|
||||
$frontendBuildJob = Start-Job -ScriptBlock {
|
||||
param($workDir)
|
||||
Set-Location $workDir
|
||||
$output = docker-compose -f docker-compose.frontend.win.yml build 2>&1
|
||||
return @{
|
||||
Success = $LASTEXITCODE -eq 0
|
||||
Output = $output
|
||||
}
|
||||
} -ArgumentList $PWD.Path
|
||||
|
||||
Write-Host " 백엔드 빌드 진행 중..." -ForegroundColor Gray
|
||||
Write-Host " 프론트엔드 빌드 진행 중..." -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# 빌드 완료 대기
|
||||
$null = Wait-Job -Job $backendBuildJob, $frontendBuildJob
|
||||
|
||||
$backendResult = Receive-Job -Job $backendBuildJob
|
||||
$frontendResult = Receive-Job -Job $frontendBuildJob
|
||||
|
||||
Remove-Job -Job $backendBuildJob, $frontendBuildJob -Force
|
||||
|
||||
# 빌드 결과 확인
|
||||
$buildFailed = $false
|
||||
|
||||
if ($backendResult.Success) {
|
||||
Write-Host "[OK] 백엔드 빌드 완료" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[ERROR] 백엔드 빌드 실패!" -ForegroundColor Red
|
||||
Write-Host $backendResult.Output -ForegroundColor Red
|
||||
$buildFailed = $true
|
||||
}
|
||||
|
||||
if ($frontendResult.Success) {
|
||||
Write-Host "[OK] 프론트엔드 빌드 완료" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[ERROR] 프론트엔드 빌드 실패!" -ForegroundColor Red
|
||||
Write-Host $frontendResult.Output -ForegroundColor Red
|
||||
$buildFailed = $true
|
||||
}
|
||||
|
||||
if ($buildFailed) {
|
||||
Read-Host "빌드 실패. Enter를 누르면 종료됩니다"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$parallelEnd = Get-Date
|
||||
$parallelDuration = ($parallelEnd - $parallelStart).TotalSeconds
|
||||
Write-Host "[INFO] 빌드 소요 시간: $([math]::Round($parallelDuration))초" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# 서비스 시작
|
||||
$serviceStart = Get-Date
|
||||
Write-Host "[4/5] 서비스 시작 중..." -ForegroundColor White
|
||||
|
||||
# 기존 컨테이너 정리
|
||||
docker-compose -f docker-compose.backend.win.yml down -v 2>$null | Out-Null
|
||||
docker-compose -f docker-compose.frontend.win.yml down -v 2>$null | Out-Null
|
||||
|
||||
# 백엔드 시작
|
||||
Write-Host " 백엔드 서비스 시작..." -ForegroundColor Gray
|
||||
docker-compose -f docker-compose.backend.win.yml up -d 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] 백엔드 시작 실패!" -ForegroundColor Red
|
||||
Read-Host "계속하려면 Enter를 누르세요"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 프론트엔드 시작
|
||||
Write-Host " 프론트엔드 서비스 시작..." -ForegroundColor Gray
|
||||
docker-compose -f docker-compose.frontend.win.yml up -d 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] 프론트엔드 시작 실패!" -ForegroundColor Red
|
||||
Read-Host "계속하려면 Enter를 누르세요"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[OK] 서비스 시작 완료" -ForegroundColor Green
|
||||
|
||||
$serviceEnd = Get-Date
|
||||
$serviceDuration = ($serviceEnd - $serviceStart).TotalSeconds
|
||||
Write-Host "[INFO] 서비스 시작 소요 시간: $([math]::Round($serviceDuration))초" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# 안정화 대기
|
||||
Write-Host "[5/5] 서비스 안정화 대기 중... (10초)" -ForegroundColor White
|
||||
Start-Sleep -Seconds 10
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "[완료] 모든 서비스가 시작되었습니다!" -ForegroundColor Green
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "[DATABASE] PostgreSQL: http://39.117.244.52:11132" -ForegroundColor White
|
||||
Write-Host "[BACKEND] Node.js API: http://localhost:8080/api" -ForegroundColor White
|
||||
Write-Host "[FRONTEND] Next.js: http://localhost:9771" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "[서비스 상태 확인]" -ForegroundColor Yellow
|
||||
Write-Host " docker-compose -f docker-compose.backend.win.yml ps" -ForegroundColor Gray
|
||||
Write-Host " docker-compose -f docker-compose.frontend.win.yml ps" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "[로그 확인]" -ForegroundColor Yellow
|
||||
Write-Host " 백엔드: docker-compose -f docker-compose.backend.win.yml logs -f" -ForegroundColor Gray
|
||||
Write-Host " 프론트엔드: docker-compose -f docker-compose.frontend.win.yml logs -f" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "[서비스 중지]" -ForegroundColor Yellow
|
||||
Write-Host " .\scripts\dev\stop-all.ps1" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# 종료 시간 계산
|
||||
$endTime = Get-Date
|
||||
$endTimeFormatted = $endTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
$totalDuration = ($endTime - $startTime).TotalSeconds
|
||||
$minutes = [math]::Floor($totalDuration / 60)
|
||||
$seconds = [math]::Round($totalDuration % 60)
|
||||
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "[종료 시간] $endTimeFormatted" -ForegroundColor Yellow
|
||||
Write-Host "[총 소요 시간] ${minutes}분 ${seconds}초" -ForegroundColor Yellow
|
||||
Write-Host " - 빌드: $([math]::Round($parallelDuration))초" -ForegroundColor Gray
|
||||
Write-Host " - 서비스 시작: $([math]::Round($serviceDuration))초" -ForegroundColor Gray
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
|
||||
Read-Host "계속하려면 Enter를 누르세요"
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
|
||||
REM 스크립트가 있는 디렉토리에서 루트로 이동
|
||||
cd /d "%~dp0\..\.."
|
||||
|
||||
echo ============================================
|
||||
echo WACE 솔루션 - 전체 서비스 중지
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
echo 🛑 백엔드 서비스 중지 중...
|
||||
docker-compose -f docker-compose.backend.win.yml down -v 2>nul
|
||||
echo ✅ 백엔드 서비스 중지 완료
|
||||
echo.
|
||||
|
||||
echo 🛑 프론트엔드 서비스 중지 중...
|
||||
docker-compose -f docker-compose.frontend.win.yml down -v 2>nul
|
||||
echo ✅ 프론트엔드 서비스 중지 완료
|
||||
echo.
|
||||
|
||||
echo 🧹 네트워크 정리 중...
|
||||
docker network rm pms-network 2>nul
|
||||
echo.
|
||||
|
||||
echo ============================================
|
||||
echo 🎉 모든 서비스가 중지되었습니다!
|
||||
echo ============================================
|
||||
|
||||
pause
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# WACE 솔루션 - 전체 서비스 중지 - PowerShell 버전
|
||||
# 실행 방법: powershell -ExecutionPolicy Bypass -File .\scripts\dev\stop-all.ps1
|
||||
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# 스크립트 위치에서 루트 디렉토리로 이동
|
||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
Set-Location (Join-Path $scriptPath "..\..")
|
||||
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "WACE 솔루션 - 전체 서비스 중지" -ForegroundColor Cyan
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🛑 백엔드 서비스 중지 중..." -ForegroundColor Yellow
|
||||
docker-compose -f docker-compose.backend.win.yml down -v 2>$null
|
||||
Write-Host "✅ 백엔드 서비스 중지 완료" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🛑 프론트엔드 서비스 중지 중..." -ForegroundColor Yellow
|
||||
docker-compose -f docker-compose.frontend.win.yml down -v 2>$null
|
||||
Write-Host "✅ 프론트엔드 서비스 중지 완료" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "🧹 네트워크 정리 중..." -ForegroundColor Yellow
|
||||
docker network rm pms-network 2>$null
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "🎉 모든 서비스가 중지되었습니다!" -ForegroundColor Green
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
|
||||
Read-Host "계속하려면 Enter를 누르세요"
|
||||
Binary file not shown.
Loading…
Reference in New Issue