docs: 다양한 문서 및 가이드 업데이트

- 여러 문서의 내용을 업데이트하여 최신 정보를 반영하였습니다.
- 컴포넌트 개발 가이드와 관련된 문서의 목차를 재구성하고, V2 및 Zod 레이아웃 시스템에 대한 내용을 추가하였습니다.
- 화면 컴포넌트 개발 가이드를 개선하여 핵심 원칙과 패턴을 명확히 설명하였습니다.
- 불필요한 문서 및 가이드를 삭제하고, 통합된 가이드를 통해 개발자들이 쉽게 참고할 수 있도록 하였습니다.
This commit is contained in:
kjs
2026-01-28 17:36:19 +09:00
parent e0ee375f01
commit 95bef976a5
276 changed files with 2544 additions and 2495 deletions
@@ -527,7 +527,7 @@ flowchart TB
subgraph Usage["사용처"]
U1[NumberingRuleDesigner.tsx]
U2[UnifiedSelect.tsx]
U2[V2Select.tsx]
U3[screenManagementService.ts]
end
@@ -185,7 +185,7 @@ POST /api/screen-management/screens/:screenId/layout-v2
| `@/lib/registry/components/flow-widget` | 플로우 위젯 |
| `@/lib/registry/components/category-management` | 카테고리 관리 |
| `@/lib/registry/components/pivot-table` | 피벗 테이블 |
| `@/lib/registry/components/unified-grid` | 통합 그리드 |
| `@/lib/registry/components/v2-grid` | 통합 그리드 |
---
@@ -192,7 +192,7 @@ async function verifyRenderingEquality(layoutId: number) {
| 6 | select-basic | 129 | 100% | 낮음 |
| 7 | split-panel-layout | 129 | 100% | 높음 |
| 8 | date-input | 116 | 100% | 낮음 |
| 9 | unified-list | 97 | 100% | 높음 |
| 9 | v2-list | 97 | 100% | 높음 |
| 10 | number-input | 87 | 100% | 낮음 |
### 4.2 발견된 문제점
@@ -433,7 +433,7 @@ DROP TABLE screen_layouts_v2;
- [ ] select-basic
- [ ] split-panel-layout
- [ ] date-input
- [ ] unified-list
- [ ] v2-list
- [ ] number-input
### Step 3: 마이그레이션 스크립트
+304
View File
@@ -0,0 +1,304 @@
# vexplor 프로젝트 NCP Kubernetes 배포 가이드
## 배포 환경
- **Kubernetes 클러스터**: NCP Kubernetes
- **네임스페이스**: apps
- **GitOps 도구**: Argo CD (https://argocd.kpslp.kr)
- **CI/CD**: Jenkins (Kaniko 빌드)
- **컨테이너 레지스트리**: registry.kpslp.kr
## 전제 조건
### 1. GitLab 레포지토리
- [x] 프로젝트 코드 레포: 이미 생성됨 (현재 레포)
- [ ] Helm Charts 레포: `https://gitlab.kpslp.kr/root/helm-charts` 접근 권한 필요
### 2. 필요한 권한
- [ ] GitLab 계정 및 레포지토리 접근 권한
- [ ] Jenkins 프로젝트 생성 권한 또는 담당자 요청
- [ ] Argo CD 접속 계정
- [ ] Container Registry 푸시 권한
---
## 배포 단계
### Step 1: Helm Charts 레포지토리 설정
김욱동 책임님께 다음 사항을 요청하세요:
```
안녕하세요.
vexplor 프로젝트 배포를 위해 다음 작업이 필요합니다:
1. helm-charts 레포지토리 접근 권한 부여
- 레포지토리: https://gitlab.kpslp.kr/root/helm-charts
- 현재 404 오류로 접근 불가
- 계정: [본인 GitLab 사용자명]
2. values 파일 업로드
- 첨부된 values_vexplor.yaml 파일을
- kpslp/values_vexplor.yaml 경로에 업로드해주시거나
- 업로드 방법을 안내해주세요
3. Jenkins 프로젝트 생성
- 프로젝트명: vexplor
- Git 레포지토리: [현재 프로젝트 GitLab URL]
- Jenkinsfile: 프로젝트 루트에 이미 준비됨
감사합니다.
```
**첨부 파일**: `values_vexplor.yaml` (프로젝트 루트에 생성됨)
---
### Step 2: Jenkins 프로젝트 등록
Jenkins에서 새 파이프라인 프로젝트를 생성합니다:
1. **Jenkins 접속** (URL은 담당자에게 문의)
2. **New Item** 클릭
3. **프로젝트명**: `vexplor`
4. **Pipeline** 선택
5. **Pipeline 설정**:
- Definition: `Pipeline script from SCM`
- SCM: `Git`
- Repository URL: `[현재 프로젝트 GitLab URL]`
- Credentials: `gitlab_userpass_root` (또는 담당자가 안내한 credential)
- Branch: `*/main`
- Script Path: `Jenkinsfile`
---
### Step 3: Argo CD 애플리케이션 등록
1. **Argo CD 접속**: https://argocd.kpslp.kr
2. **New App 생성**:
- **Application Name**: `vexplor`
- **Project**: `default`
- **Sync Policy**: `Automatic` (자동 배포) 또는 `Manual` (수동 배포)
- **Auto-Create Namespace**: ✓ (체크)
3. **Source 설정**:
- **Repository URL**: `https://gitlab.kpslp.kr/root/helm-charts`
- **Revision**: `HEAD` 또는 `main`
- **Path**: `kpslp`
- **Helm Values**: `values_vexplor.yaml`
4. **Destination 설정**:
- **Cluster URL**: `https://kubernetes.default.svc` (기본값)
- **Namespace**: `apps`
5. **Create** 클릭
---
### Step 4: 첫 배포 실행
#### 4-1. Git Push로 Jenkins 빌드 트리거
```bash
git add .
git commit -m "feat: NCP Kubernetes 배포 설정 완료"
git push origin main
```
#### 4-2. Jenkins 빌드 모니터링
1. Jenkins에서 `vexplor` 프로젝트 열기
2. 빌드 시작 확인 (자동 트리거 또는 수동 빌드)
3. 로그 확인:
- **Checkout**: Git 소스 다운로드
- **Build**: Docker 이미지 빌드 (`registry.kpslp.kr/slp/vexplor:xxxxx`)
- **Update Image Tag**: helm-charts 레포의 values 파일 업데이트
#### 4-3. Argo CD 배포 확인
1. Argo CD 대시보드에서 `vexplor` 앱 열기
2. **Sync Status**: `OutOfSync``Synced` 변경 확인
3. **Health Status**: `Progressing``Healthy` 변경 확인
4. Pod 상태 확인 (Running 상태여야 함)
---
## 배포 후 확인사항
### 1. Pod 상태 확인
```bash
kubectl get pods -n apps | grep vexplor
```
**예상 출력**:
```
vexplor-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
```
### 2. 서비스 확인
```bash
kubectl get svc -n apps | grep vexplor
```
### 3. Ingress 확인
```bash
kubectl get ingress -n apps | grep vexplor
```
### 4. 로그 확인
```bash
# 전체 로그
kubectl logs -n apps -l app=vexplor
# 최근 50줄
kubectl logs -n apps -l app=vexplor --tail=50
# 실시간 로그 (스트리밍)
kubectl logs -n apps -l app=vexplor -f
```
### 5. 애플리케이션 접속
- **URL**: `https://vexplor.kpslp.kr` (values 파일에 설정한 도메인)
- **헬스체크**: `https://vexplor.kpslp.kr/api/health`
---
## 트러블슈팅
### 문제 1: Jenkins 빌드 실패
**증상**: Build 단계에서 에러 발생
**확인사항**:
- Docker 이미지 빌드 로그 확인
- `Dockerfile`이 프로젝트 루트에 있는지 확인
- 빌드 컨텍스트에 필요한 파일들이 있는지 확인
**해결**:
```bash
# 로컬에서 Docker 빌드 테스트
docker build -f Dockerfile -t vexplor:test .
```
### 문제 2: helm-charts 레포 푸시 실패
**증상**: Update Image Tag 단계에서 실패
**원인**: `gitlab_userpass_root` credential 문제 또는 권한 부족
**해결**: 김욱동 책임님께 credential 확인 요청
### 문제 3: Argo CD Sync 실패
**증상**: `OutOfSync` 상태에서 변경 없음
**확인사항**:
- values 파일이 올바른 경로에 있는지 (`kpslp/values_vexplor.yaml`)
- Argo CD가 helm-charts 레포를 읽을 수 있는지
**해결**: Argo CD에서 수동 Sync 시도 또는 담당자에게 문의
### 문제 4: Pod가 CrashLoopBackOff 상태
**증상**: Pod가 계속 재시작됨
**확인**:
```bash
kubectl describe pod -n apps [pod-name]
kubectl logs -n apps [pod-name] --previous
```
**일반적인 원인**:
- 환경 변수 누락 (DATABASE_HOST 등)
- 데이터베이스 연결 실패
- 포트 바인딩 문제
**해결**:
1. `values_vexplor.yaml``env` 섹션 확인
2. 데이터베이스 서비스명 확인 (`postgres-service.apps.svc.cluster.local`)
3. Secret 설정 확인 (DB 비밀번호 등)
---
## 업데이트 배포 프로세스
코드 수정 후 배포 절차:
```bash
# 1. 코드 수정
git add .
git commit -m "feat: 새로운 기능 추가"
git push origin main
# 2. Jenkins 자동 빌드 (자동 트리거)
# - Git push 감지
# - Docker 이미지 빌드
# - 새 이미지 태그로 values 파일 업데이트
# 3. Argo CD 자동 배포 (Sync Policy가 Automatic인 경우)
# - helm-charts 레포 변경 감지
# - Kubernetes에 새 이미지 배포
# - Rolling Update 수행
```
**수동 배포**: Argo CD 대시보드에서 `Sync` 버튼 클릭
---
## 체크리스트
배포 전 확인사항:
- [ ] Jenkinsfile 수정 완료 (단일 이미지 빌드)
- [ ] Dockerfile 확인 (멀티스테이지 빌드)
- [ ] values_vexplor.yaml 작성 및 업로드
- [ ] Jenkins 프로젝트 생성
- [ ] Argo CD 애플리케이션 등록
- [ ] 환경 변수 설정 (DATABASE_HOST 등)
- [ ] Secret 생성 (DB 비밀번호 등)
- [ ] Ingress 도메인 설정
- [ ] 헬스체크 엔드포인트 확인 (`/api/health`)
---
## 참고 자료
- **Kaniko**: 컨테이너 내에서 Docker 이미지를 빌드하는 도구
- **GitOps**: Git을 Single Source of Truth로 사용하는 배포 방식
- **Argo CD**: GitOps를 위한 Kubernetes CD 도구
- **Helm**: Kubernetes 패키지 매니저
---
## 담당자 연락처
- **NCP 클러스터 관리**: 김욱동 책임 (엘에스티라유텍)
- **Bastion 서버**: 223.130.135.25:22 (Docker 직접 배포용 아님)
- **Argo CD**: https://argocd.kpslp.kr
- **Kubernetes 네임스페이스**: apps
---
## 추가 설정 (선택사항)
### PostgreSQL 데이터베이스 설정
클러스터 내부에 PostgreSQL이 없다면:
```yaml
# values_vexplor.yaml 에 추가
postgresql:
enabled: true
auth:
username: vexplor
password: changeme123 # Secret으로 관리 권장
database: vexplor
primary:
persistence:
enabled: true
size: 10Gi
```
### Secret 생성 (민감 정보)
```bash
kubectl create secret generic vexplor-secrets \
--from-literal=db-password='your-secure-password' \
--from-literal=jwt-secret='your-jwt-secret' \
-n apps
```
### 모니터링 (Prometheus + Grafana)
담당자에게 메트릭 수집 설정 요청
@@ -0,0 +1,58 @@
# 프로젝트 진행 상황 (2025-11-20)
## 작업 개요: 디지털 트윈 3D 야드 고도화 (동적 계층 구조)
### 1. 핵심 변경 사항
기존의 고정된 `Area` -> `Location` 2단계 구조를 유연한 **N-Level 동적 계층 구조**로 변경하고, 공간적 제약을 강화했습니다.
### 2. 완료된 작업
#### 데이터베이스
- **마이그레이션 실행**: `db/migrations/042_refactor_digital_twin_hierarchy.sql`
- **스키마 변경**:
- `digital_twin_layout` 테이블에 `hierarchy_config` (JSONB) 컬럼 추가
- `digital_twin_objects` 테이블에 `hierarchy_level`, `parent_key`, `external_key` 컬럼 추가
- 기존 하드코딩된 테이블 매핑 컬럼 제거
#### 백엔드 (Node.js)
- **API 추가/수정**:
- `POST /api/digital-twin/data/hierarchy`: 계층 설정에 따른 전체 데이터 조회
- `POST /api/digital-twin/data/children`: 특정 부모의 하위 데이터 조회
- 기존 레거시 API (`getWarehouses` 등) 호환성 유지
- **컨트롤러 수정**:
- `digitalTwinDataController.ts`: 동적 쿼리 생성 로직 구현
- `digitalTwinLayoutController.ts`: 레이아웃 저장/수정 시 `hierarchy_config` 및 객체 계층 정보 처리
#### 프론트엔드 (React)
- **신규 컴포넌트**: `HierarchyConfigPanel.tsx`
- 레벨 추가/삭제, 테이블 및 컬럼 매핑 설정 UI
- **유틸리티**: `spatialContainment.ts`
- `validateSpatialContainment`: 자식 객체가 부모 객체 내부에 있는지 검증 (AABB)
- `updateChildrenPositions`: 부모 이동 시 자식 객체 자동 이동 (그룹 이동)
- **에디터 통합 (`DigitalTwinEditor.tsx`)**:
- `HierarchyConfigPanel` 적용
- 동적 데이터 로드 로직 구현
- 3D 캔버스 드래그앤드롭 시 공간적 종속성 검증 적용
- 객체 이동 시 그룹 이동 적용
### 3. 현재 상태
- **백엔드 서버**: 재시작 완료, 정상 동작 중 (PostgreSQL 연결 이슈 해결됨)
- **DB**: 마이그레이션 스크립트 실행 완료
### 4. 다음 단계 (테스트 필요)
새로운 세션에서 다음 시나리오를 테스트해야 합니다:
1. **계층 설정**: 에디터에서 창고 -> 구역(Lv1) -> 위치(Lv2) 설정 및 매핑 저장
2. **배치 검증**:
- 구역 배치 후, 위치를 구역 **내부**에 배치 (성공해야 함)
- 위치를 구역 **외부**에 배치 (실패해야 함)
3. **이동 검증**: 구역 이동 시 내부의 위치들도 같이 따라오는지 확인
### 5. 관련 파일
- `frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx`
- `frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx`
- `frontend/components/admin/dashboard/widgets/yard-3d/spatialContainment.ts`
- `backend-node/src/controllers/digitalTwinDataController.ts`
- `backend-node/src/routes/digitalTwinRoutes.ts`
- `db/migrations/042_refactor_digital_twin_hierarchy.sql`
@@ -0,0 +1,773 @@
# 테이블 변경 이력 로그 시스템 구현 계획서
## 1. 개요
테이블 생성 시 해당 테이블의 변경 이력을 자동으로 기록하는 로그 테이블 생성 기능을 추가합니다.
사용자가 테이블을 생성할 때 로그 테이블 생성 여부를 선택할 수 있으며, 선택 시 자동으로 로그 테이블과 트리거가 생성됩니다.
## 2. 핵심 기능
### 2.1 로그 테이블 생성 옵션
- 테이블 생성 폼에 "변경 이력 로그 테이블 생성" 체크박스 추가
- 체크 시 `{원본테이블명}_log` 형식의 로그 테이블 자동 생성
### 2.2 로그 테이블 스키마 구조
```sql
CREATE TABLE {table_name}_log (
log_id SERIAL PRIMARY KEY, -- 로그 고유 ID
operation_type VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE
original_id {PK타입}, -- 원본 테이블의 PK 값
changed_column VARCHAR(100), -- 변경된 컬럼명 (UPDATE 시)
old_value TEXT, -- 변경 전 값
new_value TEXT, -- 변경 후 값
changed_by VARCHAR(50), -- 변경한 사용자 ID
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 변경 시각
ip_address VARCHAR(50), -- 변경 요청 IP
user_agent TEXT, -- 변경 요청 User-Agent
full_row_before JSONB, -- 변경 전 전체 행 (JSON)
full_row_after JSONB -- 변경 후 전체 행 (JSON)
);
CREATE INDEX idx_{table_name}_log_original_id ON {table_name}_log(original_id);
CREATE INDEX idx_{table_name}_log_changed_at ON {table_name}_log(changed_at);
CREATE INDEX idx_{table_name}_log_operation ON {table_name}_log(operation_type);
```
### 2.3 트리거 함수 생성
```sql
CREATE OR REPLACE FUNCTION {table_name}_log_trigger_func()
RETURNS TRIGGER AS $$
DECLARE
v_column_name TEXT;
v_old_value TEXT;
v_new_value TEXT;
v_user_id VARCHAR(50);
v_ip_address VARCHAR(50);
BEGIN
-- 세션 변수에서 사용자 정보 가져오기
v_user_id := current_setting('app.user_id', TRUE);
v_ip_address := current_setting('app.ip_address', TRUE);
IF (TG_OP = 'INSERT') THEN
INSERT INTO {table_name}_log (
operation_type, original_id, changed_by, ip_address,
full_row_after
) VALUES (
'INSERT', NEW.{pk_column}, v_user_id, v_ip_address,
row_to_json(NEW)::jsonb
);
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
-- 각 컬럼별로 변경사항 기록
FOR v_column_name IN
SELECT column_name
FROM information_schema.columns
WHERE table_name = TG_TABLE_NAME
LOOP
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT',
v_column_name, v_column_name)
INTO v_old_value, v_new_value
USING OLD, NEW;
IF v_old_value IS DISTINCT FROM v_new_value THEN
INSERT INTO {table_name}_log (
operation_type, original_id, changed_column,
old_value, new_value, changed_by, ip_address,
full_row_before, full_row_after
) VALUES (
'UPDATE', NEW.{pk_column}, v_column_name,
v_old_value, v_new_value, v_user_id, v_ip_address,
row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb
);
END IF;
END LOOP;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO {table_name}_log (
operation_type, original_id, changed_by, ip_address,
full_row_before
) VALUES (
'DELETE', OLD.{pk_column}, v_user_id, v_ip_address,
row_to_json(OLD)::jsonb
);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
```
### 2.4 트리거 생성
```sql
CREATE TRIGGER {table_name}_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON {table_name}
FOR EACH ROW EXECUTE FUNCTION {table_name}_log_trigger_func();
```
## 3. 데이터베이스 스키마 변경
### 3.1 table_type_mng 테이블 수정
```sql
ALTER TABLE table_type_mng
ADD COLUMN use_log_table VARCHAR(1) DEFAULT 'N';
COMMENT ON COLUMN table_type_mng.use_log_table IS '변경 이력 로그 테이블 사용 여부 (Y/N)';
```
### 3.2 새로운 관리 테이블 추가
```sql
CREATE TABLE table_log_config (
config_id SERIAL PRIMARY KEY,
original_table_name VARCHAR(100) NOT NULL,
log_table_name VARCHAR(100) NOT NULL,
trigger_name VARCHAR(100) NOT NULL,
trigger_function_name VARCHAR(100) NOT NULL,
is_active VARCHAR(1) DEFAULT 'Y',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
UNIQUE(original_table_name)
);
COMMENT ON TABLE table_log_config IS '테이블 로그 설정 관리';
COMMENT ON COLUMN table_log_config.original_table_name IS '원본 테이블명';
COMMENT ON COLUMN table_log_config.log_table_name IS '로그 테이블명';
COMMENT ON COLUMN table_log_config.trigger_name IS '트리거명';
COMMENT ON COLUMN table_log_config.trigger_function_name IS '트리거 함수명';
COMMENT ON COLUMN table_log_config.is_active IS '활성 상태 (Y/N)';
```
## 4. 백엔드 구현
### 4.1 Service Layer 수정
**파일**: `backend-node/src/services/admin/table-type-mng.service.ts`
#### 4.1.1 로그 테이블 생성 로직
```typescript
/**
* 로그 테이블 생성
*/
private async createLogTable(
tableName: string,
columns: any[],
connectionId?: number,
userId?: string
): Promise<void> {
const logTableName = `${tableName}_log`;
const triggerFuncName = `${tableName}_log_trigger_func`;
const triggerName = `${tableName}_audit_trigger`;
// PK 컬럼 찾기
const pkColumn = columns.find(col => col.isPrimaryKey);
if (!pkColumn) {
throw new Error('PK 컬럼이 없으면 로그 테이블을 생성할 수 없습니다.');
}
// 로그 테이블 DDL 생성
const logTableDDL = this.generateLogTableDDL(
logTableName,
pkColumn.COLUMN_NAME,
pkColumn.DATA_TYPE
);
// 트리거 함수 DDL 생성
const triggerFuncDDL = this.generateTriggerFunctionDDL(
triggerFuncName,
logTableName,
tableName,
pkColumn.COLUMN_NAME
);
// 트리거 DDL 생성
const triggerDDL = this.generateTriggerDDL(
triggerName,
tableName,
triggerFuncName
);
try {
// 1. 로그 테이블 생성
await this.executeDDL(logTableDDL, connectionId);
// 2. 트리거 함수 생성
await this.executeDDL(triggerFuncDDL, connectionId);
// 3. 트리거 생성
await this.executeDDL(triggerDDL, connectionId);
// 4. 로그 설정 저장
await this.saveLogConfig({
originalTableName: tableName,
logTableName,
triggerName,
triggerFunctionName: triggerFuncName,
createdBy: userId
});
console.log(`로그 테이블 생성 완료: ${logTableName}`);
} catch (error) {
console.error('로그 테이블 생성 실패:', error);
throw error;
}
}
/**
* 로그 테이블 DDL 생성
*/
private generateLogTableDDL(
logTableName: string,
pkColumnName: string,
pkDataType: string
): string {
return `
CREATE TABLE ${logTableName} (
log_id SERIAL PRIMARY KEY,
operation_type VARCHAR(10) NOT NULL,
original_id ${pkDataType},
changed_column VARCHAR(100),
old_value TEXT,
new_value TEXT,
changed_by VARCHAR(50),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(50),
user_agent TEXT,
full_row_before JSONB,
full_row_after JSONB
);
CREATE INDEX idx_${logTableName}_original_id ON ${logTableName}(original_id);
CREATE INDEX idx_${logTableName}_changed_at ON ${logTableName}(changed_at);
CREATE INDEX idx_${logTableName}_operation ON ${logTableName}(operation_type);
COMMENT ON TABLE ${logTableName} IS '${logTableName.replace('_log', '')} 테이블 변경 이력';
COMMENT ON COLUMN ${logTableName}.operation_type IS '작업 유형 (INSERT/UPDATE/DELETE)';
COMMENT ON COLUMN ${logTableName}.original_id IS '원본 테이블 PK 값';
COMMENT ON COLUMN ${logTableName}.changed_column IS '변경된 컬럼명';
COMMENT ON COLUMN ${logTableName}.old_value IS '변경 전 값';
COMMENT ON COLUMN ${logTableName}.new_value IS '변경 후 값';
`;
}
/**
* 트리거 함수 DDL 생성
*/
private generateTriggerFunctionDDL(
funcName: string,
logTableName: string,
originalTableName: string,
pkColumnName: string
): string {
return `
CREATE OR REPLACE FUNCTION ${funcName}()
RETURNS TRIGGER AS $$
DECLARE
v_column_name TEXT;
v_old_value TEXT;
v_new_value TEXT;
v_user_id VARCHAR(50);
v_ip_address VARCHAR(50);
BEGIN
v_user_id := current_setting('app.user_id', TRUE);
v_ip_address := current_setting('app.ip_address', TRUE);
IF (TG_OP = 'INSERT') THEN
INSERT INTO ${logTableName} (
operation_type, original_id, changed_by, ip_address, full_row_after
) VALUES (
'INSERT', NEW.${pkColumnName}, v_user_id, v_ip_address, row_to_json(NEW)::jsonb
);
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
FOR v_column_name IN
SELECT column_name
FROM information_schema.columns
WHERE table_name = '${originalTableName}'
LOOP
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_column_name, v_column_name)
INTO v_old_value, v_new_value
USING OLD, NEW;
IF v_old_value IS DISTINCT FROM v_new_value THEN
INSERT INTO ${logTableName} (
operation_type, original_id, changed_column, old_value, new_value,
changed_by, ip_address, full_row_before, full_row_after
) VALUES (
'UPDATE', NEW.${pkColumnName}, v_column_name, v_old_value, v_new_value,
v_user_id, v_ip_address, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb
);
END IF;
END LOOP;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO ${logTableName} (
operation_type, original_id, changed_by, ip_address, full_row_before
) VALUES (
'DELETE', OLD.${pkColumnName}, v_user_id, v_ip_address, row_to_json(OLD)::jsonb
);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
`;
}
/**
* 트리거 DDL 생성
*/
private generateTriggerDDL(
triggerName: string,
tableName: string,
funcName: string
): string {
return `
CREATE TRIGGER ${triggerName}
AFTER INSERT OR UPDATE OR DELETE ON ${tableName}
FOR EACH ROW EXECUTE FUNCTION ${funcName}();
`;
}
/**
* 로그 설정 저장
*/
private async saveLogConfig(config: {
originalTableName: string;
logTableName: string;
triggerName: string;
triggerFunctionName: string;
createdBy?: string;
}): Promise<void> {
const query = `
INSERT INTO table_log_config (
original_table_name, log_table_name, trigger_name,
trigger_function_name, created_by
) VALUES ($1, $2, $3, $4, $5)
`;
await this.executeQuery(query, [
config.originalTableName,
config.logTableName,
config.triggerName,
config.triggerFunctionName,
config.createdBy
]);
}
```
#### 4.1.2 테이블 생성 메서드 수정
```typescript
async createTable(params: {
tableName: string;
columns: any[];
useLogTable?: boolean; // 추가
connectionId?: number;
userId?: string;
}): Promise<void> {
const { tableName, columns, useLogTable, connectionId, userId } = params;
// 1. 원본 테이블 생성
const ddl = this.generateCreateTableDDL(tableName, columns);
await this.executeDDL(ddl, connectionId);
// 2. 로그 테이블 생성 (옵션)
if (useLogTable === true) {
await this.createLogTable(tableName, columns, connectionId, userId);
}
// 3. 메타데이터 저장
await this.saveTableMetadata({
tableName,
columns,
useLogTable: useLogTable ? 'Y' : 'N',
connectionId,
userId
});
}
```
### 4.2 Controller Layer 수정
**파일**: `backend-node/src/controllers/admin/table-type-mng.controller.ts`
```typescript
/**
* 테이블 생성
*/
async createTable(req: Request, res: Response): Promise<void> {
try {
const { tableName, columns, useLogTable, connectionId } = req.body;
const userId = req.user?.userId;
await this.tableTypeMngService.createTable({
tableName,
columns,
useLogTable: useLogTable === 'Y' || useLogTable === true,
connectionId,
userId
});
res.json({
success: true,
message: useLogTable
? '테이블 및 로그 테이블이 생성되었습니다.'
: '테이블이 생성되었습니다.'
});
} catch (error) {
console.error('테이블 생성 오류:', error);
res.status(500).json({
success: false,
message: '테이블 생성 중 오류가 발생했습니다.'
});
}
}
```
### 4.3 세션 변수 설정 미들웨어
**파일**: `backend-node/src/middleware/db-session.middleware.ts`
```typescript
import { Request, Response, NextFunction } from "express";
/**
* DB 세션 변수 설정 미들웨어
* 트리거에서 사용할 사용자 정보를 세션 변수에 설정
*/
export const setDBSessionVariables = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const userId = req.user?.userId || "system";
const ipAddress = req.ip || req.socket.remoteAddress || "unknown";
// PostgreSQL 세션 변수 설정
const queries = [
`SET app.user_id = '${userId}'`,
`SET app.ip_address = '${ipAddress}'`,
];
// 각 DB 연결에 세션 변수 설정
// (실제 구현은 DB 연결 풀 관리 방식에 따라 다름)
next();
} catch (error) {
console.error("DB 세션 변수 설정 오류:", error);
next(error);
}
};
```
## 5. 프론트엔드 구현
### 5.1 테이블 생성 폼 수정
**파일**: `frontend/src/app/admin/tableMng/components/TableCreateForm.tsx`
```typescript
const TableCreateForm = () => {
const [useLogTable, setUseLogTable] = useState<boolean>(false);
return (
<div className="table-create-form">
{/* 기존 폼 필드들 */}
{/* 로그 테이블 옵션 추가 */}
<div className="form-group">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={useLogTable}
onChange={(e) => setUseLogTable(e.target.checked)}
className="w-4 h-4"
/>
<span> </span>
</label>
<p className="text-sm text-gray-600 mt-1">
.
(: {tableName}_log)
</p>
</div>
{useLogTable && (
<div className="bg-blue-50 p-4 rounded border border-blue-200">
<h4 className="font-semibold mb-2"> </h4>
<ul className="text-sm space-y-1">
<li> INSERT/UPDATE/DELETE </li>
<li> </li>
<li>
</li>
</ul>
</div>
)}
</div>
);
};
```
### 5.2 로그 조회 화면 추가
**파일**: `frontend/src/app/admin/tableMng/components/TableLogViewer.tsx`
```typescript
interface TableLogViewerProps {
tableName: string;
}
const TableLogViewer: React.FC<TableLogViewerProps> = ({ tableName }) => {
const [logs, setLogs] = useState<any[]>([]);
const [filters, setFilters] = useState({
operationType: "",
startDate: "",
endDate: "",
changedBy: "",
});
const fetchLogs = async () => {
// 로그 데이터 조회
const response = await fetch(`/api/admin/table-log/${tableName}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(filters),
});
const data = await response.json();
setLogs(data.logs);
};
return (
<div className="table-log-viewer">
<h3> : {tableName}</h3>
{/* 필터 */}
<div className="filters">
<select
value={filters.operationType}
onChange={(e) =>
setFilters({ ...filters, operationType: e.target.value })
}
>
<option value=""></option>
<option value="INSERT"></option>
<option value="UPDATE"></option>
<option value="DELETE"></option>
</select>
{/* 날짜 필터, 사용자 필터 등 */}
</div>
{/* 로그 테이블 */}
<table className="log-table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr key={log.log_id}>
<td>{log.operation_type}</td>
<td>{log.original_id}</td>
<td>{log.changed_column}</td>
<td>{log.old_value}</td>
<td>{log.new_value}</td>
<td>{log.changed_by}</td>
<td>{log.changed_at}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
```
## 6. API 엔드포인트
### 6.1 로그 조회 API
```
POST /api/admin/table-log/:tableName
Request Body:
{
"operationType": "UPDATE", // 선택: INSERT, UPDATE, DELETE
"startDate": "2024-01-01", // 선택
"endDate": "2024-12-31", // 선택
"changedBy": "user123", // 선택
"originalId": 123 // 선택
}
Response:
{
"success": true,
"logs": [
{
"log_id": 1,
"operation_type": "UPDATE",
"original_id": "123",
"changed_column": "user_name",
"old_value": "홍길동",
"new_value": "김철수",
"changed_by": "admin",
"changed_at": "2024-10-21T10:30:00Z",
"ip_address": "192.168.1.100"
}
]
}
```
### 6.2 로그 테이블 활성화/비활성화 API
```
POST /api/admin/table-log/:tableName/toggle
Request Body:
{
"isActive": "Y" // Y 또는 N
}
Response:
{
"success": true,
"message": "로그 기능이 활성화되었습니다."
}
```
## 7. 테스트 계획
### 7.1 단위 테스트
- [ ] 로그 테이블 DDL 생성 함수 테스트
- [ ] 트리거 함수 DDL 생성 함수 테스트
- [ ] 트리거 DDL 생성 함수 테스트
- [ ] 로그 설정 저장 함수 테스트
### 7.2 통합 테스트
- [ ] 테이블 생성 시 로그 테이블 자동 생성 테스트
- [ ] INSERT 작업 시 로그 기록 테스트
- [ ] UPDATE 작업 시 로그 기록 테스트
- [ ] DELETE 작업 시 로그 기록 테스트
- [ ] 여러 컬럼 동시 변경 시 로그 기록 테스트
### 7.3 성능 테스트
- [ ] 대량 데이터 INSERT 시 성능 영향 측정
- [ ] 대량 데이터 UPDATE 시 성능 영향 측정
- [ ] 로그 테이블 크기 증가에 따른 성능 영향 측정
## 8. 주의사항 및 제약사항
### 8.1 성능 고려사항
- 트리거는 모든 변경 작업에 대해 실행되므로 성능 영향이 있을 수 있음
- 대량 데이터 처리 시 로그 테이블 크기가 급격히 증가할 수 있음
- 로그 테이블에 적절한 인덱스 설정 필요
### 8.2 운영 고려사항
- 로그 데이터의 보관 주기 정책 수립 필요
- 오래된 로그 데이터 아카이빙 전략 필요
- 로그 테이블의 정기적인 파티셔닝 고려
### 8.3 보안 고려사항
- 로그 데이터에는 민감한 정보가 포함될 수 있으므로 접근 권한 관리 필요
- 로그 데이터 자체의 무결성 보장 필요
- 로그 데이터의 암호화 저장 고려
## 9. 향후 확장 계획
### 9.1 로그 분석 기능
- 변경 패턴 분석
- 사용자별 변경 통계
- 시간대별 변경 추이
### 9.2 로그 알림 기능
- 특정 테이블/컬럼 변경 시 알림
- 비정상적인 대량 변경 감지
- 특정 사용자의 변경 작업 모니터링
### 9.3 로그 복원 기능
- 특정 시점으로 데이터 롤백
- 변경 이력 기반 데이터 복구
- 변경 이력 시각화
## 10. 마이그레이션 가이드
### 10.1 기존 테이블에 로그 기능 추가
```typescript
// 기존 테이블에 로그 테이블 추가하는 API
POST /api/admin/table-log/:tableName/enable
// 실행 순서:
// 1. 로그 테이블 생성
// 2. 트리거 함수 생성
// 3. 트리거 생성
// 4. 로그 설정 저장
```
### 10.2 로그 기능 제거
```typescript
// 로그 기능 제거 API
POST /api/admin/table-log/:tableName/disable
// 실행 순서:
// 1. 트리거 삭제
// 2. 트리거 함수 삭제
// 3. 로그 테이블 삭제 (선택)
// 4. 로그 설정 비활성화
```
## 11. 개발 우선순위
### Phase 1: 기본 기능 (필수)
1. DB 스키마 변경 (table_type_mng, table_log_config)
2. 로그 테이블 DDL 생성 로직
3. 트리거 함수/트리거 DDL 생성 로직
4. 테이블 생성 시 로그 테이블 자동 생성
### Phase 2: UI 개발
1. 테이블 생성 폼에 로그 옵션 추가
2. 로그 조회 화면 개발
3. 로그 필터링 기능
### Phase 3: 고급 기능
1. 로그 활성화/비활성화 기능
2. 기존 테이블에 로그 추가 기능
3. 로그 데이터 아카이빙 기능
### Phase 4: 분석 및 최적화
1. 로그 분석 대시보드
2. 성능 최적화
3. 로그 데이터 파티셔닝
+192
View File
@@ -0,0 +1,192 @@
# V2 Components 구현 완료 보고서
## 구현 일시
2024-12-19
## 구현된 컴포넌트 목록 (10개)
### Phase 1: 핵심 입력 컴포넌트
| 컴포넌트 | 파일 | 모드/타입 | 설명 |
| :---------------- | :------------------ | :-------------------------------------------- | :---------------------- |
| **V2Input** | `V2Input.tsx` | text, number, password, slider, color, button | 통합 입력 컴포넌트 |
| **V2Select** | `V2Select.tsx` | dropdown, radio, check, tag, toggle, swap | 통합 선택 컴포넌트 |
| **V2Date** | `V2Date.tsx` | date, time, datetime + range | 통합 날짜/시간 컴포넌트 |
### Phase 2: 레이아웃 및 그룹 컴포넌트
| 컴포넌트 | 파일 | 모드/타입 | 설명 |
| :---------------- | :------------------ | :-------------------------------------------------------- | :--------------------- |
| **V2List** | `V2List.tsx` | table, card, kanban, list | 통합 리스트 컴포넌트 |
| **V2Layout** | `V2Layout.tsx` | grid, split, flex, divider, screen-embed | 통합 레이아웃 컴포넌트 |
| **V2Group** | `V2Group.tsx` | tabs, accordion, section, card-section, modal, form-modal | 통합 그룹 컴포넌트 |
### Phase 3: 미디어 및 비즈니스 컴포넌트
| 컴포넌트 | 파일 | 모드/타입 | 설명 |
| :------------------- | :--------------------- | :------------------------------------------------------------- | :---------------------- |
| **V2Media** | `V2Media.tsx` | file, image, video, audio | 통합 미디어 컴포넌트 |
| **V2Biz** | `V2Biz.tsx` | flow, rack, map, numbering, category, mapping, related-buttons | 통합 비즈니스 컴포넌트 |
| **V2Hierarchy** | `V2Hierarchy.tsx` | tree, org, bom, cascading | 통합 계층 구조 컴포넌트 |
---
## 공통 인프라
### 설정 패널
- **DynamicConfigPanel**: JSON Schema 기반 동적 설정 UI 생성
### 렌더러
- **V2ComponentRenderer**: v2Type에 따른 동적 컴포넌트 렌더링
---
## 파일 구조
```
frontend/components/v2/
├── index.ts # 모듈 인덱스
├── V2ComponentRenderer.tsx # 동적 렌더러
├── DynamicConfigPanel.tsx # JSON Schema 설정 패널
├── V2Input.tsx # 통합 입력
├── V2Select.tsx # 통합 선택
├── V2Date.tsx # 통합 날짜
├── V2List.tsx # 통합 리스트
├── V2Layout.tsx # 통합 레이아웃
├── V2Group.tsx # 통합 그룹
├── V2Media.tsx # 통합 미디어
├── V2Biz.tsx # 통합 비즈니스
└── V2Hierarchy.tsx # 통합 계층
frontend/types/
└── v2-components.ts # 타입 정의
db/migrations/
└── v2_component_schema.sql # DB 스키마 (미실행)
```
---
## 사용 예시
### 기본 사용법
```tsx
import {
V2Input,
V2Select,
V2Date,
V2List,
V2ComponentRenderer
} from "@/components/v2";
// V2Input 사용
<V2Input
id="name"
label="이름"
required
config={{ type: "text", placeholder: "이름을 입력하세요" }}
value={name}
onChange={setName}
/>
// V2Select 사용
<V2Select
id="status"
label="상태"
config={{
mode: "dropdown",
source: "code",
codeGroup: "ORDER_STATUS",
searchable: true
}}
value={status}
onChange={setStatus}
/>
// V2Date 사용
<V2Date
id="orderDate"
label="주문일"
config={{ type: "date", format: "YYYY-MM-DD" }}
value={orderDate}
onChange={setOrderDate}
/>
// V2List 사용
<V2List
id="orderList"
label="주문 목록"
config={{
viewMode: "table",
searchable: true,
pageable: true,
pageSize: 10,
columns: [
{ field: "orderId", header: "주문번호", sortable: true },
{ field: "customerName", header: "고객명" },
{ field: "orderDate", header: "주문일", format: "date" },
]
}}
data={orders}
onRowClick={handleRowClick}
/>
```
### 동적 렌더링
```tsx
import { V2ComponentRenderer } from "@/components/v2";
// v2Type에 따라 자동으로 적절한 컴포넌트 렌더링
<V2ComponentRenderer
props={{
v2Type: "V2Input",
id: "dynamicField",
label: "동적 필드",
config: { type: "text" },
value: fieldValue,
onChange: setFieldValue,
}}
/>;
```
---
## 주의사항
### 기존 컴포넌트와의 공존
1. **기존 컴포넌트는 그대로 유지**: 모든 레거시 컴포넌트는 정상 동작
2. **신규 화면에서만 V2 컴포넌트 사용**: 기존 화면에 영향 없음
3. **마이그레이션 없음**: 자동 마이그레이션 진행하지 않음
### 데이터베이스 마이그레이션
`db/migrations/v2_component_schema.sql` 파일은 아직 실행되지 않았습니다.
필요시 수동으로 실행해야 합니다:
```bash
psql -h localhost -U postgres -d plm_db -f db/migrations/v2_component_schema.sql
```
---
## 다음 단계 (선택)
1. **화면 관리 에디터 통합**: V2 컴포넌트를 화면 에디터의 컴포넌트 팔레트에 추가
2. **기존 비즈니스 컴포넌트 연동**: V2Biz의 플레이스홀더를 실제 구현으로 교체
3. **테스트 페이지 작성**: 모든 V2 컴포넌트 데모 페이지
4. **문서화**: 각 컴포넌트별 상세 사용 가이드
---
## 관련 문서
- `PLAN_RENEWAL.md`: 리뉴얼 계획서
- `docs/phase0-component-usage-analysis.md`: 컴포넌트 사용 현황 분석
- `docs/phase0-migration-strategy.md`: 마이그레이션 전략 (참고용)
+386
View File
@@ -0,0 +1,386 @@
# 🐳 Docker 가이드 - WACE 솔루션 (ERP-node)
이 문서는 WACE 솔루션의 Docker 환경 설정 및 사용법을 설명합니다.
## 📋 개요
**기술 스택:**
- **백엔드**: Node.js + TypeScript + PostgreSQL (Raw Query)
- **프론트엔드**: Next.js + TypeScript + Tailwind CSS
- **컨테이너**: Docker + Docker Compose
**환경:**
- **개발**: Mac (볼륨 마운트 + Hot Reload)
- **운영**: Linux 서버 (최적화된 프로덕션 빌드)
---
## 🔧 개발 환경 (Mac)
### 빠른 시작
```bash
# 전체 서비스 시작 (병렬 빌드 - 가장 빠름!)
./scripts/dev/start-all-parallel.sh
```
### 개별 서비스 시작
```bash
# 백엔드만 시작
./scripts/dev/start-backend.sh
# 프론트엔드만 시작
./scripts/dev/start-frontend.sh
```
### 개발용 Docker Compose 파일들
- **`docker/dev/docker-compose.backend.mac.yml`** - Mac 개발용 백엔드
- 볼륨 마운트: `./backend-node:/app` (Hot Reload)
- Dockerfile: `docker/dev/backend.Dockerfile`
- 포트: `8080`
- **`docker/dev/docker-compose.frontend.mac.yml`** - Mac 개발용 프론트엔드
- 볼륨 마운트: `./frontend:/app` (Hot Reload)
- Dockerfile: `docker/dev/frontend.Dockerfile`
- 포트: `3000`
### 개발 환경 특징
-**Hot Reload**: 코드 변경 시 자동 반영
-**볼륨 마운트**: 실시간 개발
-**디버그 모드**: 상세 로그 출력
-**빠른 재시작**: Docker 재빌드 불필요
### 🔥 Hot Reload 상세 가이드
#### ✅ **바로 반영되는 것들 (즉시 Hot Reload)**
**백엔드 (Node.js + TypeScript):**
```bash
backend-node/src/controllers/*.ts # API 컨트롤러 수정
backend-node/src/services/*.ts # 비즈니스 로직 수정
backend-node/src/routes/*.ts # 라우터 설정 수정
backend-node/src/middleware/*.ts # 미들웨어 수정
backend-node/src/utils/*.ts # 유틸리티 함수 수정
backend-node/src/types/*.ts # 타입 정의 수정
backend-node/src/config/*.ts # 애플리케이션 설정
```
**반영 시간**: 1-2초 (nodemon 자동 재시작)
**프론트엔드 (Next.js + TypeScript):**
```bash
frontend/components/**/*.tsx # React 컴포넌트 수정
frontend/app/**/*.tsx # 페이지 컴포넌트 수정
frontend/lib/**/*.ts # 유틸리티 함수 수정
frontend/hooks/*.ts # 커스텀 훅 수정
frontend/types/*.ts # 타입 정의 수정
frontend/constants/*.ts # 상수 정의 수정
CSS/SCSS 파일 수정 # 스타일 변경
```
**반영 시간**: 즉시 (Fast Refresh)
#### ❌ **Docker 재시작이 필요한 것들**
**의존성 변경:**
```bash
package.json 수정 # 새 패키지 추가/제거
npm install / npm uninstall # 패키지 설치/제거
package-lock.json 변경 # 의존성 잠금 파일
```
**데이터베이스 관련:**
```bash
db/ilshin.pgsql # DB 스키마 파일 변경
db/00-create-roles.sh # DB 초기화 스크립트 변경
# SQL 마이그레이션은 직접 실행
```
**설정 파일:**
```bash
next.config.mjs # Next.js 설정
tsconfig.json # TypeScript 설정
tailwind.config.js # Tailwind CSS 설정
.env / .env.local # 환경 변수
eslint.config.mjs # ESLint 설정
```
**Docker 관련:**
```bash
Dockerfile / Dockerfile.dev # 도커 파일 수정
docker-compose.*.yml # Docker Compose 설정
.dockerignore # Docker 무시 파일
```
#### 🔄 **재시작 방법**
**특정 서비스만 재시작:**
```bash
# 백엔드만 재시작
docker-compose -f docker-compose.backend.mac.yml restart backend
# 프론트엔드만 재시작
docker-compose -f docker-compose.frontend.mac.yml restart frontend
```
**전체 재빌드:**
```bash
# 의존성 변경 시 (rebuild 필요)
docker-compose -f docker-compose.backend.mac.yml up --build -d
docker-compose -f docker-compose.frontend.mac.yml up --build -d
```
---
## 🚀 운영 환경 (Linux)
### 운영 서버 배포
```bash
# Linux 서버에서 실행
./scripts/prod/start-all-linux.sh
```
### 개별 서비스 시작 (운영용)
```bash
# 직접 Docker Compose 사용
docker-compose -f docker/prod/docker-compose.backend.prod.yml up -d
docker-compose -f docker/prod/docker-compose.frontend.prod.yml up -d
```
### 운영용 Docker Compose 파일들
- **`docker/prod/docker-compose.backend.prod.yml`** - 운영용 백엔드
- Dockerfile: `docker/prod/backend.Dockerfile` (프로덕션 최적화)
- 포트: `8080`
- 환경: `NODE_ENV=production`
- **`docker/prod/docker-compose.frontend.prod.yml`** - 운영용 프론트엔드
- Dockerfile: `docker/prod/frontend.Dockerfile` (프로덕션 최적화)
- 포트: `3000`
- 환경: 최적화된 빌드
### 운영 환경 특징
-**최적화된 빌드**: 프로덕션용 이미지
-**보안 강화**: 운영 환경 설정
-**성능 최적화**: 이미지 크기 최소화
-**안정성**: 프로덕션 모드
---
## 📁 프로젝트 구조
```
ERP-node/
├── 🔧 개발용 (Mac)
│ ├── start-all-parallel.sh # 병렬 시작 (추천)
│ ├── start-backend.sh # 백엔드만
│ ├── start-frontend.sh # 프론트엔드만
│ ├── docker-compose.backend.mac.yml # Mac 개발용 백엔드
│ └── docker-compose.frontend.mac.yml# Mac 개발용 프론트엔드
├── 🚀 운영용 (Linux)
│ ├── start-all-separated-linux.sh # Linux 운영용
│ ├── start-backend-linux.sh # 백엔드만 (Linux)
│ ├── start-frontend-linux.sh # 프론트엔드만 (Linux)
│ ├── docker-compose.backend.prod.yml# 운영용 백엔드
│ └── docker-compose.frontend.prod.yml# 운영용 프론트엔드
├── 📁 백엔드
│ ├── backend-node/
│ │ ├── Dockerfile # 프로덕션용
│ │ └── Dockerfile.dev # 개발용
│ └── src/, database/, package.json...
├── 📁 프론트엔드
│ ├── frontend/
│ │ ├── Dockerfile # 프로덕션용
│ │ └── Dockerfile.dev # 개발용
│ └── app/, components/, hooks/...
└── 🗂️ 기타
├── db/00-create-roles.sh # DB 초기화
└── README.md, DOCKER.md...
```
---
## 🌐 접속 정보
### 개발 환경
- **프론트엔드**: http://localhost:3000
- **백엔드 API**: http://localhost:8080
- **전체 앱**: http://localhost:9771 (프록시 설정 시)
### 운영 환경
- **서버 IP에 따라 다름** (Linux 서버 설정 확인)
---
## 🛠️ 주요 명령어
### Docker 컨테이너 관리
```bash
# 실행 중인 컨테이너 확인
docker ps
# 모든 컨테이너 중지
docker stop $(docker ps -q)
# 사용하지 않는 컨테이너/이미지 정리
docker system prune -f
```
### 로그 확인
```bash
# 백엔드 로그
docker logs pms-backend-mac -f # 개발용
docker logs pms-backend-prod -f # 운영용
# 프론트엔드 로그
docker logs pms-frontend-mac -f # 개발용
docker logs pms-frontend-prod -f # 운영용
```
### 컨테이너 내부 접속
```bash
# 백엔드 컨테이너 접속
docker exec -it pms-backend-mac bash # 개발용
docker exec -it pms-backend-prod bash # 운영용
# 프론트엔드 컨테이너 접속
docker exec -it pms-frontend-mac sh # 개발용
docker exec -it pms-frontend-prod sh # 운영용
```
---
## 🚨 트러블슈팅
### 자주 발생하는 문제들
#### 1. 포트 충돌
```bash
# 포트 사용 중인 프로세스 확인
lsof -i :8080
lsof -i :3000
# 프로세스 종료
kill -9 <PID>
```
#### 2. Docker 빌드 오류
```bash
# Docker 캐시 클리어 후 재빌드
docker builder prune -f
./start-all-parallel.sh
```
#### 3. 볼륨 마운트 문제 (개발환경)
```bash
# Docker Desktop 설정에서 파일 공유 확인
# Docker Desktop > Settings > Resources > File Sharing
```
#### 4. 데이터베이스 연결 오류
```bash
# 데이터베이스 초기화
./db/00-create-roles.sh
# PostgreSQL 연결 확인
docker exec -it <db-container> psql -U postgres
```
### Warning 메시지들 (무시해도 됨)
```
WARN: the attribute `version` is obsolete
Network Error (일시적)
```
이런 메시지들은 Docker Compose 버전 차이로 발생하며, 기능에는 영향 없습니다.
---
## 📈 성능 최적화
### 개발 환경 최적화
-**병렬 빌드**: `start-all-parallel.sh` 사용
-**Docker 캐시**: `--no-cache` 제거됨
-**npm 최적화**: `--prefer-offline --no-audit` 적용
### 운영 환경 최적화
-**멀티 스테이지 빌드**: Dockerfile 최적화
-**이미지 크기 최소화**: Alpine Linux 기반
-**의존성 캐시**: 레이어 캐싱 활용
---
## 🔄 업데이트 가이드
### 개발 환경 업데이트
```bash
# 코드 변경 시 (Hot Reload 자동 반영)
# 별도 작업 불필요
# 의존성 변경 시
docker-compose -f docker-compose.backend.mac.yml up --build -d
```
### 운영 환경 업데이트
```bash
# 새로운 버전 배포
./start-all-separated-linux.sh
```
---
## 📞 지원
**문제 발생 시:**
1. 이 문서의 트러블슈팅 섹션 확인
2. Docker 로그 확인 (`docker logs <container-name>`)
3. 개발팀에 문의
**프로젝트 관련:**
- Node.js 백엔드: `backend-node/` 디렉토리
- Next.js 프론트엔드: `frontend/` 디렉토리
- 데이터베이스: PostgreSQL (JNDI 설정)
---
**버전**: 1.0.0
**마지막 업데이트**: 2024년 12월 28일
**작성자**: PLM 개발팀
@@ -95,7 +95,7 @@
| 파일 | 참조 횟수 | 영향도 | 용도 |
|------|----------|--------|------|
| `UnifiedRepeater.tsx` | 3회 | 🟢 낮음 | 타입 주석 |
| `V2Repeater.tsx` | 3회 | 🟢 낮음 | 타입 주석 |
| `ScreenDesigner.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
| `ButtonConfigPanel.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
| `ScreenRelationFlow.tsx` | 2회 | 🟢 낮음 | 타입 주석 |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,779 @@
# Entity 조인 기능 개발 계획서
> **ID값을 의미있는 데이터로 자동 변환하는 스마트 테이블 시스템**
---
## 📋 프로젝트 개요
### 🎯 목표
테이블 타입 관리에서 Entity 웹타입으로 설정된 컬럼을 참조 테이블과 조인하여, ID값 대신 의미있는 데이터(예: 사용자명)를 TableList 컴포넌트에서 자동으로 표시하는 기능 구현
### 🔍 현재 문제점
```
Before: 회사 테이블에서
┌─────────────┬─────────┬────────────┐
│ company_name│ writer │ created_at │
├─────────────┼─────────┼────────────┤
│ 삼성전자 │ user001 │ 2024-01-15 │
│ LG전자 │ user002 │ 2024-01-16 │
└─────────────┴─────────┴────────────┘
😕 user001이 누구인지 알 수 없음
```
```
After: Entity 조인 적용 시
┌─────────────┬─────────────┬────────────┐
│ company_name│ writer_name │ created_at │
├─────────────┼─────────────┼────────────┤
│ 삼성전자 │ 김철수 │ 2024-01-15 │
│ LG전자 │ 박영희 │ 2024-01-16 │
└─────────────┴─────────────┴────────────┘
😍 즉시 누가 등록했는지 알 수 있음
```
### 🚀 핵심 기능
1. **자동 Entity 감지**: Entity 웹타입으로 설정된 컬럼 자동 스캔
2. **스마트 조인**: 참조 테이블과 자동 LEFT JOIN 수행
3. **컬럼 별칭**: `writer``writer_name`으로 자동 변환
4. **성능 최적화**: 필요한 컬럼만 선택적 조인
5. **캐시 시스템**: 참조 데이터 캐싱으로 성능 향상
---
## 🔧 기술 설계
### 📊 데이터베이스 구조
#### 현재 Entity 설정 (column_labels 테이블)
```sql
column_labels :
- table_name: 'companies'
- column_name: 'writer'
- web_type: 'entity'
- reference_table: 'user_info' -- 참조할 테이블
- reference_column: 'user_id' -- 조인 조건 컬럼
- display_column: 'user_name' -- ⭐ 새로 추가할 필드 (표시할 컬럼)
```
#### 필요한 스키마 확장
```sql
-- column_labels 테이블에 display_column 컬럼 추가
ALTER TABLE column_labels
ADD COLUMN display_column VARCHAR(255) NULL
COMMENT '참조 테이블에서 표시할 컬럼명';
-- 기본값 설정 (없으면 reference_column 사용)
UPDATE column_labels
SET display_column = CASE
WHEN web_type = 'entity' AND reference_table = 'user_info' THEN 'user_name'
WHEN web_type = 'entity' AND reference_table = 'companies' THEN 'company_name'
ELSE reference_column
END
WHERE web_type = 'entity' AND display_column IS NULL;
```
### 🏗️ 백엔드 아키텍처
#### 1. Entity 조인 감지 서비스
```typescript
// src/services/entityJoinService.ts
export interface EntityJoinConfig {
sourceTable: string; // companies
sourceColumn: string; // writer
referenceTable: string; // user_info
referenceColumn: string; // user_id (조인 키)
displayColumn: string; // user_name (표시할 값)
aliasColumn: string; // writer_name (결과 컬럼명)
}
export class EntityJoinService {
/**
* 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성
*/
async detectEntityJoins(tableName: string): Promise<EntityJoinConfig[]>;
/**
* Entity 조인이 포함된 SQL 쿼리 생성
*/
buildJoinQuery(
tableName: string,
joinConfigs: EntityJoinConfig[],
selectColumns: string[],
whereClause: string,
orderBy: string,
limit: number,
offset: number
): string;
/**
* 참조 테이블 데이터 캐싱
*/
async cacheReferenceData(tableName: string): Promise<void>;
}
```
#### 2. 캐시 시스템
```typescript
// src/services/referenceCache.ts
export class ReferenceCacheService {
private cache = new Map<string, Map<string, any>>();
/**
* 작은 참조 테이블 전체 캐싱 (user_info, departments 등)
*/
async preloadReferenceTable(
tableName: string,
keyColumn: string,
displayColumn: string
): Promise<void>;
/**
* 캐시에서 참조 값 조회
*/
getLookupValue(table: string, key: string): any | null;
/**
* 배치 룩업 (성능 최적화)
*/
async batchLookup(
requests: BatchLookupRequest[]
): Promise<BatchLookupResponse[]>;
}
```
#### 3. 테이블 데이터 서비스 확장
```typescript
// tableManagementService.ts 확장
export class TableManagementService {
/**
* Entity 조인이 포함된 데이터 조회
*/
async getTableDataWithEntityJoins(
tableName: string,
options: {
page: number;
size: number;
search?: Record<string, any>;
sortBy?: string;
sortOrder?: string;
enableEntityJoin?: boolean; // 🎯 Entity 조인 활성화
}
): Promise<{
data: any[];
total: number;
page: number;
size: number;
totalPages: number;
entityJoinInfo?: {
// 🎯 조인 정보
joinConfigs: EntityJoinConfig[];
strategy: "full_join" | "cache_lookup";
performance: {
queryTime: number;
cacheHitRate: number;
};
};
}>;
}
```
### 🎨 프론트엔드 구조
#### 1. Entity 타입 설정 UI 확장
```typescript
// frontend/app/(main)/admin/tableMng/page.tsx 확장
// Entity 타입 설정 시 표시할 컬럼도 선택 가능하도록 확장
{column.webType === "entity" && (
<div className="space-y-2">
{/* 기존: 참조 테이블 선택 */}
<Select value={column.referenceTable} onValueChange={...}>
<SelectContent>
{referenceTableOptions.map(option => ...)}
</SelectContent>
</Select>
{/* 🎯 새로 추가: 표시할 컬럼 선택 */}
<Select value={column.displayColumn} onValueChange={...}>
<SelectTrigger>
<SelectValue placeholder="표시할 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{getDisplayColumnOptions(column.referenceTable).map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
```
#### 2. TableList 컴포넌트 확장
```typescript
// TableListComponent.tsx 확장
// Entity 조인 데이터 조회
const result = await tableTypeApi.getTableDataWithEntityJoins(
tableConfig.selectedTable,
{
page: currentPage,
size: localPageSize,
search: searchConditions,
sortBy: sortColumn,
sortOrder: sortDirection,
enableEntityJoin: true, // 🎯 Entity 조인 활성화
}
);
// Entity 조인된 컬럼 시각적 구분
<TableHead>
<div className="flex items-center space-x-1">
{isEntityJoinedColumn && (
<span className="text-xs text-blue-600" title="Entity 조인됨">
🔗
</span>
)}
<span className={cn(isEntityJoinedColumn && "text-blue-700 font-medium")}>
{getColumnDisplayName(column)}
</span>
</div>
</TableHead>;
```
#### 3. API 타입 확장
```typescript
// frontend/lib/api/screen.ts 확장
export const tableTypeApi = {
// 🎯 Entity 조인 지원 데이터 조회
getTableDataWithEntityJoins: async (
tableName: string,
params: {
page?: number;
size?: number;
search?: Record<string, any>;
sortBy?: string;
sortOrder?: "asc" | "desc";
enableEntityJoin?: boolean;
}
): Promise<{
data: Record<string, any>[];
total: number;
page: number;
size: number;
totalPages: number;
entityJoinInfo?: {
joinConfigs: EntityJoinConfig[];
strategy: string;
performance: any;
};
}> => {
// 구현...
},
// 🎯 참조 테이블의 표시 가능한 컬럼 목록 조회
getReferenceTableColumns: async (
tableName: string
): Promise<
{
columnName: string;
displayName: string;
dataType: string;
}[]
> => {
// 구현...
},
};
```
---
## 🗂️ 구현 단계
### Phase 1: 백엔드 기반 구축 (2일)
#### Day 1: Entity 조인 감지 시스템 ✅ **완료!**
```typescript
목록:
1. EntityJoinService
- detectEntityJoins(): Entity
- buildJoinQuery(): LEFT JOIN
- validateJoinConfig():
2.
- column_labels display_column
- Entity
3.
- Entity
- SQL
```
#### Day 2: 캐시 시스템 및 성능 최적화
```typescript
목록:
1. ReferenceCacheService
- (user_info, departments)
-
- TTL
2. TableManagementService
- getTableDataWithEntityJoins()
- vs
-
3.
-
- ( vs )
```
### Phase 2: 프론트엔드 연동 (2일)
#### Day 3: 관리자 UI 확장
```typescript
목록:
1.
- Entity display_column UI
-
-
2. API
- Entity / API
- API
-
3.
- (user_info user_name )
-
```
#### Day 4: TableList 컴포넌트 확장
```typescript
목록:
1. Entity
- getTableDataWithEntityJoins API
- (🔗 )
- (writer writer_name)
2. UI
- (full_join / cache_lookup)
- ( , )
-
3.
-
-
-
```
### Phase 3: 고급 기능 및 최적화 (1일)
#### Day 5: 고급 기능 및 완성도
```typescript
목록:
1. Entity
- Entity
-
-
2.
- Entity 릿
-
-
3.
-
-
-
```
---
## 📊 예상 결과
### 🎯 핵심 사용 시나리오
#### 시나리오 1: 회사 관리 테이블
```sql
-- Entity 설정
companies.writer (entity) user_info.user_name
-- 실행되는 쿼리
SELECT
c.*,
u.user_name as writer_name
FROM companies c
LEFT JOIN user_info u ON c.writer = u.user_id
WHERE c.company_name ILIKE '%삼성%'
ORDER BY c.created_date DESC
LIMIT 20;
-- 화면 표시
company_name writer_name created_date
2024-01-15
SDI 2024-01-16
```
#### 시나리오 2: 프로젝트 관리 테이블
```sql
-- Entity 설정 (다중)
projects.manager_id (entity) user_info.user_name
projects.company_id (entity) companies.company_name
-- 실행되는 쿼리
SELECT
p.*,
u.user_name as manager_name,
c.company_name as company_name
FROM projects p
LEFT JOIN user_info u ON p.manager_id = u.user_id
LEFT JOIN companies c ON p.company_id = c.company_id
ORDER BY p.created_date DESC;
-- 화면 표시
project_name manager_name company_name created_date
ERP 2024-01-15
AI LG전자 2024-01-16
```
### 📈 성능 예상 지표
#### 캐시 전략 성능
```
🎯 작은 참조 테이블 (user_info < 1000건)
- 전체 캐싱: 메모리 사용량 ~1MB
- 룩업 속도: O(1) - 평균 0.1ms
- 캐시 적중률: 95%+
🎯 큰 참조 테이블 (companies > 10000건)
- 쿼리 조인: 평균 50-100ms
- 인덱스 최적화로 성능 보장
- 페이징으로 메모리 효율성 확보
```
#### 사용자 경험 개선
```
Before: "user001이 누구지? 🤔"
→ 별도 조회 필요 (추가 5-10초)
After: "김철수님이 등록하셨구나! 😍"
→ 즉시 이해 (0초)
💰 업무 효율성: 직원 1명당 하루 2-3분 절약
→ 100명 기준 연간 80-120시간 절약
```
---
## 🔒 고려사항 및 제약
### ⚠️ 주의사항
#### 1. 성능 영향
```
✅ 대응 방안:
- 작은 참조 테이블 (< 1000건): 전체 캐싱
- 큰 참조 테이블 (> 1000건): 인덱스 최적화 + 쿼리 조인
- 조인 수 제한: 테이블당 최대 5개 Entity 컬럼
- 자동 성능 모니터링 및 알림
```
#### 2. 데이터 일관성
```
✅ 대응 방안:
- 참조 테이블 데이터 변경 시 캐시 자동 무효화
- Foreign Key 제약조건 권장 (필수 아님)
- 참조 데이터 없는 경우 원본 ID 표시
- 실시간 데이터 유효성 검증
```
#### 3. 사용자 설정 복잡도
```
✅ 대응 방안:
- 자동 추천 시스템 (user_info → user_name)
- 일반적인 Entity 설정 템플릿 제공
- 설정 미리보기 및 검증 기능
- 단계별 설정 가이드 제공
```
### 🚀 확장 가능성
#### 1. 고급 Entity 기능
- **조건부 조인**: WHERE 조건이 있는 Entity 조인
- **계층적 Entity**: Entity 안의 또 다른 Entity (user → department → company)
- **집계 Entity**: 관련 데이터 개수나 합계 표시 (project_count, total_amount)
#### 2. 성능 최적화
- **지능형 캐싱**: 사용 빈도 기반 캐시 전략
- **배경 업데이트**: 사용자 요청과 독립적인 캐시 갱신
- **분산 캐싱**: Redis 등 외부 캐시 서버 연동
#### 3. 사용자 경험
- **실시간 프리뷰**: Entity 설정 변경 시 즉시 미리보기
- **자동 완성**: Entity 설정 시 테이블/컬럼 자동 완성
- **성능 인사이트**: 조인 성능 분석 및 최적화 제안
---
## 📋 체크리스트
### 개발 완료 기준
#### 백엔드 ✅
- [x] EntityJoinService 구현 및 테스트 ✅
- [x] ReferenceCacheService 구현 및 테스트 ✅
- [x] column_labels 스키마 확장 (display_column) ✅
- [x] getTableDataWithEntityJoins API 구현 ✅
- [x] TableManagementService 확장 ✅
- [x] 새로운 API 엔드포인트 추가: `/api/table-management/tables/:tableName/data-with-joins`
- [ ] 성능 벤치마크 (< 100ms 목표)
- [ ] 에러 처리 및 fallback 로직
#### 프론트엔드 ✅
- [x] Entity 타입 설정 UI 확장 (display_column 선택) ✅
- [ ] TableList Entity 조인 데이터 표시
- [ ] 조인된 컬럼 시각적 구분 (🔗 아이콘)
- [ ] 성능 모니터링 UI (쿼리 시간, 캐시 적중률)
- [ ] 에러 상황 사용자 피드백
#### 시스템 통합 ✅
- [x] **성능 최적화 완료** 🚀
- [x] 프론트엔드 전역 코드 캐시 매니저 (TTL 기반)
- [x] 백엔드 참조 테이블 메모리 캐시 시스템 강화
- [x] Entity 조인용 데이터베이스 인덱스 최적화
- [x] 스마트 조인 전략 (테이블 크기 기반 자동 선택)
- [x] 배치 데이터 로딩 및 메모이제이션 최적화
- [ ] 전체 기능 통합 테스트
- [ ] 성능 테스트 (다양한 데이터 크기)
- [ ] 사용자 시나리오 테스트
- [ ] 문서화 및 사용 가이드
- [ ] 프로덕션 배포 준비
---
## ⚡ 성능 최적화 완료 보고서
### 🎯 최적화 개요
Entity 조인 시스템의 성능을 대폭 개선하여 **70-90%의 성능 향상**을 달성했습니다.
### 🚀 구현된 최적화 기술
#### 1. 프론트엔드 전역 코드 캐시 시스템 ✅
- **TTL 기반 스마트 캐싱**: 5분 자동 만료 + 배경 갱신
- **배치 로딩**: 여러 코드 카테고리 병렬 처리
- **메모리 관리**: 자동 정리 + 사용량 모니터링
- **성능 개선**: 코드 변환 속도 **90%↑** (200ms → 10ms)
```typescript
// 사용 예시
const cacheManager = CodeCacheManager.getInstance();
await cacheManager.preloadCodes(["USER_STATUS", "DEPT_TYPE"]); // 배치 로딩
const result = cacheManager.convertCodeToName("USER_STATUS", "A"); // 고속 변환
```
#### 2. 백엔드 참조 테이블 메모리 캐시 강화 ✅
- **테이블 크기 기반 전략**: 1000건 이하 전체 캐싱, 5000건 이하 선택적 캐싱
- **배경 갱신**: TTL 80% 지점에서 자동 갱신
- **메모리 최적화**: 최대 50MB 제한 + LRU 제거
- **성능 개선**: 참조 조회 속도 **85%↑** (100ms → 15ms)
```typescript
// 향상된 캐시 시스템
const cachedData = await referenceCacheService.getCachedReference(
"user_info",
"user_id",
"user_name"
); // 자동 전략 선택
```
#### 3. 데이터베이스 인덱스 최적화 ✅
- **Entity 조인 전용 인덱스**: 조인 성능 **60%↑**
- **커버링 인덱스**: 추가 테이블 접근 제거
- **부분 인덱스**: 활성 데이터만 인덱싱으로 공간 효율성 향상
- **텍스트 검색 최적화**: GIN 인덱스로 LIKE 쿼리 가속
```sql
-- 핵심 성능 인덱스
CREATE INDEX CONCURRENTLY idx_user_info_covering
ON user_info(user_id) INCLUDE (user_name, email, dept_code);
CREATE INDEX CONCURRENTLY idx_column_labels_entity_lookup
ON column_labels(table_name, column_name) WHERE web_type = 'entity';
```
#### 4. 스마트 조인 전략 (하이브리드) ✅
- **자동 전략 선택**: 테이블 크기와 캐시 상태 기반
- **하이브리드 조인**: 일부는 SQL 조인, 일부는 캐시 룩업
- **실시간 최적화**: 캐시 적중률에 따른 전략 동적 변경
- **성능 개선**: 복합 조인 **75%↑** (500ms → 125ms)
```typescript
// 스마트 전략 선택
const strategy = await entityJoinService.determineJoinStrategy(joinConfigs);
// 'full_join' | 'cache_lookup' | 'hybrid' 자동 선택
```
#### 5. 배치 데이터 로딩 & 메모이제이션 ✅
- **React 최적화 훅**: `useEntityJoinOptimization`
- **배치 크기 조절**: 서버 부하 방지
- **성능 메트릭 추적**: 실시간 캐시 적중률 모니터링
- **프리로딩**: 공통 코드 자동 사전 로딩
```typescript
// 최적화 훅 사용
const { optimizedConvertCode, metrics, isOptimizing } =
useEntityJoinOptimization(columnMeta);
```
### 📊 성능 개선 결과
| 최적화 항목 | Before | After | 개선율 |
| ----------------- | ------ | --------- | ---------- |
| **코드 변환** | 200ms | 10ms | **95%↑** |
| **Entity 조인** | 500ms | 125ms | **75%↑** |
| **참조 조회** | 100ms | 15ms | **85%↑** |
| **대용량 페이징** | 3000ms | 300ms | **90%↑** |
| **캐시 적중률** | 0% | 90%+ | **신규** |
| **메모리 효율성** | N/A | 50MB 제한 | **최적화** |
### 🎯 핵심 성능 지표
#### 응답 시간 개선
- **일반 조회**: 200ms → 50ms (**75% 개선**)
- **복합 조인**: 500ms → 125ms (**75% 개선**)
- **코드 변환**: 100ms → 5ms (**95% 개선**)
#### 처리량 개선
- **동시 사용자**: 50명 → 200명 (**4배 증가**)
- **초당 요청**: 100 req/s → 400 req/s (**4배 증가**)
#### 자원 효율성
- **메모리 사용량**: 무제한 → 50MB 제한
- **캐시 적중률**: 90%+ 달성
- **CPU 사용률**: 30% 감소
### 🛠️ 성능 모니터링 도구
#### 1. 실시간 성능 대시보드
- 개발 모드에서 캐시 적중률 실시간 표시
- 평균 응답 시간 모니터링
- 최적화 상태 시각적 피드백
#### 2. 성능 벤치마크 스크립트
```bash
# 성능 벤치마크 실행
node backend-node/scripts/performance-benchmark.js
```
#### 3. 캐시 상태 조회 API
```bash
GET /api/table-management/cache/status
```
### 🔧 운영 가이드
#### 캐시 관리
```typescript
// 캐시 상태 확인
const status = codeCache.getCacheInfo();
// 수동 캐시 새로고침
await codeCache.clear();
await codeCache.preloadCodes(["USER_STATUS"]);
```
#### 성능 튜닝
1. **인덱스 사용률 모니터링**
2. **캐시 적중률 90% 이상 유지**
3. **메모리 사용량 50MB 이하 유지**
4. **응답 시간 100ms 이하 목표**
### 🎉 사용자 경험 개선
#### Before (최적화 전)
- 코드 표시: "A" → 의미 불명 ❌
- 로딩 시간: 3-5초 ⏰
- 사용자 불편: 별도 조회 필요 😕
#### After (최적화 후)
- 코드 표시: "활성" → 즉시 이해 ✅
- 로딩 시간: 0.1-0.3초 ⚡
- 사용자 만족: 끊김 없는 경험 😍
### 💡 향후 확장 계획
1. **Redis 분산 캐시**: 멀티 서버 환경 지원
2. **AI 기반 캐시 예측**: 사용 패턴 학습
3. **GraphQL 최적화**: N+1 문제 완전 해결
4. **실시간 통계**: 성능 트렌드 분석
---
## 🎯 결론
이 Entity 조인 기능은 단순한 데이터 표시 개선을 넘어서 **사용자 경험의 혁신**을 가져올 것입니다.
**"user001"** 같은 의미없는 ID 대신 **"김철수님"** 같은 의미있는 정보를 즉시 보여줌으로써, 업무 효율성을 크게 향상시킬 수 있습니다.
특히 **자동 감지**와 **스마트 캐싱** 시스템으로 개발자와 사용자 모두에게 편리한 기능이 될 것으로 기대됩니다.
---
**🚀 "ID에서 이름으로, 데이터에서 정보로의 진화!"**
+680
View File
@@ -0,0 +1,680 @@
# Screen Designer 2.0 리뉴얼 계획: 컴포넌트 통합 및 속성 기반 고도화
## 1. 개요
현재 **68개 이상**으로 파편화된 화면 관리 컴포넌트들을 **9개의 핵심 통합 컴포넌트(V2 Components)**로 재편합니다.
각 컴포넌트는 **속성(Config)** 설정을 통해 다양한 형태(View Mode)와 기능(Behavior)을 수행하도록 설계되어, 유지보수성과 확장성을 극대화합니다.
### 현재 컴포넌트 현황 (AS-IS)
| 카테고리 | 파일 수 | 주요 파일들 |
| :------------- | :-----: | :------------------------------------------------------------------ |
| Widget 타입별 | 14개 | TextWidget, NumberWidget, SelectWidget, DateWidget, EntityWidget 등 |
| Config Panel | 28개 | TextConfigPanel, SelectConfigPanel, DateConfigPanel 등 |
| WebType Config | 11개 | TextTypeConfigPanel, SelectTypeConfigPanel 등 |
| 기타 패널 | 15개+ | PropertiesPanel, DetailSettingsPanel 등 |
---
## 2. 통합 전략: 9 Core Widgets
### A. 입력 위젯 (Input Widgets) - 5종
단순 데이터 입력 필드를 통합합니다.
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) |
| :-------------------- | :------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. V2 Select** | Select, Radio, Checkbox, Boolean, Code, Entity, Combobox, Toggle | **`mode`**: "dropdown" / "radio" / "check" / "tag"<br>**`source`**: "static" / "code" / "db" / "api"<br>**`dependency`**: { parentField: "..." } |
| **2. V2 Input** | Text, Number, Email, Tel, Password, Color, Search, Integer, Decimal | **`type`**: "text" / "number" / "password"<br>**`format`**: "email", "currency", "biz_no"<br>**`mask`**: "000-0000-0000" |
| **3. V2 Date** | Date, Time, DateTime, DateRange, Month, Year | **`type`**: "date" / "time" / "datetime"<br>**`range`**: true/false |
| **4. V2 Text** | Textarea, RichEditor, Markdown, HTML | **`mode`**: "simple" / "rich" / "code"<br>**`rows`**: number |
| **5. V2 Media** | File, Image, Video, Audio, Attachment | **`type`**: "file" / "image"<br>**`multiple`**: true/false<br>**`preview`**: true/false |
### B. 구조/데이터 위젯 (Structure & Data Widgets) - 4종
레이아웃 배치와 데이터 시각화를 담당합니다.
| 통합 컴포넌트 (TO-BE) | 포함되는 기존 컴포넌트 (AS-IS) | 핵심 속성 (Configuration) | 활용 예시 |
| :-------------------- | :-------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------- |
| **6. V2 List** | **Table, Card List, Repeater, DataGrid, List View** | **`viewMode`**: "table" / "card" / "kanban"<br>**`editable`**: true/false | - `viewMode='table'`: 엑셀형 리스트<br>- `viewMode='card'`: **카드 디스플레이**<br>- `editable=true`: **반복 필드 그룹** |
| **7. V2 Layout** | **Row, Col, Split Panel, Grid, Spacer** | **`type`**: "grid" / "split" / "flex"<br>**`columns`**: number | - `type='split'`: **화면 분할 패널**<br>- `type='grid'`: 격자 레이아웃 |
| **8. V2 Group** | Tab, Accordion, FieldSet, Modal, Section | **`type`**: "tab" / "accordion" / "modal" | - 탭이나 아코디언으로 내용 그룹화 |
| **9. V2 Biz** | **Rack Structure**, Calendar, Gantt | **`type`**: "rack" / "calendar" / "gantt" | - `type='rack'`: **랙 구조 설정**<br>- 특수 비즈니스 로직 플러그인 탑재 |
### C. Config Panel 통합 전략 (핵심)
현재 28개의 ConfigPanel을 **1개의 DynamicConfigPanel**로 통합합니다.
| AS-IS | TO-BE | 방식 |
| :-------------------- | :--------------------- | :------------------------------- |
| TextConfigPanel.tsx | | |
| SelectConfigPanel.tsx | **DynamicConfigPanel** | DB의 `sys_input_type` 테이블에서 |
| DateConfigPanel.tsx | (단일 컴포넌트) | JSON Schema를 읽어 |
| NumberConfigPanel.tsx | | 속성 UI를 동적 생성 |
| ... 24개 더 | | |
---
## 3. 구현 시나리오 (속성 기반 변신)
### Case 1: "테이블을 카드 리스트로 변경"
- **AS-IS**: `DataTable` 컴포넌트를 삭제하고 `CardList` 컴포넌트를 새로 추가해야 함.
- **TO-BE**: `V2List`의 속성창에서 **[View Mode]**를 `Table``Card`로 변경하면 즉시 반영.
### Case 2: "단일 선택을 라디오 버튼으로 변경"
- **AS-IS**: `SelectWidget`을 삭제하고 `RadioWidget` 추가.
- **TO-BE**: `V2Select` 속성창에서 **[Display Mode]**를 `Dropdown``Radio`로 변경.
### Case 3: "입력 폼에 반복 필드(Repeater) 추가"
- **TO-BE**: `V2List` 컴포넌트 배치 후 `editable: true`, `viewMode: "table"` 설정.
---
## 4. 실행 로드맵 (Action Plan)
### Phase 0: 준비 단계 (1주)
통합 작업 전 필수 분석 및 설계를 진행합니다.
- [ ] 기존 컴포넌트 사용 현황 분석 (화면별 위젯 사용 빈도 조사)
- [ ] 데이터 마이그레이션 전략 설계 (`widgetType``V2Widget.type` 매핑 정의)
- [ ] `sys_input_type` 테이블 JSON Schema 설계
- [ ] DynamicConfigPanel 프로토타입 설계
### Phase 1: 입력 위젯 통합 (2주)
가장 중복이 많고 효과가 즉각적인 입력 필드부터 통합합니다.
- [ ] **V2Input 구현**: Text, Number, Email, Tel, Password 통합
- [ ] **V2Select 구현**: Select, Radio, Checkbox, Boolean 통합
- [ ] **V2Date 구현**: Date, DateTime, Time 통합
- [ ] 기존 위젯과 **병행 운영** (deprecated 마킹, 삭제하지 않음)
### Phase 2: Config Panel 통합 (2주)
28개의 ConfigPanel을 단일 동적 패널로 통합합니다.
- [ ] **DynamicConfigPanel 구현**: DB 스키마 기반 속성 UI 자동 생성
- [ ] `sys_input_type` 테이블에 위젯별 JSON Schema 정의 저장
- [ ] 기존 ConfigPanel과 **병행 운영** (삭제하지 않음)
### Phase 3: 데이터/레이아웃 위젯 통합 (2주)
프로젝트의 데이터를 보여주는 핵심 뷰를 통합합니다.
- [ ] **V2List 구현**: Table, Card, Repeater 통합 렌더러 개발
- [ ] **V2Layout 구현**: Split Panel, Grid, Flex 통합
- [ ] **V2Group 구현**: Tab, Accordion, Modal 통합
### Phase 4: 안정화 및 마이그레이션 (2주)
신규 컴포넌트 안정화 후 점진적 전환을 진행합니다.
- [ ] 신규 화면은 V2 컴포넌트만 사용하도록 가이드
- [ ] 기존 화면 데이터 마이그레이션 스크립트 개발
- [ ] 마이그레이션 테스트 (스테이징 환경)
- [ ] 문서화 및 개발 가이드 작성
### Phase 5: 레거시 정리 (추후 결정)
충분한 안정화 기간 후 레거시 컴포넌트 정리를 검토합니다.
- [ ] 사용 현황 재분석 (V2 전환율 확인)
- [ ] 미전환 화면 목록 정리
- [ ] 레거시 컴포넌트 삭제 여부 결정 (별도 회의)
---
## 5. 데이터 마이그레이션 전략
### 5.1 위젯 타입 매핑 테이블
기존 `widgetType`을 신규 V2 컴포넌트로 매핑합니다.
| 기존 widgetType | 신규 컴포넌트 | 속성 설정 |
| :-------------- | :------------ | :------------------------------ |
| `text` | V2Input | `type: "text"` |
| `number` | V2Input | `type: "number"` |
| `email` | V2Input | `type: "text", format: "email"` |
| `tel` | V2Input | `type: "text", format: "tel"` |
| `select` | V2Select | `mode: "dropdown"` |
| `radio` | V2Select | `mode: "radio"` |
| `checkbox` | V2Select | `mode: "check"` |
| `date` | V2Date | `type: "date"` |
| `datetime` | V2Date | `type: "datetime"` |
| `textarea` | V2Text | `mode: "simple"` |
| `file` | V2Media | `type: "file"` |
| `image` | V2Media | `type: "image"` |
### 5.2 마이그레이션 원칙
1. **비파괴적 전환**: 기존 데이터 구조 유지, 신규 필드 추가 방식
2. **하위 호환성**: 기존 `widgetType` 필드는 유지, `v2Type` 필드 추가
3. **점진적 전환**: 화면 수정 시점에 자동 또는 수동 전환
---
## 6. 기대 효과
1. **컴포넌트 수 감소**: 68개 → **9개** (관리 포인트 87% 감소)
2. **Config Panel 통합**: 28개 → **1개** (DynamicConfigPanel)
3. **유연한 UI 변경**: 컴포넌트 교체 없이 속성 변경만으로 UI 모드 전환 가능
4. **Low-Code 확장성**: 새로운 유형의 입력 방식이 필요할 때 코딩 없이 DB 설정만으로 추가 가능
---
## 7. 리스크 및 대응 방안
| 리스크 | 영향도 | 대응 방안 |
| :----------------------- | :----: | :-------------------------------- |
| 기존 화면 호환성 깨짐 | 높음 | 병행 운영 + 하위 호환성 유지 |
| 마이그레이션 데이터 손실 | 높음 | 백업 필수 + 롤백 스크립트 준비 |
| 개발자 학습 곡선 | 중간 | 상세 가이드 문서 + 예제 코드 제공 |
| 성능 저하 (동적 렌더링) | 중간 | 메모이제이션 + 지연 로딩 적용 |
---
## 8. 현재 컴포넌트 매핑 분석
### 8.1 Registry 등록 컴포넌트 전수 조사 (44개)
현재 `frontend/lib/registry/components/`에 등록된 모든 컴포넌트의 통합 가능 여부를 분석했습니다.
#### V2Input으로 통합 (4개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------- | :--------------- | :------------- |
| text-input | `type: "text"` | |
| number-input | `type: "number"` | |
| slider-basic | `type: "slider"` | 속성 추가 필요 |
| button-primary | `type: "button"` | 별도 검토 |
#### V2Select로 통합 (8개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------------ | :----------------------------------- | :------------- |
| select-basic | `mode: "dropdown"` | |
| checkbox-basic | `mode: "check"` | |
| radio-basic | `mode: "radio"` | |
| toggle-switch | `mode: "toggle"` | 속성 추가 필요 |
| autocomplete-search-input | `mode: "dropdown", searchable: true` | |
| entity-search-input | `source: "entity"` | |
| mail-recipient-selector | `mode: "multi", type: "email"` | 복합 컴포넌트 |
| location-swap-selector | `mode: "swap"` | 특수 UI |
#### V2Date로 통합 (1개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------ | :------------- | :--- |
| date-input | `type: "date"` | |
#### V2Text로 통합 (1개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------- | :--------------- | :--- |
| textarea-basic | `mode: "simple"` | |
#### V2Media로 통합 (3개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------ | :------------------------------ | :--- |
| file-upload | `type: "file"` | |
| image-widget | `type: "image"` | |
| image-display | `type: "image", readonly: true` | |
#### V2List로 통합 (8개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :-------------------- | :------------------------------------ | :------------ |
| table-list | `viewMode: "table"` | |
| card-display | `viewMode: "card"` | |
| repeater-field-group | `editable: true` | |
| modal-repeater-table | `viewMode: "table", modal: true` | |
| simple-repeater-table | `viewMode: "table", simple: true` | |
| repeat-screen-modal | `viewMode: "card", modal: true` | |
| table-search-widget | `viewMode: "table", searchable: true` | |
| tax-invoice-list | `viewMode: "table", bizType: "tax"` | 특수 비즈니스 |
#### V2Layout으로 통합 (4개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------ | :-------------------------- | :------------- |
| split-panel-layout | `type: "split"` | |
| split-panel-layout2 | `type: "split", version: 2` | |
| divider-line | `type: "divider"` | 속성 추가 필요 |
| screen-split-panel | `type: "screen-embed"` | 화면 임베딩 |
#### V2Group으로 통합 (5개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :------------------- | :--------------------- | :------------ |
| accordion-basic | `type: "accordion"` | |
| tabs | `type: "tabs"` | |
| section-paper | `type: "section"` | |
| section-card | `type: "card-section"` | |
| universal-form-modal | `type: "form-modal"` | 복합 컴포넌트 |
#### V2Biz로 통합 (7개)
| 현재 컴포넌트 | 매핑 속성 | 비고 |
| :-------------------- | :------------------------ | :--------------- |
| flow-widget | `type: "flow"` | 플로우 관리 |
| rack-structure | `type: "rack"` | 창고 렉 구조 |
| map | `type: "map"` | 지도 |
| numbering-rule | `type: "numbering"` | 채번 규칙 |
| category-manager | `type: "category"` | 카테고리 관리 |
| customer-item-mapping | `type: "mapping"` | 거래처-품목 매핑 |
| related-data-buttons | `type: "related-buttons"` | 연관 데이터 |
#### 별도 검토 필요 (3개)
| 현재 컴포넌트 | 문제점 | 제안 |
| :-------------------------- | :------------------- | :------------------------------ |
| conditional-container | 조건부 렌더링 로직 | 공통 속성으로 분리 |
| selected-items-detail-input | 복합 (선택+상세입력) | V2List + V2Group 조합 |
| text-display | 읽기 전용 텍스트 | V2Input (readonly: true) |
### 8.2 매핑 분석 결과
```
┌─────────────────────────────────────────────────────────┐
│ 전체 44개 컴포넌트 분석 결과 │
├─────────────────────────────────────────────────────────┤
│ ✅ 즉시 통합 가능 : 36개 (82%) │
│ ⚠️ 속성 추가 후 통합 : 5개 (11%) │
│ 🔄 별도 검토 필요 : 3개 (7%) │
└─────────────────────────────────────────────────────────┘
```
### 8.3 속성 확장 필요 사항
#### V2Input 속성 확장
```typescript
// 기존
type: "text" | "number" | "password";
// 확장
type: "text" | "number" | "password" | "slider" | "color" | "button";
```
#### V2Select 속성 확장
```typescript
// 기존
mode: "dropdown" | "radio" | "check" | "tag";
// 확장
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
```
#### V2Layout 속성 확장
```typescript
// 기존
type: "grid" | "split" | "flex";
// 확장
type: "grid" | "split" | "flex" | "divider" | "screen-embed";
```
### 8.4 조건부 렌더링 공통화
`conditional-container`의 기능을 모든 컴포넌트에서 사용 가능한 공통 속성으로 분리합니다.
```typescript
// 모든 V2 컴포넌트에 적용 가능한 공통 속성
interface BaseV2Props {
// ... 기존 속성
/** 조건부 렌더링 설정 */
conditional?: {
enabled: boolean;
field: string; // 참조할 필드명
operator: "=" | "!=" | ">" | "<" | "in" | "notIn";
value: any; // 비교 값
hideOnFalse?: boolean; // false일 때 숨김 (기본: true)
};
}
```
---
## 9. 계층 구조(Hierarchy) 컴포넌트 전략
### 9.1 현재 계층 구조 지원 현황
DB 테이블 `cascading_hierarchy_group`에서 4가지 계층 타입을 지원합니다:
| 타입 | 설명 | 예시 |
| :----------------- | :---------------------- | :--------------- |
| **MULTI_TABLE** | 다중 테이블 계층 | 국가 > 도시 > 구 |
| **SELF_REFERENCE** | 자기 참조 (단일 테이블) | 조직도, 메뉴 |
| **BOM** | 자재명세서 구조 | 부품 > 하위부품 |
| **TREE** | 일반 트리 | 카테고리 |
### 9.2 통합 방안: V2Hierarchy 신설 (10번째 컴포넌트)
계층 구조는 일반 입력/표시 위젯과 성격이 다르므로 **별도 컴포넌트로 분리**합니다.
```typescript
interface V2HierarchyProps {
/** 계층 유형 */
type: "tree" | "org" | "bom" | "cascading";
/** 표시 방식 */
viewMode: "tree" | "table" | "indent" | "dropdown";
/** 계층 그룹 코드 (cascading_hierarchy_group 연동) */
source: string;
/** 편집 가능 여부 */
editable?: boolean;
/** 드래그 정렬 가능 */
draggable?: boolean;
/** BOM 수량 표시 (BOM 타입 전용) */
showQty?: boolean;
/** 최대 레벨 제한 */
maxLevel?: number;
}
```
### 9.3 활용 예시
| 설정 | 결과 |
| :---------------------------------------- | :------------------------- |
| `type: "tree", viewMode: "tree"` | 카테고리 트리뷰 |
| `type: "org", viewMode: "tree"` | 조직도 |
| `type: "bom", viewMode: "indent"` | BOM 들여쓰기 테이블 |
| `type: "cascading", viewMode: "dropdown"` | 연쇄 셀렉트 (국가>도시>구) |
---
## 10. 최종 통합 컴포넌트 목록 (10개)
| # | 컴포넌트 | 역할 | 커버 범위 |
| :-: | :------------------- | :------------- | :----------------------------------- |
| 1 | **V2Input** | 단일 값 입력 | text, number, slider, button 등 |
| 2 | **V2Select** | 선택 입력 | dropdown, radio, checkbox, toggle 등 |
| 3 | **V2Date** | 날짜/시간 입력 | date, datetime, time, range |
| 4 | **V2Text** | 다중 행 텍스트 | textarea, rich editor, markdown |
| 5 | **V2Media** | 파일/미디어 | file, image, video, audio |
| 6 | **V2List** | 데이터 목록 | table, card, repeater, kanban |
| 7 | **V2Layout** | 레이아웃 배치 | grid, split, flex, divider |
| 8 | **V2Group** | 콘텐츠 그룹화 | tabs, accordion, section, modal |
| 9 | **V2Biz** | 비즈니스 특화 | flow, rack, map, numbering 등 |
| 10 | **V2Hierarchy** | 계층 구조 | tree, org, bom, cascading |
---
## 11. 연쇄관계 관리 메뉴 통합 전략
### 11.1 현재 연쇄관계 관리 현황
**관리 메뉴**: `연쇄 드롭다운 통합 관리` (6개 탭)
| 탭 | DB 테이블 | 실제 데이터 | 복잡도 |
| :--------------- | :--------------------------------------- | :---------: | :----: |
| 2단계 연쇄관계 | `cascading_relation` | 2건 | 낮음 |
| 다단계 계층 | `cascading_hierarchy_group/level` | 1건 | 높음 |
| 조건부 필터 | `cascading_condition` | 0건 | 중간 |
| 자동 입력 | `cascading_auto_fill_group/mapping` | 0건 | 낮음 |
| 상호 배제 | `cascading_mutual_exclusion` | 0건 | 낮음 |
| 카테고리 값 연쇄 | `category_value_cascading_group/mapping` | 2건 | 중간 |
### 11.2 통합 방향: 속성 기반 vs 공통 정의
#### 판단 기준
| 기능 | 재사용 빈도 | 설정 복잡도 | 권장 방식 |
| :--------------- | :---------: | :---------: | :----------------------- |
| 2단계 연쇄 | 낮음 | 간단 | **속성에 inline 정의** |
| 다단계 계층 | 높음 | 복잡 | **공통 정의 유지** |
| 조건부 필터 | 낮음 | 간단 | **속성에 inline 정의** |
| 자동 입력 | 낮음 | 간단 | **속성에 inline 정의** |
| 상호 배제 | 낮음 | 간단 | **속성에 inline 정의** |
| 카테고리 값 연쇄 | 중간 | 중간 | **카테고리 관리와 통합** |
### 11.3 속성 통합 설계
#### 2단계 연쇄 → V2Select 속성
```typescript
// AS-IS: 별도 관리 메뉴에서 정의 후 참조
<SelectWidget cascadingRelation="WAREHOUSE_LOCATION" />
// TO-BE: 컴포넌트 속성에서 직접 정의
<V2Select
source="db"
table="warehouse_location"
valueColumn="location_code"
labelColumn="location_name"
cascading={{
parentField: "warehouse_code", // 같은 화면 내 부모 필드
filterColumn: "warehouse_code", // 필터링할 컬럼
clearOnChange: true // 부모 변경 시 초기화
}}
/>
```
#### 조건부 필터 → 공통 conditional 속성
```typescript
// AS-IS: 별도 관리 메뉴에서 조건 정의
// cascading_condition 테이블에 저장
// TO-BE: 모든 컴포넌트에 공통 속성으로 적용
<V2Input
conditional={{
enabled: true,
field: "order_type", // 참조할 필드
operator: "=", // 비교 연산자
value: "EXPORT", // 비교 값
action: "show", // show | hide | disable | enable
}}
/>
```
#### 자동 입력 → autoFill 속성
```typescript
// AS-IS: cascading_auto_fill_group 테이블에 정의
// TO-BE: 컴포넌트 속성에서 직접 정의
<V2Input
autoFill={{
enabled: true,
sourceTable: "company_mng", // 조회할 테이블
filterColumn: "company_code", // 필터링 컬럼
userField: "companyCode", // 사용자 정보 필드
displayColumn: "company_name", // 표시할 컬럼
}}
/>
```
#### 상호 배제 → mutualExclusion 속성
```typescript
// AS-IS: cascading_mutual_exclusion 테이블에 정의
// TO-BE: 컴포넌트 속성에서 직접 정의
<V2Select
mutualExclusion={{
enabled: true,
targetField: "sub_category", // 상호 배제 대상 필드
type: "exclusive", // exclusive | inclusive
}}
/>
```
### 11.4 관리 메뉴 정리 계획
| 현재 메뉴 | TO-BE | 비고 |
| :-------------------------- | :----------------------- | :-------------------- |
| **연쇄 드롭다운 통합 관리** | **삭제** | 6개 탭 전체 제거 |
| ├─ 2단계 연쇄관계 | V2Select 속성 | inline 정의 |
| ├─ 다단계 계층 | **테이블관리로 이동** | 복잡한 구조 유지 필요 |
| ├─ 조건부 필터 | 공통 conditional 속성 | 모든 컴포넌트에 적용 |
| ├─ 자동 입력 | autoFill 속성 | 컴포넌트별 정의 |
| ├─ 상호 배제 | mutualExclusion 속성 | 컴포넌트별 정의 |
| └─ 카테고리 값 연쇄 | **카테고리 관리로 이동** | 기존 메뉴 통합 |
### 11.5 DB 테이블 정리 (Phase 5)
| 테이블 | 조치 | 시점 |
| :--------------------------- | :----------------------- | :------ |
| `cascading_relation` | 마이그레이션 후 삭제 | Phase 5 |
| `cascading_condition` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_auto_fill_*` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_mutual_exclusion` | 삭제 (데이터 없음) | Phase 5 |
| `cascading_hierarchy_*` | **유지** | - |
| `category_value_cascading_*` | **유지** (카테고리 관리) | - |
### 11.6 마이그레이션 스크립트 필요 항목
```sql
-- cascading_relation → 화면 레이아웃 데이터로 마이그레이션
-- 기존 2건의 연쇄관계를 사용하는 화면을 찾아서
-- 해당 컴포넌트의 cascading 속성으로 변환
-- 예시: WAREHOUSE_LOCATION 연쇄관계
-- 이 관계를 사용하는 화면의 컴포넌트에
-- cascading: { parentField: "warehouse_code", filterColumn: "warehouse_code" }
-- 속성 추가
```
---
## 12. 최종 아키텍처 요약
### 12.1 통합 컴포넌트 (10개)
| # | 컴포넌트 | 역할 |
| :-: | :------------------- | :--------------------------------------- |
| 1 | **V2Input** | 단일 값 입력 (text, number, slider 등) |
| 2 | **V2Select** | 선택 입력 (dropdown, radio, checkbox 등) |
| 3 | **V2Date** | 날짜/시간 입력 |
| 4 | **V2Text** | 다중 행 텍스트 (textarea, rich editor) |
| 5 | **V2Media** | 파일/미디어 (file, image) |
| 6 | **V2List** | 데이터 목록 (table, card, repeater) |
| 7 | **V2Layout** | 레이아웃 배치 (grid, split, flex) |
| 8 | **V2Group** | 콘텐츠 그룹화 (tabs, accordion, section) |
| 9 | **V2Biz** | 비즈니스 특화 (flow, rack, map 등) |
| 10 | **V2Hierarchy** | 계층 구조 (tree, org, bom, cascading) |
### 12.2 공통 속성 (모든 컴포넌트에 적용)
```typescript
interface BaseV2Props {
// 기본 속성
id: string;
label?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
// 스타일
style?: ComponentStyle;
className?: string;
// 조건부 렌더링 (conditional-container 대체)
conditional?: {
enabled: boolean;
field: string;
operator:
| "="
| "!="
| ">"
| "<"
| "in"
| "notIn"
| "isEmpty"
| "isNotEmpty";
value: any;
action: "show" | "hide" | "disable" | "enable";
};
// 자동 입력 (autoFill 대체)
autoFill?: {
enabled: boolean;
sourceTable: string;
filterColumn: string;
userField: "companyCode" | "userId" | "deptCode";
displayColumn: string;
};
// 유효성 검사
validation?: ValidationRule[];
}
```
### 12.3 V2Select 전용 속성
```typescript
interface V2SelectProps extends BaseV2Props {
// 표시 모드
mode: "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
// 데이터 소스
source: "static" | "code" | "db" | "api" | "entity";
// static 소스
options?: Array<{ value: string; label: string }>;
// db 소스
table?: string;
valueColumn?: string;
labelColumn?: string;
// code 소스
codeGroup?: string;
// 연쇄 관계 (cascading_relation 대체)
cascading?: {
parentField: string; // 부모 필드명
filterColumn: string; // 필터링할 컬럼
clearOnChange?: boolean; // 부모 변경 시 초기화
};
// 상호 배제 (mutual_exclusion 대체)
mutualExclusion?: {
enabled: boolean;
targetField: string; // 상호 배제 대상
type: "exclusive" | "inclusive";
};
// 다중 선택
multiple?: boolean;
maxSelect?: number;
}
```
### 12.4 관리 메뉴 정리 결과
| AS-IS | TO-BE |
| :---------------------------- | :----------------------------------- |
| 연쇄 드롭다운 통합 관리 (6탭) | **삭제** |
| - 2단계 연쇄관계 | → V2Select.cascading 속성 |
| - 다단계 계층 | → 테이블관리 > 계층 구조 설정 |
| - 조건부 필터 | → 공통 conditional 속성 |
| - 자동 입력 | → 공통 autoFill 속성 |
| - 상호 배제 | → V2Select.mutualExclusion 속성 |
| - 카테고리 값 연쇄 | → 카테고리 관리와 통합 |
---
## 13. 주의사항
> **기존 컴포넌트 삭제 금지**
> 모든 Phase에서 기존 컴포넌트는 삭제하지 않고 **병행 운영**합니다.
> 레거시 정리는 Phase 5에서 충분한 안정화 후 별도 검토합니다.
> **연쇄관계 마이그레이션 필수**
> 관리 메뉴 삭제 전 기존 `cascading_relation` 데이터(2건)를
> 해당 화면의 컴포넌트 속성으로 마이그레이션해야 합니다.
+195
View File
@@ -0,0 +1,195 @@
# PLM 윈도우 개발환경 가이드
## 🚀 빠른 시작
### 1. 전체 환경 시작
```cmd
start-windows.bat
```
### 2. 환경 중지
```cmd
stop-windows.bat
```
### 3. 빌드만 실행
```cmd
build-windows.bat
```
## 📋 사전 요구사항
### 필수 소프트웨어
- **Docker Desktop for Windows** (WSL2 백엔드 사용)
- **Java Development Kit (JDK) 7 이상**
- **Git for Windows**
### Docker Desktop 설정
1. Docker Desktop 설치
2. **Settings > General**에서 "Use WSL 2 based engine" 체크
3. **Settings > Resources > WSL Integration**에서 WSL 배포판 활성화
## 📁 파일 구조
```
new_ph/
├── start-windows.bat # 🎯 메인 시작 스크립트
├── stop-windows.bat # ⏹️ 중지 스크립트
├── build-windows.bat # 🔨 Java 빌드 스크립트
├── docker-compose.win.yml # 🐳 윈도우용 Docker Compose
├── Dockerfile.win # 🐳 윈도우용 Dockerfile
├── config.windows.env # ⚙️ 환경 변수 설정
└── README-WINDOWS.md # 📖 이 파일
```
## ⚙️ 환경 설정
### config.windows.env 파일 수정
```env
# 데이터베이스 설정
DB_PASSWORD=your_password_here
# 포트 설정 (충돌 시 변경)
TOMCAT_PORT=9090
# 메모리 설정
TOMCAT_MEMORY_MIN=512m
TOMCAT_MEMORY_MAX=1024m
```
## 🐳 Docker 서비스
### 애플리케이션 서비스
- **컨테이너명**: plm-windows
- **포트**: 9090 → 8080
- **접속 URL**: http://localhost:9090
### 데이터베이스 서비스
- **컨테이너명**: plm-postgres-win
- **포트**: 5432 → 5432
- **데이터베이스**: plm
- **사용자**: postgres
- **패스워드**: ph0909!!
## 🔧 주요 명령어
### Docker 관리
```cmd
# 컨테이너 상태 확인
docker-compose -f docker-compose.win.yml ps
# 로그 확인
docker-compose -f docker-compose.win.yml logs -f
# 특정 서비스 로그
docker-compose -f docker-compose.win.yml logs -f plm-app
docker-compose -f docker-compose.win.yml logs -f plm-db
# 컨테이너 재시작
docker-compose -f docker-compose.win.yml restart plm-app
```
### 개발 작업
```cmd
# 빌드만 실행
build-windows.bat
# 컨테이너 재빌드
docker-compose -f docker-compose.win.yml up --build -d
# 데이터베이스 리셋
docker-compose -f docker-compose.win.yml down -v
docker-compose -f docker-compose.win.yml up -d
```
## 🐛 문제 해결
### Docker Desktop 실행 안됨
```cmd
# Windows 서비스 확인
sc query com.docker.service
# WSL2 상태 확인
wsl --status
# Docker Desktop 재시작
taskkill /f /im "Docker Desktop.exe"
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
```
### Java 컴파일 오류
```cmd
# Java 버전 확인
java -version
javac -version
# 클래스패스 문제 시 수동 빌드
javac -cp "WebContent\WEB-INF\lib\*" -d WebContent\WEB-INF\classes src\com\pms\**\*.java
```
### 포트 충돌
```cmd
# 포트 사용 확인
netstat -ano | findstr :9090
# 프로세스 종료
taskkill /PID <PID번호> /F
```
### 볼륨 권한 문제
```cmd
# Docker 볼륨 정리
docker volume prune -f
# WSL2 재시작
wsl --shutdown
```
## 📊 모니터링
### 리소스 사용량
```cmd
# Docker 시스템 정보
docker system df
# 컨테이너 리소스 사용량
docker stats
# 로그 크기 확인
dir logs /s
```
### 헬스체크
```cmd
# 애플리케이션 상태
curl http://localhost:9090
# 데이터베이스 연결 테스트
docker exec plm-postgres-win psql -U postgres -d plm -c "SELECT version();"
```
## 🔄 업데이트
### 코드 변경 후
1. `build-windows.bat` 실행
2. `docker-compose -f docker-compose.win.yml restart plm-app`
### Docker 이미지 업데이트
```cmd
docker-compose -f docker-compose.win.yml down
docker-compose -f docker-compose.win.yml pull
docker-compose -f docker-compose.win.yml up --build -d
```
## 📞 지원
문제가 발생하면 다음을 확인하세요:
1. **로그 파일**: `logs/` 디렉토리
2. **Docker 로그**: `docker-compose -f docker-compose.win.yml logs`
3. **시스템 요구사항**: Docker Desktop, WSL2, Java JDK
4. **네트워크**: 방화벽, 포트 충돌
---
**🎉 즐거운 개발 되세요!**
View File
@@ -1,4 +1,4 @@
# V2 컴포넌트 및 Unified 폼 컴포넌트 결합도 분석 보고서
# V2 컴포넌트 및 V2 폼 컴포넌트 결합도 분석 보고서
> 작성일: 2026-01-26
> 목적: 컴포넌트 간 결합도 분석 및 느슨한 결합 전환 가능성 평가
@@ -29,23 +29,23 @@
| 16 | v2-table-search-widget | `v2-table-search-widget/` | 테이블 검색 위젯 |
| 17 | v2-tabs-widget | `v2-tabs-widget/` | 탭 위젯 |
| 18 | v2-text-display | `v2-text-display/` | 텍스트 표시 |
| 19 | v2-unified-repeater | `v2-unified-repeater/` | 통합 리피터 |
| 19 | v2-repeater | `v2-repeater/` | 통합 리피터 |
### 1.2 Unified 폼 컴포넌트 (11개)
### 1.2 V2 폼 컴포넌트 (11개)
| # | 컴포넌트 | 파일 | 주요 용도 |
|---|---------|------|----------|
| 1 | UnifiedInput | `UnifiedInput.tsx` | 텍스트/숫자/이메일 등 입력 |
| 2 | UnifiedSelect | `UnifiedSelect.tsx` | 선택박스/라디오/체크박스 |
| 3 | UnifiedDate | `UnifiedDate.tsx` | 날짜/시간 입력 |
| 4 | UnifiedRepeater | `UnifiedRepeater.tsx` | 리피터 (테이블 형태) |
| 5 | UnifiedLayout | `UnifiedLayout.tsx` | 레이아웃 컨테이너 |
| 6 | UnifiedGroup | `UnifiedGroup.tsx` | 그룹 컨테이너 (카드/탭/접기) |
| 7 | UnifiedHierarchy | `UnifiedHierarchy.tsx` | 계층 구조 표시 |
| 8 | UnifiedList | `UnifiedList.tsx` | 리스트 표시 |
| 9 | UnifiedMedia | `UnifiedMedia.tsx` | 파일/이미지/비디오 업로드 |
| 10 | UnifiedBiz | `UnifiedBiz.tsx` | 비즈니스 컴포넌트 |
| 11 | UnifiedFormContext | `UnifiedFormContext.tsx` | 폼 상태 관리 컨텍스트 |
| 1 | V2Input | `V2Input.tsx` | 텍스트/숫자/이메일 등 입력 |
| 2 | V2Select | `V2Select.tsx` | 선택박스/라디오/체크박스 |
| 3 | V2Date | `V2Date.tsx` | 날짜/시간 입력 |
| 4 | V2Repeater | `V2Repeater.tsx` | 리피터 (테이블 형태) |
| 5 | V2Layout | `V2Layout.tsx` | 레이아웃 컨테이너 |
| 6 | V2Group | `V2Group.tsx` | 그룹 컨테이너 (카드/탭/접기) |
| 7 | V2Hierarchy | `V2Hierarchy.tsx` | 계층 구조 표시 |
| 8 | V2List | `V2List.tsx` | 리스트 표시 |
| 9 | V2Media | `V2Media.tsx` | 파일/이미지/비디오 업로드 |
| 10 | V2Biz | `V2Biz.tsx` | 비즈니스 컴포넌트 |
| 11 | V2FormContext | `V2FormContext.tsx` | 폼 상태 관리 컨텍스트 |
---
@@ -140,29 +140,29 @@ window.addEventListener("checkboxSelectionChange", handleSelectionChange);
| v2-table-search-widget | ❌ | 0개 | ❌ | 🟢 1/10 |
| v2-text-display | ❌ | 0개 | ❌ | 🟢 1/10 |
| v2-repeat-screen-modal | ❌ | 0개 | ❌ | 🟢 1/10 |
| v2-unified-repeater | ❌ | 0개 | ❌ | 🟢 1/10 |
| v2-repeater | ❌ | 0개 | ❌ | 🟢 1/10 |
### 2.3 Unified 폼 컴포넌트 결합도 상세
### 2.3 V2 폼 컴포넌트 결합도 상세
| 컴포넌트 | buttonActions Import | CustomEvent 사용 | window.__ 사용 | 결합도 점수 |
|---------|---------------------|------------------|----------------|------------|
| **UnifiedRepeater** | ❌ | 7개 수신/발생 | 2개 사용 | 🔴 8/10 |
| **UnifiedFormContext** | ❌ | 3개 발생 | ❌ | 🟠 4/10 |
| UnifiedInput | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedSelect | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedDate | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedLayout | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedGroup | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedHierarchy | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedList | ❌ | 0개 (TableList 래핑) | ❌ | 🟢 2/10 |
| UnifiedMedia | ❌ | 0개 | ❌ | 🟢 1/10 |
| UnifiedBiz | ❌ | 0개 | ❌ | 🟢 1/10 |
| **V2Repeater** | ❌ | 7개 수신/발생 | 2개 사용 | 🔴 8/10 |
| **V2FormContext** | ❌ | 3개 발생 | ❌ | 🟠 4/10 |
| V2Input | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Select | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Date | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Layout | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Group | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Hierarchy | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2List | ❌ | 0개 (TableList 래핑) | ❌ | 🟢 2/10 |
| V2Media | ❌ | 0개 | ❌ | 🟢 1/10 |
| V2Biz | ❌ | 0개 | ❌ | 🟢 1/10 |
**UnifiedRepeater 상세:**
**V2Repeater 상세:**
```typescript
// 전역 상태 사용
window.__unifiedRepeaterInstances = new Set();
window.__unifiedRepeaterInstances.add(targetTableName);
window.__v2RepeaterInstances = new Set();
window.__v2RepeaterInstances.add(targetTableName);
// CustomEvent 수신
window.addEventListener("repeaterSave", handleSaveEvent);
@@ -171,7 +171,7 @@ window.addEventListener("componentDataTransfer", handleComponentDataTransfer);
window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer);
```
**UnifiedFormContext 상세:**
**V2FormContext 상세:**
```typescript
// CustomEvent 발생 (레거시 호환)
window.dispatchEvent(new CustomEvent("beforeFormSave", { detail: eventDetail }));
@@ -211,7 +211,7 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
│ │
▼ ▼
┌───────────┐ ┌───────────┐
Unified │ │Unified
V2 │ │V2
│Repeater │ │FormContext│
└───────────┘ └───────────┘
```
@@ -227,10 +227,10 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
| `refreshTable` | v2-button-primary, buttonActions | 테이블 데이터 새로고침 |
| `closeEditModal` | v2-button-primary, buttonActions | 수정 모달 닫기 |
| `saveSuccessInModal` | v2-button-primary, buttonActions | 저장 성공 알림 (연속 등록) |
| `beforeFormSave` | UnifiedFormContext, buttonActions | 저장 전 데이터 수집 |
| `afterFormSave` | UnifiedFormContext | 저장 완료 알림 |
| `beforeFormSave` | V2FormContext, buttonActions | 저장 전 데이터 수집 |
| `afterFormSave` | V2FormContext | 저장 완료 알림 |
| `tableListDataChange` | v2-table-list | 테이블 데이터 변경 알림 |
| `repeaterDataChange` | UnifiedRepeater | 리피터 데이터 변경 알림 |
| `repeaterDataChange` | V2Repeater | 리피터 데이터 변경 알림 |
| `repeaterSave` | buttonActions | 리피터 저장 요청 |
| `openScreenModal` | v2-split-panel-layout | 화면 모달 열기 |
| `refreshCardDisplay` | buttonActions | 카드 디스플레이 새로고침 |
@@ -240,13 +240,13 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
| 이벤트명 | 수신 컴포넌트 | 처리 내용 |
|---------|-------------|----------|
| `refreshTable` | v2-table-list, v2-split-panel-layout | 데이터 재조회 |
| `beforeFormSave` | v2-repeat-container, UnifiedRepeater | formData에 섹션 데이터 추가 |
| `beforeFormSave` | v2-repeat-container, V2Repeater | formData에 섹션 데이터 추가 |
| `tableListDataChange` | v2-aggregation-widget, v2-repeat-container | 집계 재계산, 데이터 동기화 |
| `repeaterDataChange` | v2-aggregation-widget, v2-repeat-container | 집계 재계산, 데이터 동기화 |
| `repeaterSave` | UnifiedRepeater | 리피터 데이터 저장 실행 |
| `repeaterSave` | V2Repeater | 리피터 데이터 저장 실행 |
| `selectionChange` | v2-aggregation-widget | 선택 기반 집계 |
| `componentDataTransfer` | UnifiedRepeater | 컴포넌트 간 데이터 전달 |
| `splitPanelDataTransfer` | UnifiedRepeater | 분할 패널 데이터 전달 |
| `componentDataTransfer` | V2Repeater | 컴포넌트 간 데이터 전달 |
| `splitPanelDataTransfer` | V2Repeater | 분할 패널 데이터 전달 |
| `refreshCardDisplay` | v2-card-display | 카드 데이터 재조회 |
---
@@ -255,7 +255,7 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
| 전역 변수 | 사용 컴포넌트 | 용도 | 위험도 |
|----------|-------------|------|--------|
| `window.__unifiedRepeaterInstances` | UnifiedRepeater, buttonActions | 리피터 인스턴스 추적 | 🟠 중간 |
| `window.__v2RepeaterInstances` | V2Repeater, buttonActions | 리피터 인스턴스 추적 | 🟠 중간 |
| `window.__relatedButtonsTargetTables` | v2-table-list | 관련 버튼 대상 테이블 | 🟠 중간 |
| `window.__relatedButtonsSelectedData` | v2-table-list, buttonActions | 관련 버튼 선택 데이터 | 🟠 중간 |
| `window.__dataRegistry` | v2-table-list (v1/v2) | 테이블 데이터 레지스트리 | 🟠 중간 |
@@ -272,12 +272,12 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
| 🟠 중간 (4-6점) | 4개 | v2-repeat-container, v2-split-panel-layout, v2-aggregation-widget, v2-tabs-widget |
| 🟢 낮음 (1-3점) | 12개 | 나머지 |
### 6.2 Unified 컴포넌트 (11개)
### 6.2 V2 컴포넌트 (11개)
| 결합도 수준 | 개수 | 컴포넌트 |
|------------|------|---------|
| 🔴 높음 (7-10점) | 1개 | UnifiedRepeater |
| 🟠 중간 (4-6점) | 1개 | UnifiedFormContext |
| 🔴 높음 (7-10점) | 1개 | V2Repeater |
| 🟠 중간 (4-6점) | 1개 | V2FormContext |
| 🟢 낮음 (1-3점) | 9개 | 나머지 |
### 6.3 전체 결합도 분포
@@ -288,14 +288,14 @@ window.dispatchEvent(new CustomEvent("afterFormSave", { detail: { ... } }));
높은 결합도 (🔴): 3개 (10.3%)
├── v2-button-primary
├── v2-table-list
└── UnifiedRepeater
└── V2Repeater
중간 결합도 (🟠): 5개 (17.2%)
├── v2-repeat-container
├── v2-split-panel-layout
├── v2-aggregation-widget
├── v2-tabs-widget
└── UnifiedFormContext
└── V2FormContext
낮은 결합도 (🟢): 21개 (72.5%)
└── 나머지 모든 컴포넌트
@@ -318,7 +318,7 @@ v2-table-list 오류 발생 시:
├── related-button 이벤트 미발생 → 관련 버튼 비활성화
└── 전역 상태 오염 가능성
UnifiedRepeater 오류 발생 시:
V2Repeater 오류 발생 시:
├── beforeFormSave 처리 실패 → 리피터 데이터 저장 누락
├── repeaterSave 수신 실패 → 저장 요청 무시
└── 전역 인스턴스 레지스트리 오류
@@ -330,7 +330,7 @@ UnifiedRepeater 오류 발생 시:
|---------|-----------------|----------|
| v2-button-primary | 저장/삭제 전체 | ❌ 격리 안됨 |
| v2-table-list | 집계/관련버튼 | ❌ 격리 안됨 |
| UnifiedRepeater | 리피터 저장 | ❌ 격리 안됨 |
| V2Repeater | 리피터 저장 | ❌ 격리 안됨 |
| v2-aggregation-widget | 자신만 | ✅ 부분 격리 |
| v2-repeat-container | 자신만 | ✅ 부분 격리 |
| 나머지 21개 | 자신만 | ✅ 완전 격리 |
@@ -357,7 +357,7 @@ UnifiedRepeater 오류 발생 시:
|---------|---------|----------|
| 1 | v2-button-primary | buttonActions 의존성 제거, 독립 저장 서비스 |
| 2 | v2-table-list | 전역 상태 제거, EventBus 전환 |
| 3 | UnifiedRepeater | 전역 상태 제거, EventBus 전환 |
| 3 | V2Repeater | 전역 상태 제거, EventBus 전환 |
### 8.3 3단계: 이벤트 통합 (2-3일)
@@ -422,7 +422,7 @@ UnifiedRepeater 오류 발생 시:
|---------|-----------------|-------------------|-------------|------|
| **v2-button-primary** | ✅ | ✅ | ✅ | 완료 |
| **v2-table-list** | ✅ | - | ✅ | 완료 |
| **UnifiedRepeater** | ✅ | - | ✅ | 완료 |
| **V2Repeater** | ✅ | - | ✅ | 완료 |
### 10.3 아키텍처 특징
@@ -469,7 +469,7 @@ useEffect(() => {
### 11.1 현재 상태 요약
- **전체 29개 컴포넌트 중 72.5%(21개)는 이미 낮은 결합도**를 가지고 있어 독립적으로 동작
- **핵심 문제 컴포넌트 3개 (v2-button-primary, v2-table-list, UnifiedRepeater) 마이그레이션 완료**
- **핵심 문제 컴포넌트 3개 (v2-button-primary, v2-table-list, V2Repeater) 마이그레이션 완료**
- **buttonActions.ts (7,145줄)**는 추후 분할 예정 (현재는 동작 유지)
### 11.2 달성 목표
@@ -514,22 +514,22 @@ frontend/
│ │ ├── v2-table-search-widget/
│ │ ├── v2-tabs-widget/
│ │ ├── v2-text-display/
│ │ └── v2-unified-repeater/
│ │ └── v2-repeater/
│ └── utils/
│ └── buttonActions.ts (7,145줄)
└── components/
└── unified/
├── UnifiedInput.tsx
├── UnifiedSelect.tsx
├── UnifiedDate.tsx
├── UnifiedRepeater.tsx
├── UnifiedLayout.tsx
├── UnifiedGroup.tsx
├── UnifiedHierarchy.tsx
├── UnifiedList.tsx
├── UnifiedMedia.tsx
├── UnifiedBiz.tsx
└── UnifiedFormContext.tsx
└── v2/
├── V2Input.tsx
├── V2Select.tsx
├── V2Date.tsx
├── V2Repeater.tsx
├── V2Layout.tsx
├── V2Group.tsx
├── V2Hierarchy.tsx
├── V2List.tsx
├── V2Media.tsx
├── V2Biz.tsx
└── V2FormContext.tsx
```
## 부록 B: V2 Core 파일 구조 (구현됨)
@@ -67,7 +67,7 @@ V2(Version 2) 컴포넌트는 기존 레거시 컴포넌트의 문제점을 해
|------------|------|------|
| `v2-rack-structure` | 렉 구조 | 창고 렉 구조 표시 |
| `v2-repeat-screen-modal` | 반복 화면 모달 | 반복 가능한 화면 모달 |
| `v2-unified-repeater` | 통합 리피터 | 통합 리피터 테이블 |
| `v2-repeater` | 통합 리피터 | 통합 리피터 테이블 |
---
@@ -172,10 +172,10 @@ import { V2ErrorBoundary } from "@/lib/v2-core";
|--------|------|--------|--------|
| `v2:table:refresh` | 테이블 새로고침 | v2-button-primary | v2-table-list |
| `v2:table:data:change` | 테이블 데이터 변경 | v2-table-list | v2-aggregation-widget |
| `v2:form:save:collect` | 폼 저장 전 데이터 수집 | buttonActions | v2-repeat-container, UnifiedRepeater |
| `v2:form:save:collect` | 폼 저장 전 데이터 수집 | buttonActions | v2-repeat-container, V2Repeater |
| `v2:modal:close` | 모달 닫기 | v2-button-primary | EditModal |
| `v2:modal:save:success` | 모달 저장 성공 | v2-button-primary | EditModal |
| `v2:repeater:save` | 리피터 저장 | buttonActions | UnifiedRepeater |
| `v2:repeater:save` | 리피터 저장 | buttonActions | V2Repeater |
| `v2:component:error` | 컴포넌트 에러 | V2ErrorBoundary | 로깅/모니터링 |
### 4.2 이벤트 흐름 다이어그램
@@ -333,7 +333,7 @@ export const BadConfigPanel: React.FC<Props> = ({ config, onChange }) => {
|---------|-------------|---------------|-------------|
| v2-button-primary | ✅ | ✅ | ✅ |
| v2-table-list | ✅ | ✅ | ✅ |
| UnifiedRepeater | ✅ | ✅ | ✅ |
| V2Repeater | ✅ | ✅ | ✅ |
### 7.3 장애 격리 검증
@@ -341,7 +341,7 @@ export const BadConfigPanel: React.FC<Props> = ({ config, onChange }) => {
v2-button-primary 에러 발생 시:
├── V2ErrorBoundary 캐치 → 버튼만 에러 UI 표시
├── v2-table-list: 정상 동작 ✅
└── UnifiedRepeater: 정상 동작 ✅
└── V2Repeater: 정상 동작 ✅
v2-table-list 에러 발생 시:
├── V2ErrorBoundary 캐치 → 테이블만 에러 UI 표시
@@ -351,30 +351,30 @@ v2-table-list 에러 발생 시:
---
## 8. Unified 폼 컴포넌트
## 8. V2 폼 컴포넌트
### 8.1 목록 (11개)
| 컴포넌트 | 파일 | 용도 |
|---------|------|------|
| UnifiedInput | UnifiedInput.tsx | 텍스트/숫자/이메일/채번 입력 |
| UnifiedSelect | UnifiedSelect.tsx | 선택박스/라디오/체크박스/카테고리 |
| UnifiedDate | UnifiedDate.tsx | 날짜/시간 입력 |
| UnifiedRepeater | UnifiedRepeater.tsx | 리피터 테이블 |
| UnifiedLayout | UnifiedLayout.tsx | 레이아웃 컨테이너 |
| UnifiedGroup | UnifiedGroup.tsx | 그룹 컨테이너 |
| UnifiedHierarchy | UnifiedHierarchy.tsx | 계층 구조 표시 |
| UnifiedList | UnifiedList.tsx | 리스트 표시 |
| UnifiedMedia | UnifiedMedia.tsx | 파일/이미지/비디오 |
| UnifiedBiz | UnifiedBiz.tsx | 비즈니스 컴포넌트 |
| UnifiedFormContext | UnifiedFormContext.tsx | 폼 상태 관리 |
| V2Input | V2Input.tsx | 텍스트/숫자/이메일/채번 입력 |
| V2Select | V2Select.tsx | 선택박스/라디오/체크박스/카테고리 |
| V2Date | V2Date.tsx | 날짜/시간 입력 |
| V2Repeater | V2Repeater.tsx | 리피터 테이블 |
| V2Layout | V2Layout.tsx | 레이아웃 컨테이너 |
| V2Group | V2Group.tsx | 그룹 컨테이너 |
| V2Hierarchy | V2Hierarchy.tsx | 계층 구조 표시 |
| V2List | V2List.tsx | 리스트 표시 |
| V2Media | V2Media.tsx | 파일/이미지/비디오 |
| V2Biz | V2Biz.tsx | 비즈니스 컴포넌트 |
| V2FormContext | V2FormContext.tsx | 폼 상태 관리 |
### 8.2 inputType 자동 처리
Unified 컴포넌트는 `inputType`에 따라 자동으로 적절한 UI를 렌더링합니다:
V2 컴포넌트는 `inputType`에 따라 자동으로 적절한 UI를 렌더링합니다:
```typescript
// UnifiedInput.tsx
// V2Input.tsx
switch (inputType) {
case "numbering":
// 채번 규칙 자동 조회 및 코드 생성
@@ -386,7 +386,7 @@ switch (inputType) {
break;
}
// UnifiedSelect.tsx
// V2Select.tsx
switch (inputType) {
case "category":
// 카테고리 값 자동 조회 및 드롭다운 표시
@@ -468,8 +468,8 @@ V2 Core:
V2 컴포넌트:
- frontend/lib/registry/components/v2-*/
Unified 폼 컴포넌트:
- frontend/components/unified/
V2 폼 컴포넌트:
- frontend/components/v2/
채번/카테고리 테스트 테이블:
- db/migrations/040_create_numbering_rules_test.sql
@@ -534,6 +534,6 @@ frontend/lib/registry/components/
├── v2-table-search-widget/
├── v2-tabs-widget/
├── v2-text-display/
└── v2-unified-repeater/
└── v2-repeater/
```
@@ -15,29 +15,29 @@
### 상위 15개 컴포넌트
| 순위 | 컴포넌트 | 사용 횟수 | 사용 화면 수 | Unified 매핑 |
| 순위 | 컴포넌트 | 사용 횟수 | 사용 화면 수 | V2 매핑 |
| :--: | :-------------------------- | :-------: | :----------: | :------------------------------ |
| 1 | button-primary | 571 | 364 | UnifiedInput (type: button) |
| 2 | text-input | 805 | 166 | **UnifiedInput (type: text)** |
| 3 | table-list | 130 | 130 | UnifiedList (viewMode: table) |
| 4 | table-search-widget | 127 | 127 | UnifiedList (searchable: true) |
| 5 | select-basic | 121 | 76 | **UnifiedSelect** |
| 6 | number-input | 86 | 34 | **UnifiedInput (type: number)** |
| 7 | date-input | 83 | 51 | **UnifiedDate** |
| 8 | file-upload | 41 | 18 | UnifiedMedia (type: file) |
| 9 | tabs-widget | 39 | 39 | UnifiedGroup (type: tabs) |
| 10 | split-panel-layout | 39 | 39 | UnifiedLayout (type: split) |
| 11 | category-manager | 38 | 38 | UnifiedBiz (type: category) |
| 12 | numbering-rule | 31 | 31 | UnifiedBiz (type: numbering) |
| 1 | button-primary | 571 | 364 | V2Input (type: button) |
| 2 | text-input | 805 | 166 | **V2Input (type: text)** |
| 3 | table-list | 130 | 130 | V2List (viewMode: table) |
| 4 | table-search-widget | 127 | 127 | V2List (searchable: true) |
| 5 | select-basic | 121 | 76 | **V2Select** |
| 6 | number-input | 86 | 34 | **V2Input (type: number)** |
| 7 | date-input | 83 | 51 | **V2Date** |
| 8 | file-upload | 41 | 18 | V2Media (type: file) |
| 9 | tabs-widget | 39 | 39 | V2Group (type: tabs) |
| 10 | split-panel-layout | 39 | 39 | V2Layout (type: split) |
| 11 | category-manager | 38 | 38 | V2Biz (type: category) |
| 12 | numbering-rule | 31 | 31 | V2Biz (type: numbering) |
| 13 | selected-items-detail-input | 29 | 29 | 복합 컴포넌트 |
| 14 | modal-repeater-table | 25 | 25 | UnifiedList (modal: true) |
| 15 | image-widget | 29 | 29 | UnifiedMedia (type: image) |
| 14 | modal-repeater-table | 25 | 25 | V2List (modal: true) |
| 15 | image-widget | 29 | 29 | V2Media (type: image) |
---
## 2. Unified 컴포넌트별 통합 대상 분석
## 2. V2 컴포넌트별 통합 대상 분석
### UnifiedInput (예상 통합 대상: 891개)
### V2Input (예상 통합 대상: 891개)
| 기존 컴포넌트 | 사용 횟수 | 비율 |
| :------------ | :-------: | :---: |
@@ -46,7 +46,7 @@
**우선순위: 1위** - 가장 많이 사용되는 컴포넌트
### UnifiedSelect (예상 통합 대상: 140개)
### V2Select (예상 통합 대상: 140개)
| 기존 컴포넌트 | 사용 횟수 | widgetType |
| :------------------------ | :-------: | :--------- |
@@ -59,7 +59,7 @@
**우선순위: 2위** - 다양한 모드 지원 필요
### UnifiedDate (예상 통합 대상: 83개)
### V2Date (예상 통합 대상: 83개)
| 기존 컴포넌트 | 사용 횟수 |
| :---------------- | :-------: |
@@ -69,7 +69,7 @@
**우선순위: 3위**
### UnifiedList (예상 통합 대상: 283개)
### V2List (예상 통합 대상: 283개)
| 기존 컴포넌트 | 사용 횟수 | 비고 |
| :-------------------- | :-------: | :---------- |
@@ -82,14 +82,14 @@
**우선순위: 4위** - 핵심 데이터 표시 컴포넌트
### UnifiedMedia (예상 통합 대상: 70개)
### V2Media (예상 통합 대상: 70개)
| 기존 컴포넌트 | 사용 횟수 |
| :------------ | :-------: |
| file-upload | 41 |
| image-widget | 29 |
### UnifiedLayout (예상 통합 대상: 62개)
### V2Layout (예상 통합 대상: 62개)
| 기존 컴포넌트 | 사용 횟수 |
| :------------------ | :-------: |
@@ -97,7 +97,7 @@
| screen-split-panel | 21 |
| split-panel-layout2 | 2 |
### UnifiedGroup (예상 통합 대상: 99개)
### V2Group (예상 통합 대상: 99개)
| 기존 컴포넌트 | 사용 횟수 |
| :-------------------- | :-------: |
@@ -109,7 +109,7 @@
| universal-form-modal | 7 |
| repeat-screen-modal | 5 |
### UnifiedBiz (예상 통합 대상: 79개)
### V2Biz (예상 통합 대상: 79개)
| 기존 컴포넌트 | 사용 횟수 |
| :--------------------- | :-------: |
@@ -127,27 +127,27 @@
### Phase 1 우선순위 (즉시 효과가 큰 컴포넌트)
| 순위 | Unified 컴포넌트 | 통합 대상 수 | 영향 화면 수 | 이유 |
| 순위 | V2 컴포넌트 | 통합 대상 수 | 영향 화면 수 | 이유 |
| :---: | :---------------- | :----------: | :----------: | :--------------- |
| **1** | **UnifiedInput** | 891개 | 200+ | 가장 많이 사용 |
| **2** | **UnifiedSelect** | 140개 | 100+ | 다양한 모드 필요 |
| **3** | **UnifiedDate** | 83개 | 51 | 비교적 단순 |
| **1** | **V2Input** | 891개 | 200+ | 가장 많이 사용 |
| **2** | **V2Select** | 140개 | 100+ | 다양한 모드 필요 |
| **3** | **V2Date** | 83개 | 51 | 비교적 단순 |
### Phase 2 우선순위 (데이터 표시 컴포넌트)
| 순위 | Unified 컴포넌트 | 통합 대상 수 | 이유 |
| 순위 | V2 컴포넌트 | 통합 대상 수 | 이유 |
| :---: | :---------------- | :----------: | :--------------- |
| **4** | **UnifiedList** | 283개 | 핵심 데이터 표시 |
| **5** | **UnifiedLayout** | 62개 | 레이아웃 구조 |
| **6** | **UnifiedGroup** | 99개 | 콘텐츠 그룹화 |
| **4** | **V2List** | 283개 | 핵심 데이터 표시 |
| **5** | **V2Layout** | 62개 | 레이아웃 구조 |
| **6** | **V2Group** | 99개 | 콘텐츠 그룹화 |
### Phase 3 우선순위 (특수 컴포넌트)
| 순위 | Unified 컴포넌트 | 통합 대상 수 | 이유 |
| 순위 | V2 컴포넌트 | 통합 대상 수 | 이유 |
| :---: | :------------------- | :----------: | :------------ |
| **7** | **UnifiedMedia** | 70개 | 파일/이미지 |
| **8** | **UnifiedBiz** | 79개 | 비즈니스 특화 |
| **9** | **UnifiedHierarchy** | 0개 | 신규 기능 |
| **7** | **V2Media** | 70개 | 파일/이미지 |
| **8** | **V2Biz** | 79개 | 비즈니스 특화 |
| **9** | **V2Hierarchy** | 0개 | 신규 기능 |
---
@@ -156,8 +156,8 @@
### 4.1 button-primary 분리 검토
- 사용량: 571개 (1위)
- 현재 계획: UnifiedInput에 포함
- **제안**: 별도 `UnifiedButton` 컴포넌트로 분리 검토
- 현재 계획: V2Input에 포함
- **제안**: 별도 `V2Button` 컴포넌트로 분리 검토
- 버튼은 입력과 성격이 다름
- 액션 타입, 스타일, 권한 등 복잡한 설정 필요
@@ -181,5 +181,5 @@
1. [ ] 데이터 마이그레이션 전략 설계 (Phase 0-2)
2. [ ] sys_input_type JSON Schema 설계 (Phase 0-3)
3. [ ] DynamicConfigPanel 프로토타입 (Phase 0-4)
4. [ ] UnifiedInput 구현 시작 (Phase 1-1)
4. [ ] V2Input 구현 시작 (Phase 1-1)
@@ -67,8 +67,8 @@
"componentConfig": { ... },
// 신규 필드 추가
"unifiedType": "UnifiedInput", // 새로운 통합 컴포넌트 타입
"unifiedConfig": { // 새로운 설정 구조
"v2Type": "V2Input", // 새로운 통합 컴포넌트 타입
"v2Config": { // 새로운 설정 구조
"type": "text",
"format": "none",
"placeholder": "텍스트를 입력하세요"
@@ -87,13 +87,13 @@
### 2.2 렌더링 로직 수정
```typescript
// 렌더러에서 unifiedType 우선 사용
// 렌더러에서 v2Type 우선 사용
function renderComponent(props: ComponentProps) {
// 신규 타입이 있으면 Unified 컴포넌트 사용
if (props.unifiedType) {
return <UnifiedComponentRenderer
type={props.unifiedType}
config={props.unifiedConfig}
// 신규 타입이 있으면 V2 컴포넌트 사용
if (props.v2Type) {
return <V2ComponentRenderer
type={props.v2Type}
config={props.v2Config}
/>;
}
@@ -109,7 +109,7 @@ function renderComponent(props: ComponentProps) {
## 3. 컴포넌트별 매핑 규칙
### 3.1 text-input → UnifiedInput
### 3.1 text-input → V2Input
```typescript
// AS-IS
@@ -126,8 +126,8 @@ function renderComponent(props: ComponentProps) {
// TO-BE
{
"unifiedType": "UnifiedInput",
"unifiedConfig": {
"v2Type": "V2Input",
"v2Config": {
"type": "text", // componentConfig.webType 또는 "text"
"format": "none", // componentConfig.format
"placeholder": "..." // componentConfig.placeholder
@@ -135,7 +135,7 @@ function renderComponent(props: ComponentProps) {
}
```
### 3.2 number-input → UnifiedInput
### 3.2 number-input → V2Input
```typescript
// AS-IS
@@ -152,8 +152,8 @@ function renderComponent(props: ComponentProps) {
// TO-BE
{
"unifiedType": "UnifiedInput",
"unifiedConfig": {
"v2Type": "V2Input",
"v2Config": {
"type": "number",
"min": 0,
"max": 100,
@@ -162,7 +162,7 @@ function renderComponent(props: ComponentProps) {
}
```
### 3.3 select-basic → UnifiedSelect
### 3.3 select-basic → V2Select
```typescript
// AS-IS (code 타입)
@@ -178,8 +178,8 @@ function renderComponent(props: ComponentProps) {
// TO-BE
{
"unifiedType": "UnifiedSelect",
"unifiedConfig": {
"v2Type": "V2Select",
"v2Config": {
"mode": "dropdown",
"source": "code",
"codeGroup": "ORDER_STATUS"
@@ -200,8 +200,8 @@ function renderComponent(props: ComponentProps) {
// TO-BE
{
"unifiedType": "UnifiedSelect",
"unifiedConfig": {
"v2Type": "V2Select",
"v2Config": {
"mode": "dropdown",
"source": "entity",
"searchable": true,
@@ -211,7 +211,7 @@ function renderComponent(props: ComponentProps) {
}
```
### 3.4 date-input → UnifiedDate
### 3.4 date-input → V2Date
```typescript
// AS-IS
@@ -226,8 +226,8 @@ function renderComponent(props: ComponentProps) {
// TO-BE
{
"unifiedType": "UnifiedDate",
"unifiedConfig": {
"v2Type": "V2Date",
"v2Config": {
"type": "date",
"format": "YYYY-MM-DD"
}
@@ -245,11 +245,11 @@ function renderComponent(props: ComponentProps) {
interface MigrationResult {
success: boolean;
unifiedType: string;
unifiedConfig: Record<string, any>;
v2Type: string;
v2Config: Record<string, any>;
}
export function migrateToUnified(
export function migrateToV2(
componentType: string,
componentConfig: Record<string, any>
): MigrationResult {
@@ -258,8 +258,8 @@ export function migrateToUnified(
case 'text-input':
return {
success: true,
unifiedType: 'UnifiedInput',
unifiedConfig: {
v2Type: 'V2Input',
v2Config: {
type: componentConfig.webType || 'text',
format: componentConfig.format || 'none',
placeholder: componentConfig.placeholder
@@ -269,8 +269,8 @@ export function migrateToUnified(
case 'number-input':
return {
success: true,
unifiedType: 'UnifiedInput',
unifiedConfig: {
v2Type: 'V2Input',
v2Config: {
type: 'number',
min: componentConfig.min,
max: componentConfig.max,
@@ -281,8 +281,8 @@ export function migrateToUnified(
case 'select-basic':
return {
success: true,
unifiedType: 'UnifiedSelect',
unifiedConfig: {
v2Type: 'V2Select',
v2Config: {
mode: 'dropdown',
source: componentConfig.webType || 'static',
codeGroup: componentConfig.codeCategory,
@@ -295,8 +295,8 @@ export function migrateToUnified(
case 'date-input':
return {
success: true,
unifiedType: 'UnifiedDate',
unifiedConfig: {
v2Type: 'V2Date',
v2Config: {
type: componentConfig.webType || 'date',
format: componentConfig.format
}
@@ -305,8 +305,8 @@ export function migrateToUnified(
default:
return {
success: false,
unifiedType: '',
unifiedConfig: {}
v2Type: '',
v2Config: {}
};
}
}
@@ -322,8 +322,8 @@ SELECT * FROM screen_layouts;
-- 마이그레이션 실행 (text-input 예시)
UPDATE screen_layouts
SET properties = properties || jsonb_build_object(
'unifiedType', 'UnifiedInput',
'unifiedConfig', jsonb_build_object(
'v2Type', 'V2Input',
'v2Config', jsonb_build_object(
'type', COALESCE(properties->'componentConfig'->>'webType', 'text'),
'format', COALESCE(properties->'componentConfig'->>'format', 'none'),
'placeholder', properties->'componentConfig'->>'placeholder'
@@ -352,7 +352,7 @@ WHERE sl.layout_id = slb.layout_id;
-- 또는 신규 필드만 제거
UPDATE screen_layouts
SET properties = properties - 'unifiedType' - 'unifiedConfig' - '_migration';
SET properties = properties - 'v2Type' - 'v2Config' - '_migration';
```
### 5.2 단계적 롤백
@@ -362,7 +362,7 @@ SET properties = properties - 'unifiedType' - 'unifiedConfig' - '_migration';
async function rollbackScreen(screenId: number) {
await db.query(`
UPDATE screen_layouts sl
SET properties = properties - 'unifiedType' - 'unifiedConfig' - '_migration'
SET properties = properties - 'v2Type' - 'v2Config' - '_migration'
WHERE screen_id = $1
`, [screenId]);
}
@@ -375,9 +375,9 @@ async function rollbackScreen(screenId: number) {
| 단계 | 작업 | 대상 | 시점 |
|:---:|:---|:---|:---|
| 1 | 백업 테이블 생성 | 전체 | Phase 1 시작 전 |
| 2 | UnifiedInput 마이그레이션 | text-input, number-input | Phase 1 중 |
| 3 | UnifiedSelect 마이그레이션 | select-basic | Phase 1 중 |
| 4 | UnifiedDate 마이그레이션 | date-input | Phase 1 중 |
| 2 | V2Input 마이그레이션 | text-input, number-input | Phase 1 중 |
| 3 | V2Select 마이그레이션 | select-basic | Phase 1 중 |
| 4 | V2Date 마이그레이션 | date-input | Phase 1 중 |
| 5 | 검증 및 테스트 | 전체 | Phase 1 완료 후 |
| 6 | 레거시 필드 제거 | 전체 | Phase 5 (추후) |
@@ -477,7 +477,7 @@ className={cn(
- ✅ `FileComponentConfigPanel.tsx`: `text-gray-900``text-foreground`, `text-blue-*``text-primary`
- ✅ `ButtonConfigPanel.tsx`: 모든 `text-gray-*`, `bg-gray-*`, `hover:bg-gray-*` 교체
- ✅ `UnifiedPropertiesPanel.tsx`: 모든 `text-gray-*`, `border-gray-*` 교체
- ✅ `V2PropertiesPanel.tsx`: 모든 `text-gray-*`, `border-gray-*` 교체
- ✅ `app/(main)/admin/page.tsx`: 전체 페이지 하드코딩 색상 교체
- ✅ `CardDisplayComponent.tsx`: 모든 `text-gray-*`, `bg-gray-*`, 인라인 색상 교체
- ✅ `getComponentConfigPanel.tsx`: 로딩 상태 하드코딩 색상 교체
@@ -0,0 +1,346 @@
# 노드 플로우 데이터 소스 설정 가이드
## 개요
노드 플로우 편집기에서 **테이블 소스 노드**와 **외부 DB 소스 노드**에 데이터 소스 타입을 설정할 수 있습니다. 이제 버튼에서 전달된 데이터를 사용할지, 아니면 테이블의 전체 데이터를 직접 조회할지 선택할 수 있습니다.
## 지원 노드
### 1. 테이블 소스 노드 (내부 DB)
- **위치**: 노드 팔레트 > 데이터 소스 > 테이블 소스
- **용도**: 내부 데이터베이스의 테이블 데이터 조회
### 2. 외부 DB 소스 노드
- **위치**: 노드 팔레트 > 데이터 소스 > 외부 DB 소스
- **용도**: 외부 데이터베이스의 테이블 데이터 조회
## 데이터 소스 타입
### 1. 컨텍스트 데이터 (기본값)
```
💡 컨텍스트 데이터 모드
버튼 실행 시 전달된 데이터(폼 데이터, 테이블 선택 항목 등)를 사용합니다.
사용 예시:
• 폼 데이터: 1개 레코드
• 테이블 선택: N개 레코드
```
**특징:**
- ✅ 버튼에서 제어한 데이터만 처리
- ✅ 성능 우수 (필요한 데이터만 사용)
- ✅ 사용자가 선택한 데이터만 처리
- ⚠️ 버튼 설정에서 데이터 소스를 올바르게 설정해야 함
**사용 시나리오:**
- 폼 데이터로 새 레코드 생성
- 테이블에서 선택한 항목 일괄 업데이트
- 사용자가 선택한 데이터만 처리
### 2. 테이블 전체 데이터
```
📊 테이블 전체 데이터 모드
선택한 테이블의 **모든 행**을 직접 조회합니다.
⚠️ 대량 데이터 시 성능 주의
```
**특징:**
- ✅ 테이블의 모든 데이터 처리
- ✅ 버튼 설정과 무관하게 동작
- ✅ 자동으로 전체 데이터 조회
- ⚠️ 대량 데이터 시 메모리 및 성능 이슈 가능
- ⚠️ 네트워크 부하 증가
**사용 시나리오:**
- 전체 데이터 통계/집계
- 일괄 데이터 마이그레이션
- 전체 데이터 검증
- 백업/복원 작업
## 설정 방법
### 1단계: 노드 추가
1. 노드 플로우 편집기 열기
2. 좌측 팔레트에서 **테이블 소스** 또는 **외부 DB 소스** 드래그
3. 캔버스에 노드 배치
### 2단계: 테이블 선택
1. 노드 클릭하여 선택
2. 우측 **속성 패널** 열림
3. **테이블 선택** 드롭다운에서 테이블 선택
### 3단계: 데이터 소스 설정
1. **데이터 소스 설정** 섹션으로 스크롤
2. **데이터 소스 타입** 드롭다운 클릭
3. 원하는 모드 선택:
- **컨텍스트 데이터**: 버튼에서 전달된 데이터 사용
- **테이블 전체 데이터**: 테이블의 모든 행 조회
### 4단계: 저장
- 변경 사항은 **즉시 노드에 반영**됩니다.
- 별도 저장 버튼 불필요 (자동 저장)
## 사용 예시
### 예시 1: 선택된 항목만 처리 (컨텍스트 데이터)
**시나리오**: 사용자가 테이블에서 선택한 주문만 승인 처리
**플로우 구성:**
```
[테이블 소스: orders]
└─ 데이터 소스: 컨텍스트 데이터
└─ [조건: status = 'pending']
└─ [업데이트: status = 'approved']
```
**버튼 설정:**
- 제어 데이터 소스: `table-selection` (테이블 선택 항목)
**실행 결과:**
- 사용자가 선택한 3개 주문만 승인 처리
- 나머지 주문은 변경되지 않음
### 예시 2: 전체 데이터 일괄 처리 (테이블 전체 데이터)
**시나리오**: 모든 고객의 등급을 재계산
**플로우 구성:**
```
[테이블 소스: customers]
└─ 데이터 소스: 테이블 전체 데이터
└─ [데이터 변환: 등급 계산]
└─ [업데이트: grade = 계산된 등급]
```
**버튼 설정:**
- 제어 데이터 소스: 무관 (테이블 전체를 자동 조회)
**실행 결과:**
- 모든 고객 레코드의 등급 재계산
- 1,000개 고객 → 1,000개 모두 업데이트
### 예시 3: 외부 DB 전체 데이터 동기화
**시나리오**: 외부 ERP의 모든 제품 정보를 내부 DB로 동기화
**플로우 구성:**
```
[외부 DB 소스: products]
└─ 데이터 소스: 테이블 전체 데이터
└─ [Upsert: 내부 DB products 테이블]
```
**실행 결과:**
- 외부 DB의 모든 제품 데이터 조회
- 내부 DB에 동기화 (있으면 업데이트, 없으면 삽입)
## 노드 실행 로직
### 컨텍스트 데이터 모드 실행 흐름
```typescript
// 1. 버튼 클릭
// 2. 버튼에서 데이터 전달 (폼, 테이블 선택 등)
// 3. 노드 플로우 실행
// 4. 테이블 소스 노드가 전달받은 데이터 사용
{
nodeType: "tableSource",
config: {
tableName: "orders",
dataSourceType: "context-data"
},
// 실행 시 버튼에서 전달된 데이터 사용
input: [
{ id: 1, status: "pending" },
{ id: 2, status: "pending" }
]
}
```
### 테이블 전체 데이터 모드 실행 흐름
```typescript
// 1. 버튼 클릭
// 2. 노드 플로우 실행
// 3. 테이블 소스 노드가 직접 DB 조회
// 4. 모든 행을 반환
{
nodeType: "tableSource",
config: {
tableName: "orders",
dataSourceType: "table-all"
},
// 실행 시 DB에서 전체 데이터 조회
query: "SELECT * FROM orders",
output: [
{ id: 1, status: "pending" },
{ id: 2, status: "approved" },
{ id: 3, status: "completed" },
// ... 수천 개의 행
]
}
```
## 성능 고려사항
### 컨텍스트 데이터 모드
-**성능 우수**: 필요한 데이터만 처리
-**메모리 효율**: 선택된 데이터만 메모리에 로드
-**네트워크 효율**: 최소한의 데이터 전송
### 테이블 전체 데이터 모드
- ⚠️ **대량 데이터 주의**: 수천~수만 개 행 처리 시 느려질 수 있음
- ⚠️ **메모리 사용**: 모든 데이터를 메모리에 로드
- ⚠️ **네트워크 부하**: 전체 데이터 전송
**권장 사항:**
```
• 데이터가 1,000개 이하: 테이블 전체 데이터 사용 가능
• 데이터가 10,000개 이상: 컨텍스트 데이터 + 필터링 권장
• 데이터가 100,000개 이상: 배치 처리 또는 서버 사이드 처리 필요
```
## 디버깅
### 콘솔 로그 확인
**데이터 소스 타입 변경 시:**
```
✅ 데이터 소스 타입 변경: table-all
```
**노드 실행 시:**
```typescript
// 컨텍스트 데이터 모드
🔍 실행: orders
📊 데이터: 3건 ( )
// 테이블 전체 데이터 모드
🔍 실행: orders
📊 조회: 1,234
```
### 일반적인 문제
#### Q1: 컨텍스트 데이터 모드인데 데이터가 없습니다
**A**: 버튼 설정을 확인하세요.
- 버튼 설정 > 제어 데이터 소스가 올바르게 설정되어 있는지 확인
- 폼 데이터: `form`
- 테이블 선택: `table-selection`
- 테이블 전체: `table-all`
#### Q2: 테이블 전체 데이터 모드가 느립니다
**A**:
1. 데이터 양 확인 (몇 개 행인지?)
2. 필요하면 컨텍스트 데이터 + 필터링으로 변경
3. WHERE 조건으로 범위 제한
#### Q3: 외부 DB 소스가 오래 걸립니다
**A**:
1. 외부 DB 연결 상태 확인
2. 네트워크 지연 확인
3. 외부 DB의 인덱스 확인
## 버튼 설정과의 관계
### 버튼 데이터 소스 vs 노드 데이터 소스
| 버튼 설정 | 노드 설정 | 결과 |
|---------|---------|-----|
| `table-selection` | `context-data` | 선택된 항목만 처리 ✅ |
| `table-all` | `context-data` | 전체 데이터 전달됨 ⚠️ |
| 무관 | `table-all` | 노드가 직접 전체 조회 ✅ |
| `form` | `context-data` | 폼 데이터만 처리 ✅ |
**권장 조합:**
```
1. 선택된 항목 처리:
버튼: table-selection → 노드: context-data
2. 테이블 전체 처리:
버튼: 무관 → 노드: table-all
3. 폼 데이터 처리:
버튼: form → 노드: context-data
```
## 마이그레이션 가이드
### 기존 노드 업데이트
기존에 생성된 노드는 **자동으로 `context-data` 모드**로 설정됩니다.
**업데이트 방법:**
1. 노드 선택
2. 속성 패널 열기
3. 데이터 소스 설정 섹션에서 `table-all`로 변경
## 베스트 프랙티스
### ✅ 좋은 예
```typescript
// 시나리오: 사용자가 선택한 주문 취소
[ 소스: orders]
dataSourceType: "context-data" // ✅ 선택된 주문만 처리
[업데이트: status = 'cancelled']
```
```typescript
// 시나리오: 모든 만료된 쿠폰 삭제
[ 소스: coupons]
dataSourceType: "table-all" // ✅ 전체 조회 후 필터링
[조건: expiry_date < today]
[]
```
### ❌ 나쁜 예
```typescript
// 시나리오: 단일 주문 업데이트인데 전체 조회
[ 소스: orders]
dataSourceType: "table-all" // ❌ 불필요한 전체 조회
[조건: id = 123] // 한 개만 필요한데 전체를 조회함
[]
```
## 요약
### 언제 어떤 모드를 사용해야 하나요?
| 상황 | 권장 모드 |
|------|----------|
| 폼 데이터로 새 레코드 생성 | 컨텍스트 데이터 |
| 테이블에서 선택한 항목 수정 | 컨텍스트 데이터 |
| 전체 데이터 통계/집계 | 테이블 전체 데이터 |
| 일괄 데이터 마이그레이션 | 테이블 전체 데이터 |
| 특정 조건의 데이터 처리 | 테이블 전체 데이터 + 조건 |
| 외부 DB 동기화 | 테이블 전체 데이터 |
### 핵심 원칙
1. **기본은 컨텍스트 데이터**: 대부분의 경우 이것으로 충분합니다.
2. **전체 데이터는 신중히**: 성능 영향을 고려하세요.
3. **버튼과 노드를 함께 설계**: 데이터 흐름을 명확히 이해하세요.
## 관련 문서
- [제어관리_데이터소스_확장_가이드.md](./제어관리_데이터소스_확장_가이드.md) - 버튼 데이터 소스 설정
- 노드 플로우 기본 가이드 (준비 중)
## 업데이트 이력
- **2025-01-24**: 초기 문서 작성
- 테이블 소스 노드에 데이터 소스 타입 추가
- 외부 DB 소스 노드에 데이터 소스 타입 추가
- `context-data`, `table-all` 모드 지원
@@ -0,0 +1,230 @@
# 데이터 소스 일관성 개선 완료
## 문제점
기존에는 데이터 소스 설정이 일관성 없이 동작했습니다:
- ❌ 테이블 위젯에서 선택한 행 → 노드는 선택된 행만 처리
- ❌ 플로우 위젯에서 선택한 데이터 → 노드는 **전체 테이블** 조회 (예상과 다름)
- ❌ 노드에 `dataSourceType` 설정이 있어도 백엔드가 무시
## 해결 방법
### 1. 백엔드 로직 개선
#### 테이블 소스 노드 (내부 DB)
```typescript
// nodeFlowExecutionService.ts - executeTableSource()
const nodeDataSourceType = dataSourceType || "context-data";
if (nodeDataSourceType === "context-data") {
// 버튼에서 전달된 데이터 사용 (폼, 선택 항목 등)
return context.sourceData;
}
if (nodeDataSourceType === "table-all") {
// 테이블 전체 데이터를 직접 조회
const sql = `SELECT * FROM ${tableName}`;
return await query(sql);
}
```
#### 외부 DB 소스 노드
```typescript
// nodeFlowExecutionService.ts - executeExternalDBSource()
const nodeDataSourceType = dataSourceType || "context-data";
if (nodeDataSourceType === "context-data") {
// 버튼에서 전달된 데이터 사용
return context.sourceData;
}
if (nodeDataSourceType === "table-all") {
// 외부 DB 테이블 전체 데이터를 직접 조회
const result = await poolService.executeQuery(connectionId, sql);
return result;
}
```
### 2. 데이터 흐름 정리
```
┌──────────────────────────────────────────────────────────┐
│ 버튼 클릭 │
├──────────────────────────────────────────────────────────┤
│ 버튼 데이터 소스 설정: │
│ - form │
│ - table-selection │
│ - table-all │
│ - flow-selection │
│ - flow-step-all │
└──────────────────────────────────────────────────────────┘
prepareContextData()
(버튼에서 설정한 데이터 준비)
┌──────────────────────────────────────────────────────────┐
│ contextData = { │
│ sourceData: [...] // 버튼에서 전달된 데이터 │
│ formData: {...} │
│ selectedRowsData: [...] │
│ tableAllData: [...] │
│ } │
└──────────────────────────────────────────────────────────┘
노드 플로우 실행
┌──────────────────────────────────────────────────────────┐
│ 테이블 소스 노드 │
├──────────────────────────────────────────────────────────┤
│ 노드 데이터 소스 설정: │
│ │
│ context-data 모드: │
│ → contextData.sourceData 사용 │
│ → 버튼에서 전달된 데이터 그대로 사용 │
│ │
│ table-all 모드: │
│ → contextData 무시 │
│ → DB에서 테이블 전체 데이터 직접 조회 │
└──────────────────────────────────────────────────────────┘
```
## 사용 시나리오
### 시나리오 1: 선택된 항목만 처리
```
[버튼 설정]
- 데이터 소스: table-selection
[노드 설정]
- 테이블 소스 노드: context-data
[결과]
✅ 사용자가 선택한 행만 제어 실행
```
### 시나리오 2: 테이블 전체 처리 (버튼 방식)
```
[버튼 설정]
- 데이터 소스: table-all
[노드 설정]
- 테이블 소스 노드: context-data
[결과]
✅ 버튼이 테이블 전체 데이터를 로드하여 전달
✅ 노드는 전달받은 전체 데이터 처리
```
### 시나리오 3: 테이블 전체 처리 (노드 방식)
```
[버튼 설정]
- 데이터 소스: 무관 (또는 form)
[노드 설정]
- 테이블 소스 노드: table-all
[결과]
✅ 버튼 데이터 무시
✅ 노드가 직접 테이블 전체 데이터 조회
```
### 시나리오 4: 폼 데이터로 처리
```
[버튼 설정]
- 데이터 소스: form
[노드 설정]
- 테이블 소스 노드: context-data
[결과]
✅ 폼 입력값만 제어 실행
```
## 일관성 규칙
### 규칙 1: 노드가 context-data 모드일 때
- **버튼에서 전달된 데이터를 그대로 사용**
- 버튼의 `controlDataSource` 설정이 중요
- `form` → 폼 데이터 사용
- `table-selection` → 선택된 행 사용
- `table-all` → 테이블 전체 사용 (버튼이 로드)
- `flow-selection` → 플로우 선택 항목 사용
### 규칙 2: 노드가 table-all 모드일 때
- **버튼 설정 무시**
- 노드가 직접 DB에서 전체 데이터 조회
- 대량 데이터 시 성능 주의
### 규칙 3: 기본 동작
- 노드의 `dataSourceType`이 없으면 `context-data` 기본값
- 버튼의 `controlDataSource`가 없으면 자동 판단
## 권장 사항
### 일반적인 사용 패턴
| 상황 | 버튼 설정 | 노드 설정 |
|------|----------|----------|
| 선택 항목 처리 | `table-selection` | `context-data` |
| 폼 데이터 처리 | `form` | `context-data` |
| 전체 데이터 처리 (소량) | `table-all` | `context-data` |
| 전체 데이터 처리 (대량) | `form` 또는 무관 | `table-all` |
| 플로우 선택 처리 | `flow-selection` | `context-data` |
### 성능 고려사항
**버튼에서 전체 로드 vs 노드에서 전체 조회:**
```
버튼 방식 (table-all):
장점: 한 번만 조회하여 여러 노드에서 재사용 가능
단점: 플로우 실행 전에 전체 데이터 로드 (시작 지연)
노드 방식 (table-all):
장점: 필요한 노드만 조회 (선택적 로드)
단점: 여러 노드에서 사용 시 중복 조회
권장: 데이터가 많으면 노드 방식, 재사용이 많으면 버튼 방식
```
## 로그 확인
### 성공적인 실행 예시
```
📊 테이블 소스 노드 실행: orders, dataSourceType=context-data
📊 컨텍스트 데이터 사용: table-selection, 3건
✅ 노드 실행 완료: 3건 처리
또는
📊 테이블 소스 노드 실행: customers, dataSourceType=table-all
📊 테이블 전체 데이터 조회: customers, 1,234건
✅ 노드 실행 완료: 1,234건 처리
```
### 문제가 있는 경우
```
⚠️ context-data 모드이지만 전달된 데이터가 없습니다. 빈 배열 반환.
해결: 버튼의 controlDataSource 설정 확인
```
## 업데이트 내역
- **2025-01-24**: 백엔드 로직 개선 완료
- `executeTableSource()` 함수에 `dataSourceType` 처리 추가
- `executeExternalDBSource()` 함수에 `dataSourceType` 처리 추가
- 노드 설정이 올바르게 반영되도록 수정
- 일관성 있는 데이터 흐름 확립
@@ -0,0 +1,381 @@
# 동적 테이블 접근 시스템 개선 완료
> **작성일**: 2025-01-04
> **목적**: 화이트리스트 제거 및 동적 테이블 접근 시스템 구축
---
## 문제 상황
### 기존 시스템의 문제점
```typescript
// ❌ 기존 방식: 하드코딩된 화이트리스트
const ALLOWED_TABLES = [
"company_mng",
"user_info",
"dept_info",
"item_info", // 매번 수동으로 추가해야 함!
// ... 계속 추가해야 함
];
// 문제:
// 1. 새 테이블 생성 시마다 코드 수정 필요
// 2. 동적 테이블 생성 기능과 충돌
// 3. 유지보수 어려움
// 4. 확장성 부족
```
### 발생한 에러
```
GET /api/data/item_info?page=1&size=100&userLang=KR
-> 400 Bad Request
-> 접근이 허용되지 않은 테이블입니다: item_info
```
---
## 개선된 시스템
### 1. 블랙리스트 방식으로 전환
```typescript
/**
* 접근 금지 테이블 목록 (블랙리스트)
* 시스템 중요 테이블 및 보안상 접근 금지할 테이블만 명시
*/
const BLOCKED_TABLES = [
"pg_catalog",
"pg_statistic",
"pg_database",
"pg_user",
"information_schema",
"session_tokens", // 세션 토큰 테이블
"password_history", // 패스워드 이력
];
// ✅ 장점:
// - 금지할 테이블만 명시 (시스템 테이블)
// - 비즈니스 테이블은 자유롭게 추가 가능
// - 코드 수정 불필요
```
### 2. 테이블명 검증 강화
```typescript
/**
* 테이블 이름 검증 정규식
* SQL 인젝션 방지: 영문, 숫자, 언더스코어만 허용
*/
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
// 검증 순서:
// 1. 정규식으로 형식 검증 (SQL 인젝션 방지)
// 2. 블랙리스트 확인 (시스템 테이블 차단)
// 3. 테이블 존재 여부 확인 (실제 존재하는 테이블만)
```
### 3. 자동 회사별 필터링
```typescript
// ✅ company_code 컬럼 자동 감지
if (userCompany && userCompany !== "*") {
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code");
if (hasCompanyCode) {
whereConditions.push(`company_code = $${paramIndex}`);
queryParams.push(userCompany);
paramIndex++;
console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`);
}
}
// 동작 방식:
// - company_code 컬럼이 있으면 자동으로 필터링 적용
// - 최고 관리자(company_code = "*")는 전체 데이터 조회 가능
// - 일반 사용자는 자기 회사 데이터만 조회
```
### 4. 공통 검증 메서드
```typescript
/**
* 테이블 접근 검증 (공통 메서드)
*/
private async validateTableAccess(
tableName: string
): Promise<{ valid: boolean; error?: ServiceResponse<any> }> {
// 1. 테이블명 형식 검증 (SQL 인젝션 방지)
if (!TABLE_NAME_REGEX.test(tableName)) {
return { valid: false, error: { /* ... */ } };
}
// 2. 블랙리스트 검증
if (BLOCKED_TABLES.includes(tableName)) {
return { valid: false, error: { /* ... */ } };
}
// 3. 테이블 존재 여부 확인
const tableExists = await this.checkTableExists(tableName);
if (!tableExists) {
return { valid: false, error: { /* ... */ } };
}
return { valid: true };
}
// 모든 메서드에서 재사용:
// - getTableData()
// - getTableColumns()
// - getRecordDetail()
// - createRecord()
// - updateRecord()
// - deleteRecord()
// - getJoinedData()
```
---
## 개선 효과
### Before (화이트리스트 방식)
```typescript
// 1. item_info 테이블 생성
CREATE TABLE item_info (...);
// 2. 백엔드 코드 수정 필요 ❌
const ALLOWED_TABLES = [
// ...기존 테이블들
"item_info", // 수동으로 추가!
];
const COMPANY_FILTERED_TABLES = [
// ...기존 테이블들
"item_info", // 또 추가!
];
// 3. 서버 재시작 필요
// 4. 테스트
```
### After (블랙리스트 방식)
```typescript
// 1. item_info 테이블 생성
CREATE TABLE item_info (
id SERIAL PRIMARY KEY,
company_code VARCHAR(20) NOT NULL, -- !
name VARCHAR(100),
...
);
// 2. 코드 수정 불필요 ✅
// 3. 서버 재시작 불필요 ✅
// 4. 즉시 사용 가능 ✅
```
---
## 보안 강화
### 1. SQL 인젝션 방지
```typescript
// ❌ 위험한 테이블명
"user_info; DROP TABLE users; --" ->
"../../etc/passwd" ->
"pg_user" ->
// ✅ 안전한 테이블명
"user_info" ->
"item_info" ->
"order_mng_001" ->
```
### 2. 시스템 테이블 보호
```typescript
const BLOCKED_TABLES = [
"pg_catalog", // PostgreSQL 카탈로그
"pg_statistic", // 통계 정보
"pg_database", // 데이터베이스 목록
"pg_user", // 사용자 정보
"information_schema", // 스키마 정보
"session_tokens", // 세션 토큰
"password_history", // 패스워드 이력
];
```
### 3. 멀티테넌시 자동 적용
```typescript
// 테이블에 company_code 컬럼이 있으면 자동으로:
// 일반 사용자 (company_code = "COMPANY_A")
SELECT * FROM item_info WHERE company_code = 'COMPANY_A';
// 최고 관리자 (company_code = "*")
SELECT * FROM item_info; --
```
---
## 사용 예시
### 1. 새 테이블 생성
```sql
-- 회사별 데이터 격리가 필요한 테이블
CREATE TABLE product_catalog (
id SERIAL PRIMARY KEY,
company_code VARCHAR(20) NOT NULL, -- 자동 필터링 활성화
product_name VARCHAR(100),
price DECIMAL(10, 2),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 전역 공통 테이블 (회사별 격리 불필요)
CREATE TABLE global_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(50),
setting_value TEXT
);
```
### 2. API 호출
```typescript
// 프론트엔드에서 그냥 호출하면 끝!
const response = await apiClient.get("/api/data/product_catalog", {
params: { page: 1, size: 100 }
});
// 백엔드에서 자동으로:
// 1. 테이블 존재 확인 ✓
// 2. company_code 컬럼 확인 ✓
// 3. 회사별 필터링 적용 ✓
// 4. 데이터 반환 ✓
```
### 3. 동적 테이블 생성 (DDL API 연동)
```typescript
// 1. DDL API로 테이블 생성
POST /api/ddl/tables
{
"tableName": "customer_feedback",
"columns": [
{ "name": "company_code", "type": "VARCHAR(20)", "nullable": false },
{ "name": "feedback_text", "type": "TEXT" },
{ "name": "rating", "type": "INTEGER" }
]
}
// 2. 즉시 데이터 조회 가능 (코드 수정 없음)
GET /api/data/customer_feedback
```
---
## 변경된 파일
### backend-node/src/services/dataService.ts
**변경 사항:**
- ❌ 제거: `ALLOWED_TABLES` 화이트리스트
- ❌ 제거: `COMPANY_FILTERED_TABLES` 하드코딩
- ✅ 추가: `BLOCKED_TABLES` 블랙리스트
- ✅ 추가: `TABLE_NAME_REGEX` 정규식 검증
- ✅ 추가: `validateTableAccess()` 공통 검증 메서드
- ✅ 추가: `checkColumnExists()` 컬럼 존재 확인 메서드
- ✅ 개선: 자동 회사별 필터링 로직
---
## 테스트 체크리스트
### 기본 기능
- [x] 기존 테이블 조회 정상 작동
- [x] 새로운 테이블 조회 정상 작동
- [x] 존재하지 않는 테이블 접근 시 적절한 에러
- [x] 블랙리스트 테이블 접근 시 차단
### 보안
- [x] SQL 인젝션 시도 차단
- [x] 시스템 테이블 접근 차단
- [x] 회사별 데이터 격리 정상 작동
- [x] 최고 관리자 전체 데이터 조회 가능
### 성능
- [x] company_code 컬럼 존재 여부 확인 성능 (캐싱 가능)
- [x] 테이블 존재 여부 확인 성능
- [x] 정규식 검증 성능 (충분히 빠름)
---
## 향후 개선 사항
### 1. 컬럼 존재 여부 캐싱
```typescript
// 성능 최적화: 컬럼 정보 캐싱
private columnCache = new Map<string, Set<string>>();
private async checkColumnExists(
tableName: string,
columnName: string
): Promise<boolean> {
// 캐시 확인
if (this.columnCache.has(tableName)) {
return this.columnCache.get(tableName)!.has(columnName);
}
// 테이블의 모든 컬럼 조회 및 캐싱
const columns = await this.getTableColumnsSimple(tableName);
const columnSet = new Set(columns.map(c => c.column_name));
this.columnCache.set(tableName, columnSet);
return columnSet.has(columnName);
}
```
### 2. 블랙리스트 패턴 매칭
```typescript
// pg_* 형태의 패턴 지원
const BLOCKED_TABLE_PATTERNS = [
/^pg_/, // pg_로 시작하는 모든 테이블
/^information_/, // information_으로 시작
/_password$/, // _password로 끝나는 테이블
];
```
### 3. 테이블별 접근 권한 시스템
```typescript
// 향후: 사용자 역할별 테이블 접근 권한
interface TablePermission {
tableName: string;
roles: string[]; // ["ADMIN", "USER", "VIEWER"]
operations: string[]; // ["read", "write", "delete"]
}
```
---
## 결론
**동적 테이블 접근 시스템 구축 완료**
- 화이트리스트 제거로 유지보수 부담 해소
- 블랙리스트 방식으로 보안 유지
- 자동 회사별 필터링으로 멀티테넌시 보장
- 새 테이블 추가 시 코드 수정 불필요
**이제 테이블을 만들 때마다 코드를 수정할 필요가 없습니다!**
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,372 @@
# 🚨 버튼 제어관리 기능 통합 - 잠재적 문제점 및 해결방안
## 📊 성능 관련 문제점
### 1. **버튼 클릭 시 지연 시간 증가**
**문제점:**
- 기존 버튼은 단순한 액션만 수행 (50-100ms)
- 제어관리 추가 시 복합적인 처리로 인한 지연 가능성
- 데이터베이스 조건 검증: 100-300ms
- 복잡한 비즈니스 로직 실행: 500ms-2초
- 다중 테이블 업데이트: 1-5초
**현재 코드에서 확인된 문제:**
```typescript
// InteractiveScreenViewer.tsx에서 버튼 클릭 처리가 동기적
const handleButtonClick = async () => {
// 기존에는 단순한 액션만
switch (actionType) {
case "save":
await handleSaveAction();
break; // ~100ms
// 제어관리 추가 시 복합 처리로 증가 예상
}
};
```
**해결방안:**
1. **비동기 처리 + 로딩 상태**
```typescript
const [isExecuting, setIsExecuting] = useState(false);
const handleButtonClick = async () => {
setIsExecuting(true);
try {
// 제어관리 실행
} finally {
setIsExecuting(false);
}
};
```
2. **백그라운드 실행 옵션**
```typescript
// 긴급하지 않은 제어관리는 백그라운드에서 실행
if (config.executionOptions?.asyncExecution) {
// 즉시 성공 응답
// 백그라운드에서 제어관리 실행
}
```
### 2. **메모리 사용량 증가**
**문제점:**
- 각 버튼마다 제어관리 설정을 메모리에 보관
- 복잡한 조건/액션 설정으로 인한 메모리 사용량 증가
- 대량의 버튼이 있는 화면에서 메모리 부족 가능성
**해결방안:**
1. **지연 로딩**
```typescript
// 제어관리 설정을 필요할 때만 로드
const loadDataflowConfig = useCallback(async () => {
if (config.enableDataflowControl && !dataflowConfig) {
const config = await apiClient.get(`/button-dataflow/config/${buttonId}`);
setDataflowConfig(config.data);
}
}, [buttonId, config.enableDataflowControl]);
```
2. **설정 캐싱**
```typescript
// LRU 캐시로 자주 사용되는 설정만 메모리 보관
const configCache = new LRUCache({ max: 100, ttl: 300000 }); // 5분 TTL
```
### 3. **데이터베이스 성능 영향**
**문제점:**
- 버튼 클릭마다 복잡한 SQL 쿼리 실행
- EventTriggerService의 현재 구조상 전체 관계도 스캔
```typescript
// eventTriggerService.ts - 모든 관계도를 검색하는 비효율적인 쿼리
const diagrams = await prisma.$queryRaw`
SELECT * FROM dataflow_diagrams
WHERE company_code = ${companyCode}
AND (category::text = '"data-save"' OR ...)
`;
```
**해결방안:**
1. **인덱스 최적화**
```sql
-- 복합 인덱스 추가
CREATE INDEX idx_dataflow_button_lookup ON dataflow_diagrams
USING GIN ((control->'buttonId'))
WHERE category @> '["button-trigger"]';
```
2. **캐싱 계층 추가**
```typescript
// 버튼별 제어관리 매핑을 캐시
const buttonDataflowCache = new Map<string, DataflowConfig[]>();
```
## 🔧 확장성 관련 문제점
### 4. **설정 복잡도 증가**
**문제점:**
- 기존 단순한 버튼 설정에서 복잡한 제어관리 설정 추가
- 사용자 혼란 가능성
- UI가 너무 복잡해질 위험
**현재 UI 구조 문제:**
```typescript
// ButtonConfigPanel.tsx가 이미 복잡함
return (
<div className="space-y-4">
{/* 기존 15개+ 설정 항목 */}
{/* + 제어관리 설정 추가 시 더욱 복잡해짐 */}
</div>
);
```
**해결방안:**
1. **탭 구조로 분리**
```typescript
<Tabs defaultValue="basic">
<TabsList>
<TabsTrigger value="basic"> </TabsTrigger>
<TabsTrigger value="dataflow"></TabsTrigger>
<TabsTrigger value="advanced"> </TabsTrigger>
</TabsList>
<TabsContent value="basic">{/* 기존 설정 */}</TabsContent>
<TabsContent value="dataflow">{/* 제어관리 설정 */}</TabsContent>
</Tabs>
```
2. **단계별 설정 마법사**
```typescript
const DataflowConfigWizard = () => {
const [step, setStep] = useState(1);
// 1단계: 활성화 여부
// 2단계: 실행 타이밍
// 3단계: 제어 모드
// 4단계: 상세 설정
};
```
### 5. **타입 안전성 문제**
**문제점:**
- 기존 ButtonTypeConfig에 새로운 필드 추가로 인한 호환성 문제
- 런타임 오류 가능성
**현재 타입 구조 문제:**
```typescript
// 기존 코드들이 ButtonTypeConfig의 새 필드를 모름
const config = component.webTypeConfig; // enableDataflowControl 없을 수 있음
if (config.enableDataflowControl) { // undefined 체크 필요
```
**해결방안:**
1. **점진적 타입 확장**
```typescript
// 기존 타입은 유지하고 새로운 타입 정의
interface ExtendedButtonTypeConfig extends ButtonTypeConfig {
enableDataflowControl?: boolean;
dataflowConfig?: ButtonDataflowConfig;
dataflowTiming?: "before" | "after" | "replace";
}
// 타입 가드 함수
function hasDataflowConfig(
config: ButtonTypeConfig
): config is ExtendedButtonTypeConfig {
return "enableDataflowControl" in config;
}
```
2. **마이그레이션 함수**
```typescript
const migrateButtonConfig = (
config: ButtonTypeConfig
): ExtendedButtonTypeConfig => {
return {
...config,
enableDataflowControl: false, // 기본값
dataflowConfig: undefined,
dataflowTiming: "after",
};
};
```
### 6. **버전 호환성 문제**
**문제점:**
- 기존 저장된 버튼 설정과 새로운 구조 간 호환성
- 점진적 배포 시 일부 기능 불일치
**해결방안:**
1. **버전 필드 추가**
```typescript
interface ButtonTypeConfig {
version?: "1.0" | "2.0"; // 제어관리 추가 버전
// ...기존 필드들
}
```
2. **자동 마이그레이션**
```typescript
const migrateButtonConfig = (config: any) => {
if (!config.version || config.version === "1.0") {
return {
...config,
version: "2.0",
enableDataflowControl: false,
dataflowConfig: undefined,
};
}
return config;
};
```
## 🚫 보안 관련 문제점
### 7. **권한 검증 부재**
**문제점:**
- 제어관리 실행 시 추가적인 권한 검증 없음
- 사용자가 설정한 제어관리를 통해 의도치 않은 데이터 조작 가능
**해결방안:**
1. **제어관리 권한 체계**
```typescript
interface DataflowPermission {
canExecuteDataflow: boolean;
allowedTables: string[];
allowedActions: ("insert" | "update" | "delete")[];
}
const checkDataflowPermission = async (
userId: string,
dataflowConfig: ButtonDataflowConfig
): Promise<boolean> => {
// 사용자별 제어관리 권한 검증
};
```
2. **실행 로그 및 감사**
```typescript
const logDataflowExecution = async (
userId: string,
buttonId: string,
dataflowResult: ExecutionResult
) => {
await prisma.dataflow_audit_log.create({
data: {
user_id: userId,
button_id: buttonId,
executed_actions: dataflowResult.executedActions,
execution_time: dataflowResult.executionTime,
timestamp: new Date(),
},
});
};
```
### 8. **SQL 인젝션 위험**
**문제점:**
- 고급 모드에서 사용자가 직접 조건 설정 시 SQL 인젝션 가능성
- 동적 테이블명, 필드명 처리 시 보안 취약점
**해결방안:**
1. **화이트리스트 기반 검증**
```typescript
const ALLOWED_TABLES = ["user_info", "order_master" /* ... */];
const ALLOWED_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "LIKE"];
const validateDataflowConfig = (config: ButtonDataflowConfig) => {
if (config.directControl) {
if (!ALLOWED_TABLES.includes(config.directControl.sourceTable)) {
throw new Error("허용되지 않은 테이블입니다.");
}
// 추가 검증...
}
};
```
2. **파라미터화된 쿼리 강제**
```typescript
// 모든 동적 쿼리를 파라미터화
const executeCondition = async (condition: DataflowCondition, data: any) => {
const query = `SELECT * FROM ${tableName} WHERE ${fieldName} ${operator} $1`;
return await prisma.$queryRaw(query, condition.value);
};
```
## 💡 권장 해결 전략
### Phase 1: 안전한 시작 (MVP)
1. **간편 모드만 구현** (기존 관계도 선택)
2. **"after" 타이밍만 지원** (기존 액션 후 실행)
3. **기본적인 성능 최적화** (캐싱, 인덱스)
4. **상세한 로깅 및 모니터링** 추가
### Phase 2: 점진적 확장
1. **고급 모드 추가** (권한 검증 강화)
2. **"before", "replace" 타이밍 지원**
3. **성능 최적화 고도화** (비동기 실행, 큐잉)
4. **UI 개선** (탭, 마법사)
### Phase 3: 고도화
1. **배치 처리 지원**
2. **복잡한 비즈니스 로직 지원**
3. **AI 기반 설정 추천**
4. **성능 대시보드**
### 모니터링 지표
```typescript
interface DataflowMetrics {
averageExecutionTime: number;
errorRate: number;
memoryUsage: number;
cacheHitRate: number;
userSatisfactionScore: number;
}
```
이러한 문제점들을 사전에 고려하여 설계하면 안정적이고 확장 가능한 시스템을 구축할 수 있습니다.
@@ -0,0 +1,551 @@
# ⚡ 버튼 제어관리 성능 최적화 전략
## 🎯 성능 목표 설정
### 허용 가능한 응답 시간
- **즉시 반응**: 0-100ms (사용자가 지연을 느끼지 않음)
- **빠른 응답**: 100-300ms (약간의 지연이지만 허용 가능)
- **보통 응답**: 300-1000ms (Loading 스피너 필요)
- **❌ 느린 응답**: 1000ms+ (사용자 불만 발생)
### 현실적 목표
- **간단한 제어관리**: 200ms 이내
- **복잡한 제어관리**: 500ms 이내
- **매우 복잡한 로직**: 1초 이내 (비동기 처리)
## 🚀 핵심 최적화 전략
### 1. **즉시 응답 + 백그라운드 실행 패턴**
```typescript
const handleButtonClick = async (component: ComponentData) => {
const config = component.webTypeConfig;
// 🔥 즉시 UI 응답 (0ms)
setButtonState("executing");
toast.success("처리를 시작했습니다.");
try {
// Step 1: 기존 액션 우선 실행 (빠른 응답)
if (config?.actionType && config?.dataflowTiming !== "replace") {
await executeOriginalAction(config.actionType, component);
// 사용자에게 즉시 피드백
toast.success(`${getActionDisplayName(config.actionType)} 완료`);
}
// Step 2: 제어관리는 백그라운드에서 실행
if (config?.enableDataflowControl) {
// 🔥 비동기로 실행 (UI 블로킹 없음)
executeDataflowInBackground(config, component.id)
.then((result) => {
if (result.success) {
showDataflowResult(result);
}
})
.catch((error) => {
console.error("Background dataflow failed:", error);
// 조용히 실패 처리 (사용자 방해 최소화)
});
}
} finally {
setButtonState("idle");
}
};
// 백그라운드 실행 함수
const executeDataflowInBackground = async (
config: ButtonTypeConfig,
buttonId: string
): Promise<ExecutionResult> => {
// 성능 모니터링
const startTime = performance.now();
try {
const result = await apiClient.post("/api/button-dataflow/execute-async", {
buttonConfig: config,
buttonId: buttonId,
priority: "background", // 우선순위 낮게 설정
});
const executionTime = performance.now() - startTime;
console.log(`⚡ Dataflow 실행 시간: ${executionTime.toFixed(2)}ms`);
return result.data;
} catch (error) {
// 에러 로깅만 하고 사용자 방해하지 않음
console.error("Background dataflow error:", error);
throw error;
}
};
```
### 2. **스마트 캐싱 시스템**
```typescript
// 다층 캐싱 전략
class DataflowCache {
private memoryCache = new Map<string, any>(); // L1: 메모리 캐시
private persistCache: IDBDatabase | null = null; // L2: 브라우저 저장소
constructor() {
this.initPersistentCache();
}
// 버튼별 제어관리 설정 캐싱
async getButtonDataflowConfig(
buttonId: string
): Promise<ButtonDataflowConfig | null> {
const cacheKey = `button_dataflow_${buttonId}`;
// L1: 메모리에서 확인 (1ms)
if (this.memoryCache.has(cacheKey)) {
console.log("⚡ Memory cache hit:", buttonId);
return this.memoryCache.get(cacheKey);
}
// L2: 브라우저 저장소에서 확인 (5-10ms)
const cached = await this.getFromPersistentCache(cacheKey);
if (cached && !this.isExpired(cached)) {
console.log("💾 Persistent cache hit:", buttonId);
this.memoryCache.set(cacheKey, cached.data);
return cached.data;
}
// L3: 서버에서 로드 (100-300ms)
console.log("🌐 Loading from server:", buttonId);
const serverData = await this.loadFromServer(buttonId);
// 캐시에 저장
this.memoryCache.set(cacheKey, serverData);
await this.saveToPersistentCache(cacheKey, serverData);
return serverData;
}
// 관계도별 실행 계획 캐싱
async getCachedExecutionPlan(
diagramId: number
): Promise<ExecutionPlan | null> {
// 자주 사용되는 실행 계획을 캐시
const cacheKey = `execution_plan_${diagramId}`;
return this.getFromCache(cacheKey, async () => {
return await this.loadExecutionPlan(diagramId);
});
}
}
// 사용 예시
const dataflowCache = new DataflowCache();
const optimizedButtonClick = async (buttonId: string) => {
// 🔥 캐시에서 즉시 로드 (1-10ms)
const config = await dataflowCache.getButtonDataflowConfig(buttonId);
if (config) {
// 설정이 캐시되어 있으면 즉시 실행
await executeDataflow(config);
}
};
```
### 3. **데이터베이스 최적화**
```sql
-- 🔥 버튼별 제어관리 조회 최적화 인덱스
CREATE INDEX CONCURRENTLY idx_dataflow_button_fast_lookup
ON dataflow_diagrams
USING GIN ((control->'buttonId'))
WHERE category @> '["button-trigger"]'
AND company_code IS NOT NULL;
-- 🔥 실행 조건 빠른 검색 인덱스
CREATE INDEX CONCURRENTLY idx_dataflow_trigger_type
ON dataflow_diagrams (company_code, ((control->0->>'triggerType')))
WHERE control IS NOT NULL;
-- 🔥 자주 사용되는 관계도 우선 조회
CREATE INDEX CONCURRENTLY idx_dataflow_usage_priority
ON dataflow_diagrams (company_code, updated_at DESC)
WHERE category @> '["button-trigger"]';
```
```typescript
// 최적화된 데이터베이스 조회
export class OptimizedEventTriggerService {
// 🔥 버튼별 제어관리 직접 조회 (전체 스캔 제거)
static async getButtonDataflowConfigs(
buttonId: string,
companyCode: string
): Promise<DataflowConfig[]> {
// 기존: 모든 관계도 스캔 (느림)
// const allDiagrams = await prisma.$queryRaw`SELECT * FROM dataflow_diagrams WHERE...`
// 🔥 새로운: 버튼별 직접 조회 (빠름)
const configs = await prisma.$queryRaw`
SELECT
diagram_id,
control,
plan,
category
FROM dataflow_diagrams
WHERE company_code = ${companyCode}
AND control @> '[{"buttonId": ${buttonId}}]'
AND category @> '["button-trigger"]'
ORDER BY updated_at DESC
LIMIT 5; -- 최대 5개만 조회
`;
return configs as DataflowConfig[];
}
// 🔥 조건 검증 최적화 (메모리 내 처리)
static evaluateConditionsOptimized(
conditions: DataflowCondition[],
data: Record<string, any>
): boolean {
// 간단한 조건은 메모리에서 즉시 처리 (1-5ms)
for (const condition of conditions) {
if (condition.type === "condition") {
const fieldValue = data[condition.field!];
const result = this.evaluateSimpleCondition(
fieldValue,
condition.operator!,
condition.value
);
if (!result) return false;
}
}
return true;
}
private static evaluateSimpleCondition(
fieldValue: any,
operator: string,
conditionValue: any
): boolean {
switch (operator) {
case "=":
return fieldValue === conditionValue;
case "!=":
return fieldValue !== conditionValue;
case ">":
return fieldValue > conditionValue;
case "<":
return fieldValue < conditionValue;
case ">=":
return fieldValue >= conditionValue;
case "<=":
return fieldValue <= conditionValue;
case "LIKE":
return String(fieldValue)
.toLowerCase()
.includes(String(conditionValue).toLowerCase());
default:
return true;
}
}
}
```
### 4. **배치 처리 및 큐 시스템**
```typescript
// 🔥 제어관리 작업 큐 시스템
class DataflowQueue {
private queue: Array<{
id: string;
buttonId: string;
config: ButtonDataflowConfig;
priority: "high" | "normal" | "low";
timestamp: number;
}> = [];
private processing = false;
// 작업 추가 (즉시 반환)
enqueue(
buttonId: string,
config: ButtonDataflowConfig,
priority: "high" | "normal" | "low" = "normal"
): string {
const jobId = `job_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
this.queue.push({
id: jobId,
buttonId,
config,
priority,
timestamp: Date.now(),
});
// 우선순위별 정렬
this.queue.sort((a, b) => {
const priorityWeight = { high: 3, normal: 2, low: 1 };
return priorityWeight[b.priority] - priorityWeight[a.priority];
});
// 비동기 처리 시작
this.processQueue();
return jobId; // 작업 ID 즉시 반환
}
// 배치 처리
private async processQueue(): Promise<void> {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
try {
// 동시에 최대 3개 작업 처리
const batch = this.queue.splice(0, 3);
const promises = batch.map((job) =>
this.executeDataflowJob(job).catch((error) => {
console.error(`Job ${job.id} failed:`, error);
return { success: false, error };
})
);
await Promise.all(promises);
} finally {
this.processing = false;
// 큐에 더 많은 작업이 있으면 계속 처리
if (this.queue.length > 0) {
setTimeout(() => this.processQueue(), 10);
}
}
}
private async executeDataflowJob(job: any): Promise<any> {
const startTime = performance.now();
try {
const result = await OptimizedEventTriggerService.executeButtonDataflow(
job.buttonId,
job.config
);
const executionTime = performance.now() - startTime;
console.log(
`⚡ Job ${job.id} completed in ${executionTime.toFixed(2)}ms`
);
return result;
} catch (error) {
console.error(`❌ Job ${job.id} failed:`, error);
throw error;
}
}
}
// 전역 큐 인스턴스
const dataflowQueue = new DataflowQueue();
// 사용 예시: 즉시 응답하는 버튼 클릭
const optimizedButtonClick = async (
buttonId: string,
config: ButtonDataflowConfig
) => {
// 🔥 즉시 작업 큐에 추가하고 반환 (1-5ms)
const jobId = dataflowQueue.enqueue(buttonId, config, "normal");
// 사용자에게 즉시 피드백
toast.success("작업이 시작되었습니다.");
return jobId;
};
```
### 5. **프론트엔드 최적화**
```typescript
// 🔥 React 성능 최적화
const OptimizedButtonComponent = React.memo(
({ component }: { component: ComponentData }) => {
const [isExecuting, setIsExecuting] = useState(false);
const [executionTime, setExecutionTime] = useState<number | null>(null);
// 디바운싱으로 중복 클릭 방지
const handleClick = useDebouncedCallback(async () => {
if (isExecuting) return;
setIsExecuting(true);
const startTime = performance.now();
try {
await optimizedButtonClick(component.id, component.webTypeConfig);
} finally {
const endTime = performance.now();
setExecutionTime(endTime - startTime);
setIsExecuting(false);
}
}, 300); // 300ms 디바운싱
return (
<Button
onClick={handleClick}
disabled={isExecuting}
className={`
transition-all duration-200
${isExecuting ? "opacity-75 cursor-wait" : ""}
`}
>
{isExecuting ? (
<div className="flex items-center space-x-2">
<Spinner size="sm" />
<span>...</span>
</div>
) : (
component.label || "버튼"
)}
{/* 개발 모드에서 성능 정보 표시 */}
{process.env.NODE_ENV === "development" && executionTime && (
<span className="ml-2 text-xs opacity-60">
{executionTime.toFixed(0)}ms
</span>
)}
</Button>
);
}
);
// 리스트 가상화로 대량 버튼 렌더링 최적화
const VirtualizedButtonList = ({ buttons }: { buttons: ComponentData[] }) => {
return (
<FixedSizeList
height={600}
itemCount={buttons.length}
itemSize={50}
itemData={buttons}
>
{({ index, style, data }) => (
<div style={style}>
<OptimizedButtonComponent component={data[index]} />
</div>
)}
</FixedSizeList>
);
};
```
## 📊 성능 모니터링
```typescript
// 실시간 성능 모니터링
class PerformanceMonitor {
private metrics: {
buttonClicks: number;
averageResponseTime: number;
slowQueries: Array<{ query: string; time: number; timestamp: Date }>;
cacheHitRate: number;
} = {
buttonClicks: 0,
averageResponseTime: 0,
slowQueries: [],
cacheHitRate: 0,
};
recordButtonClick(executionTime: number) {
this.metrics.buttonClicks++;
// 이동 평균으로 응답 시간 계산
this.metrics.averageResponseTime =
this.metrics.averageResponseTime * 0.9 + executionTime * 0.1;
// 느린 쿼리 기록 (500ms 이상)
if (executionTime > 500) {
this.metrics.slowQueries.push({
query: "button_dataflow_execution",
time: executionTime,
timestamp: new Date(),
});
// 최대 100개만 보관
if (this.metrics.slowQueries.length > 100) {
this.metrics.slowQueries.shift();
}
}
// 성능 경고
if (executionTime > 1000) {
console.warn(`🐌 Slow button execution: ${executionTime}ms`);
}
}
getPerformanceReport() {
return {
...this.metrics,
recommendation: this.getRecommendation(),
};
}
private getRecommendation(): string[] {
const recommendations: string[] = [];
if (this.metrics.averageResponseTime > 300) {
recommendations.push(
"평균 응답 시간이 느립니다. 캐싱 설정을 확인하세요."
);
}
if (this.metrics.cacheHitRate < 80) {
recommendations.push("캐시 히트율이 낮습니다. 캐시 전략을 재검토하세요.");
}
if (this.metrics.slowQueries.length > 10) {
recommendations.push("느린 쿼리가 많습니다. 인덱스를 확인하세요.");
}
return recommendations;
}
}
// 전역 모니터
const performanceMonitor = new PerformanceMonitor();
// 사용 예시
const monitoredButtonClick = async (buttonId: string) => {
const startTime = performance.now();
try {
await executeButtonAction(buttonId);
} finally {
const executionTime = performance.now() - startTime;
performanceMonitor.recordButtonClick(executionTime);
}
};
```
## 🎯 성능 최적화 로드맵
### Phase 1: 즉시 개선 (1-2주)
1.**즉시 응답 패턴** 도입
2.**기본 캐싱** 구현
3.**데이터베이스 인덱스** 추가
4.**성능 모니터링** 설정
### Phase 2: 고급 최적화 (3-4주)
1. 🔄 **작업 큐 시스템** 구현
2. 🔄 **배치 처리** 도입
3. 🔄 **다층 캐싱** 완성
4. 🔄 **가상화 렌더링** 적용
### Phase 3: 고도화 (5-6주)
1.**프리로딩** 시스템
2.**CDN 캐싱** 도입
3.**서버 사이드 캐싱**
4.**성능 대시보드**
이렇게 단계적으로 최적화하면 사용자가 체감할 수 있는 성능 개선을 점진적으로 달성할 수 있습니다!
@@ -0,0 +1,375 @@
# 선택 항목 상세입력 완전 자동화 가이드 🚀
## ✨ 완전 자동화 완료!
이제 **아무 설정도 하지 않아도** 데이터가 자동으로 전달됩니다!
---
## 🎯 자동화된 흐름
### 1단계: TableList (선택 화면)
```
┌─────────────────────────────────┐
│ TableList 컴포넌트 │
│ - 테이블: item_info │
│ - 체크박스로 품목 3개 선택 │
└─────────────────────────────────┘
↓ (자동 저장)
┌─────────────────────────────────┐
│ modalDataStore │
│ { "item_info": [선택된 데이터] }│
└─────────────────────────────────┘
```
### 2단계: Button (다음 버튼)
```
┌─────────────────────────────────┐
│ Button 컴포넌트 │
│ - 액션: "데이터 전달 + 모달열기"│
│ - dataSourceId: (비워둠) │ ← 자동 감지!
└─────────────────────────────────┘
↓ (자동 감지)
┌─────────────────────────────────┐
│ 같은 화면에서 TableList 찾기 │
│ → tableName = "item_info" │
└─────────────────────────────────┘
↓ (URL 전달)
┌─────────────────────────────────┐
│ 모달 열기 │
│ URL: ?dataSourceId=item_info │
└─────────────────────────────────┘
```
### 3단계: SelectedItemsDetailInput (상세 입력 화면)
```
┌─────────────────────────────────┐
│ SelectedItemsDetailInput │
│ - dataSourceId: (비워둠) │ ← URL에서 자동 읽기!
└─────────────────────────────────┘
↓ (자동 읽기)
┌─────────────────────────────────┐
│ URL: ?dataSourceId=item_info │
│ → modalDataStore에서 데이터 로드│
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 화면에 데이터 표시 │
│ 선택된 3개 품목 + 입력 필드 │
└─────────────────────────────────┘
```
---
## 🛠️ 설정 방법 (완전 자동)
### 1단계: 선택 화면 구성
#### TableList 컴포넌트
```yaml
테이블: item_info
옵션:
- 체크박스 표시:
- 다중 선택:
```
**설정 끝!** 선택된 데이터는 자동으로 `modalDataStore`에 저장됩니다.
#### Button 컴포넌트
```yaml
버튼 텍스트: "다음"
버튼 액션: "데이터 전달 + 모달 열기" 🆕
설정:
- 데이터 소스 ID: (비워둠) ← ✨ 자동 감지!
- 모달 제목: "상세 정보 입력"
- 모달 크기: Large
- 대상 화면: (상세 입력 화면 선택)
```
**중요**: `데이터 소스 ID`**비워두세요**! 자동으로 같은 화면의 TableList를 찾아서 테이블명을 사용합니다.
---
### 2단계: 상세 입력 화면 구성
#### SelectedItemsDetailInput 컴포넌트 추가
```yaml
컴포넌트: "선택 항목 상세입력"
```
**설정 끝!** URL 파라미터에서 자동으로 `dataSourceId`를 읽어옵니다.
#### 상세 설정 (선택사항)
```yaml
데이터 소스 ID: (비워둠) ← ✨ URL에서 자동!
표시할 컬럼:
- 품목코드 (item_code)
- 품목명 (item_name)
- 규격 (specification)
추가 입력 필드:
- 수량 (quantity): 숫자
- 단가 (unit_price): 숫자
- 납기일 (delivery_date): 날짜
- 비고 (remarks): 텍스트영역
옵션:
- 레이아웃: 테이블 형식 (Grid)
- 항목 번호 표시:
- 항목 제거 허용:
```
---
## 📊 실제 동작 시나리오
### 시나리오: 수주 등록
#### 1단계: 품목 선택
```
사용자가 품목 테이블에서 3개 선택:
✓ [PD-001] 케이블 100m
✓ [PD-002] 커넥터 50개
✓ [PD-003] 단자대 20개
```
#### 2단계: "다음" 버튼 클릭
```javascript
// 자동으로 일어나는 일:
1. 같은 화면에서 table-list 컴포넌트 찾기
componentType === "table-list"
tableName === "item_info"
2. modalDataStore에서 데이터 확인
modalData = [
{ id: "PD-001", originalData: {...} },
{ id: "PD-002", originalData: {...} },
{ id: "PD-003", originalData: {...} }
]
3. 모달 열기 + URL 파라미터 전달
URL: /screen/detail-input?dataSourceId=item_info
```
#### 3단계: 상세 정보 입력
```
자동으로 표시됨:
┌───────────────────────────────────────────────────────┐
│ 상세 정보 입력 │
├───────────────────────────────────────────────────────┤
│ # │ 품목코드 │ 품목명 │ 수량 │ 단가 │ 납기일 │
├───┼──────────┼────────────┼──────┼────────┼─────────┤
│ 1 │ PD-001 │ 케이블100m │ [ ] │ [ ] │ [ ] │
│ 2 │ PD-002 │ 커넥터50개 │ [ ] │ [ ] │ [ ] │
│ 3 │ PD-003 │ 단자대20개 │ [ ] │ [ ] │ [ ] │
└───────────────────────────────────────────────────────┘
사용자가 수량, 단가, 납기일만 입력하면 끝!
```
---
## 🎨 UI 미리보기
### Button 설정 화면
```
버튼 액션
├─ 데이터 전달 + 모달 열기 🆕
└─ 데이터 전달 + 모달 설정
├─ 데이터 소스 ID (선택사항)
│ [ ]
│ ✨ 비워두면 같은 화면의 TableList를 자동으로 감지합니다
│ 직접 지정하려면 테이블명을 입력하세요 (예: item_info)
├─ 모달 제목
│ [상세 정보 입력 ]
├─ 모달 크기
│ [큰 (Large) - 권장 ▼]
└─ 대상 화면 선택
[화면을 선택하세요... ▼]
```
### SelectedItemsDetailInput 설정 화면
```
데이터 소스 ID (자동 설정됨)
[ ]
✨ URL 파라미터에서 자동으로 가져옵니다 (Button이 전달)
테스트용으로 직접 입력하려면 테이블명을 입력하세요
```
---
## 🔍 자동화 원리
### 1. TableList → modalDataStore
```typescript
// TableListComponent.tsx
const handleRowSelection = (rowKey: string, checked: boolean) => {
// ... 선택 처리 ...
// 🆕 자동으로 스토어에 저장
const { useModalDataStore } = await import("@/stores/modalDataStore");
const dataSourceId = tableName; // item_info
const modalDataItems = selectedRowsData.map(row => ({
id: row.item_code,
originalData: row,
additionalData: {}
}));
useModalDataStore.getState().setData(dataSourceId, modalDataItems);
};
```
### 2. Button → TableList 자동 감지
```typescript
// buttonActions.ts - handleOpenModalWithData()
let dataSourceId = config.dataSourceId;
// 🆕 비워있으면 자동 감지
if (!dataSourceId && context.allComponents) {
const tableListComponent = context.allComponents.find(
(comp) => comp.componentType === "table-list" && comp.componentConfig?.tableName
);
if (tableListComponent) {
dataSourceId = tableListComponent.componentConfig.tableName;
console.log("✨ TableList 자동 감지:", dataSourceId);
}
}
// 🆕 URL 파라미터로 전달
const modalEvent = new CustomEvent("openScreenModal", {
detail: {
screenId: config.targetScreenId,
urlParams: { dataSourceId } // ← URL에 추가
}
});
```
### 3. SelectedItemsDetailInput → URL 읽기
```typescript
// SelectedItemsDetailInputComponent.tsx
import { useSearchParams } from "next/navigation";
const searchParams = useSearchParams();
const urlDataSourceId = searchParams?.get("dataSourceId");
// 🆕 우선순위: URL > 설정 > component.id
const dataSourceId = useMemo(
() => urlDataSourceId || componentConfig.dataSourceId || component.id,
[urlDataSourceId, componentConfig.dataSourceId, component.id]
);
// 🆕 스토어에서 데이터 로드
const dataRegistry = useModalDataStore((state) => state.dataRegistry);
const modalData = dataRegistry[dataSourceId] || [];
```
---
## 🧪 테스트 시나리오
### 기본 테스트
1. **화면 편집기 열기**
2. **첫 번째 화면 (선택) 만들기**:
- TableList 추가 (item_info)
- Button 추가 (버튼 액션: "데이터 전달 + 모달 열기")
- **dataSourceId는 비워둠!**
3. **두 번째 화면 (상세 입력) 만들기**:
- SelectedItemsDetailInput 추가
- **dataSourceId는 비워둠!**
- 표시 컬럼 설정
- 추가 입력 필드 설정
4. **할당된 화면에서 테스트**:
- 품목 3개 선택
- "다음" 버튼 클릭
- 상세 입력 화면에서 데이터 확인 ✅
### 고급 테스트 (직접 지정)
```yaml
시나리오: 여러 TableList가 있는 화면
화면 구성:
- TableList (item_info) ← 품목
- TableList (customer_info) ← 고객
- Button (품목 상세입력) ← dataSourceId: "item_info"
- Button (고객 상세입력) ← dataSourceId: "customer_info"
```
---
## 🚨 주의사항
### ❌ 잘못된 사용법
```yaml
# 1. 같은 화면에 TableList가 여러 개 있는데 비워둠
TableList 1: item_info
TableList 2: customer_info
Button: dataSourceId = (비워둠) ← 어느 것을 선택해야 할까?
해결: dataSourceId를 명시적으로 지정
```
### ✅ 올바른 사용법
```yaml
# 1. TableList가 1개인 경우
TableList: item_info
Button: dataSourceId = (비워둠) ← 자동 감지 OK!
# 2. TableList가 여러 개인 경우
TableList 1: item_info
TableList 2: customer_info
Button 1: dataSourceId = "item_info" ← 명시
Button 2: dataSourceId = "customer_info" ← 명시
```
---
## 🎯 완료 체크리스트
### 구현 완료 ✅
- [x] TableList → modalDataStore 자동 저장
- [x] Button → TableList 자동 감지
- [x] Button → URL 파라미터 전달
- [x] SelectedItemsDetailInput → URL 자동 읽기
- [x] 설정 패널 UI에 "자동" 힌트 추가
### 사용자 경험 ✅
- [x] dataSourceId 입력 불필요 (자동)
- [x] 일관된 데이터 흐름 (자동)
- [x] 오류 메시지 명확 (자동)
- [x] 직관적인 UI (자동 힌트)
---
## 📝 요약
**이제 사용자는 단 3단계만 하면 됩니다:**
1. **TableList 추가** → 테이블 선택
2. **Button 추가** → 액션 "데이터 전달 + 모달 열기" 선택
3. **SelectedItemsDetailInput 추가** → 필드 설정
**dataSourceId는 자동으로 처리됩니다!**
---
## 🔗 관련 파일
- `frontend/stores/modalDataStore.ts` - 데이터 저장소
- `frontend/lib/utils/buttonActions.ts` - 버튼 액션 (자동 감지)
- `frontend/lib/registry/components/table-list/TableListComponent.tsx` - 자동 저장
- `frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx` - URL 자동 읽기
- `frontend/components/screen/config-panels/ButtonConfigPanel.tsx` - 버튼 설정 UI
- `frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx` - 상세 입력 설정 UI
---
**🎉 완전 자동화 완료!**
@@ -0,0 +1,413 @@
# 선택 항목 상세입력 컴포넌트 - 완성 가이드
## 📦 구현 완료 사항
### ✅ 1. Zustand 스토어 생성 (modalDataStore)
- 파일: `frontend/stores/modalDataStore.ts`
- 기능:
- 모달 간 데이터 전달 관리
- `setData()`: 데이터 저장
- `getData()`: 데이터 조회
- `clearData()`: 데이터 정리
- `updateItemData()`: 항목별 추가 데이터 업데이트
### ✅ 2. SelectedItemsDetailInput 컴포넌트 생성
- 디렉토리: `frontend/lib/registry/components/selected-items-detail-input/`
- 파일들:
- `types.ts`: 타입 정의
- `SelectedItemsDetailInputComponent.tsx`: 메인 컴포넌트
- `SelectedItemsDetailInputConfigPanel.tsx`: 설정 패널
- `SelectedItemsDetailInputRenderer.tsx`: 렌더러
- `index.ts`: 컴포넌트 정의
- `README.md`: 사용 가이드
### ✅ 3. 컴포넌트 기능
- 전달받은 원본 데이터 표시 (읽기 전용)
- 각 항목별 추가 입력 필드 제공
- Grid/Table 레이아웃 및 Card 레이아웃 지원
- 6가지 입력 타입 지원 (text, number, date, select, checkbox, textarea)
- 필수 입력 검증
- 항목 삭제 기능
### ✅ 4. 설정 패널 기능
- 데이터 소스 ID 설정
- 저장 대상 테이블 선택 (검색 가능한 Combobox)
- 표시할 원본 데이터 컬럼 선택
- 추가 입력 필드 정의 (필드명, 라벨, 타입, 필수 여부 등)
- 레이아웃 모드 선택 (Grid/Card)
- 옵션 설정 (번호 표시, 삭제 허용, 비활성화)
---
## 🚧 남은 작업 (구현 필요)
### 1. TableList에서 선택된 행 데이터를 스토어에 저장
**필요한 수정 파일:**
- `frontend/lib/registry/components/table-list/TableListComponent.tsx`
**구현 방법:**
```typescript
import { useModalDataStore } from "@/stores/modalDataStore";
// TableList 컴포넌트 내부
const setModalData = useModalDataStore((state) => state.setData);
// 선택된 행이 변경될 때마다 스토어에 저장
useEffect(() => {
if (selectedRows.length > 0) {
const modalDataItems = selectedRows.map((row) => ({
id: row[primaryKeyColumn] || row.id,
originalData: row,
additionalData: {},
}));
// 컴포넌트 ID를 키로 사용하여 저장
setModalData(component.id || "default", modalDataItems);
console.log("📦 [TableList] 선택된 데이터 저장:", modalDataItems);
}
}, [selectedRows, component.id, setModalData]);
```
**참고:**
- `selectedRows`: TableList의 체크박스로 선택된 행들
- `component.id`: 컴포넌트 고유 ID
- 이 ID가 SelectedItemsDetailInput의 `dataSourceId`와 일치해야 함
---
### 2. ButtonPrimary에 'openModalWithData' 액션 타입 추가
**필요한 수정 파일:**
- `frontend/lib/registry/components/button-primary/types.ts`
- `frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx`
- `frontend/lib/registry/components/button-primary/ButtonPrimaryConfigPanel.tsx`
#### A. types.ts 수정
```typescript
export interface ButtonPrimaryConfig extends ComponentConfig {
action?: {
type:
| "save"
| "delete"
| "popup"
| "navigate"
| "custom"
| "openModalWithData"; // 🆕 새 액션 타입
// 기존 필드들...
// 🆕 모달 데이터 전달용 필드
targetScreenId?: number; // 열릴 모달 화면 ID
dataSourceId?: string; // 데이터를 전달할 컴포넌트 ID
};
}
```
#### B. ButtonPrimaryComponent.tsx 수정
```typescript
import { useModalDataStore } from "@/stores/modalDataStore";
// 컴포넌트 내부
const modalData = useModalDataStore((state) => state.getData);
// handleClick 함수 수정
const handleClick = async () => {
// ... 기존 코드 ...
// openModalWithData 액션 처리
if (processedConfig.action?.type === "openModalWithData") {
const { targetScreenId, dataSourceId } = processedConfig.action;
if (!targetScreenId) {
toast.error("대상 화면이 설정되지 않았습니다.");
return;
}
if (!dataSourceId) {
toast.error("데이터 소스가 설정되지 않았습니다.");
return;
}
// 데이터 확인
const data = modalData(dataSourceId);
if (!data || data.length === 0) {
toast.warning("전달할 데이터가 없습니다. 먼저 항목을 선택해주세요.");
return;
}
console.log("📦 [ButtonPrimary] 데이터와 함께 모달 열기:", {
targetScreenId,
dataSourceId,
dataCount: data.length,
});
// 모달 열기 (기존 popup 액션과 동일)
toast.success(`${data.length}개 항목을 전달합니다.`);
// TODO: 실제 모달 열기 로직 (popup 액션 참고)
window.open(`/screens/${targetScreenId}`, "_blank");
return;
}
// ... 기존 액션 처리 코드 ...
};
```
#### C. ButtonPrimaryConfigPanel.tsx 수정
설정 패널에 openModalWithData 액션 설정 UI 추가:
```typescript
{config.action?.type === "openModalWithData" && (
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
<h4 className="text-sm font-medium"> </h4>
{/* 대상 화면 선택 */}
<div>
<Label htmlFor="target-screen"> </Label>
<Popover open={screenOpen} onOpenChange={setScreenOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="w-full justify-between">
{config.action?.targetScreenId
? screens.find((s) => s.id === config.action?.targetScreenId)?.name || "화면 선택"
: "화면 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent>
{/* 화면 목록 표시 */}
</PopoverContent>
</Popover>
</div>
{/* 데이터 소스 ID 입력 */}
<div>
<Label htmlFor="data-source-id"> ID</Label>
<Input
id="data-source-id"
value={config.action?.dataSourceId || ""}
onChange={(e) =>
updateActionConfig("dataSourceId", e.target.value)
}
placeholder="table-list-123"
/>
<p className="text-xs text-gray-500 mt-1">
💡 ID (: TableList의 ID)
</p>
</div>
</div>
)}
```
---
### 3. 저장 기능 구현
**방법 1: 기존 save 액션 활용**
SelectedItemsDetailInput의 데이터는 자동으로 `formData`에 포함되므로, 기존 save 액션을 그대로 사용할 수 있습니다:
```typescript
// formData 구조
{
"selected-items-component-id": [
{
id: "SALE-003",
originalData: { item_code: "SALE-003", ... },
additionalData: { customer_item_code: "ABC-001", unit_price: 50, ... }
},
// ... 더 많은 항목들
]
}
```
백엔드에서 이 데이터를 받아서 각 항목을 개별 INSERT하면 됩니다.
**방법 2: 전용 save 로직 추가**
더 나은 UX를 위해 전용 저장 로직을 추가할 수 있습니다:
```typescript
// ButtonPrimary의 save 액션에서
if (config.action?.type === "save") {
// formData에서 SelectedItemsDetailInput 데이터 찾기
const selectedItemsKey = Object.keys(formData).find(
(key) => Array.isArray(formData[key]) && formData[key][0]?.originalData
);
if (selectedItemsKey) {
const items = formData[selectedItemsKey] as ModalDataItem[];
// 저장할 데이터 변환
const dataToSave = items.map((item) => ({
...item.originalData,
...item.additionalData,
}));
// 백엔드 API 호출
const response = await apiClient.post(`/api/table-data/${targetTable}`, {
data: dataToSave,
batchInsert: true,
});
if (response.data.success) {
toast.success(`${dataToSave.length}개 항목이 저장되었습니다.`);
onClose?.();
}
}
}
```
---
## 🎯 통합 테스트 시나리오
### 시나리오: 수주 등록 - 품목 상세 입력
#### 1단계: 화면 구성
**[모달 1] 품목 선택 화면 (screen_id: 100)**
- TableList 컴포넌트
- ID: `item-selection-table`
- multiSelect: `true`
- selectedTable: `item_info`
- columns: 품목코드, 품목명, 규격, 단위, 단가
- ButtonPrimary 컴포넌트
- text: "다음 (상세정보 입력)"
- action.type: `openModalWithData`
- action.targetScreenId: `101` (두 번째 모달)
- action.dataSourceId: `item-selection-table`
**[모달 2] 상세 입력 화면 (screen_id: 101)**
- SelectedItemsDetailInput 컴포넌트
- ID: `selected-items-detail`
- dataSourceId: `item-selection-table`
- displayColumns: `["item_code", "item_name", "spec", "unit"]`
- additionalFields:
```json
[
{ "name": "customer_item_code", "label": "거래처 품번", "type": "text" },
{ "name": "customer_item_name", "label": "거래처 품명", "type": "text" },
{ "name": "year", "label": "연도", "type": "select", "options": [...] },
{ "name": "currency", "label": "통화", "type": "select", "options": [...] },
{ "name": "unit_price", "label": "단가", "type": "number", "required": true },
{ "name": "quantity", "label": "수량", "type": "number", "required": true }
]
```
- targetTable: `sales_detail`
- layout: `grid`
- ButtonPrimary 컴포넌트 (저장)
- text: "저장"
- action.type: `save`
- action.targetTable: `sales_detail`
#### 2단계: 테스트 절차
1. [모달 1] 품목 선택 화면 열기
2. TableList에서 3개 품목 체크박스 선택
3. "다음" 버튼 클릭
- ✅ modalDataStore에 3개 항목 저장 확인 (콘솔 로그)
- ✅ 모달 2가 열림
4. [모달 2] SelectedItemsDetailInput에 3개 항목 자동 표시 확인
- ✅ 원본 데이터 (품목코드, 품목명, 규격, 단위) 표시
- ✅ 추가 입력 필드 (거래처 품번, 단가, 수량 등) 빈 상태
5. 각 항목별로 추가 정보 입력
- 거래처 품번: "ABC-001", "ABC-002", "ABC-003"
- 단가: 50, 200, 3000
- 수량: 100, 50, 200
6. "저장" 버튼 클릭
- ✅ formData에 전체 데이터 포함 확인
- ✅ 백엔드 API 호출
- ✅ 저장 성공 토스트 메시지
- ✅ 모달 닫힘
#### 3단계: 데이터 검증
데이터베이스에 다음과 같이 저장되어야 합니다:
```sql
SELECT * FROM sales_detail;
-- 결과:
-- item_code | item_name | spec | unit | customer_item_code | unit_price | quantity
-- SALE-003 | 와셔 M8 | M8 | EA | ABC-001 | 50 | 100
-- SALE-005 | 육각 볼트 | M10 | EA | ABC-002 | 200 | 50
-- SIL-003 | 실리콘 | 325 | kg | ABC-003 | 3000 | 200
```
---
## 📚 추가 참고 자료
### 관련 파일 위치
- 스토어: `frontend/stores/modalDataStore.ts`
- 컴포넌트: `frontend/lib/registry/components/selected-items-detail-input/`
- TableList: `frontend/lib/registry/components/table-list/`
- ButtonPrimary: `frontend/lib/registry/components/button-primary/`
### 디버깅 팁
콘솔에서 다음 명령어로 상태 확인:
```javascript
// 모달 데이터 확인
__MODAL_DATA_STORE__.getState().dataRegistry
// 컴포넌트 등록 확인
__COMPONENT_REGISTRY__.get("selected-items-detail-input")
// TableList 선택 상태 확인
// (TableList 컴포넌트 내부에 로그 추가 필요)
```
### 예상 문제 및 해결
1. **데이터가 전달되지 않음**
- dataSourceId가 정확히 일치하는지 확인
- modalDataStore에 데이터가 저장되었는지 콘솔 로그 확인
2. **컴포넌트가 표시되지 않음**
- `frontend/lib/registry/components/index.ts`에 import 추가되었는지 확인
- 브라우저 새로고침 후 재시도
3. **저장이 안 됨**
- formData에 데이터가 포함되어 있는지 확인
- 백엔드 API 응답 확인
- targetTable이 올바른지 확인
---
## ✅ 완료 체크리스트
- [x] Zustand 스토어 생성 (modalDataStore)
- [x] SelectedItemsDetailInput 컴포넌트 생성
- [x] 컴포넌트 렌더링 로직 구현
- [x] 설정 패널 구현
- [ ] TableList에서 선택된 데이터를 스토어에 저장
- [ ] ButtonPrimary에 openModalWithData 액션 추가
- [ ] 저장 기능 구현
- [ ] 통합 테스트
- [ ] 사용자 매뉴얼 작성
---
## 🚀 다음 단계
1. TableList 컴포넌트에 modalDataStore 연동 추가
2. ButtonPrimary에 openModalWithData 액션 구현
3. 수주 등록 화면에서 실제 테스트
4. 문제 발견 시 디버깅 및 수정
5. 문서 업데이트 및 배포
**예상 소요 시간**: 2~3시간
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,203 @@
# 속성 패널 스크롤 문제 해결 가이드
## 적용된 수정사항
### 1. PropertiesPanel.tsx
```tsx
// 최상위 컨테이너
<div className="flex h-full w-full flex-col">
// 헤더 (고정 높이)
<div className="flex h-16 shrink-0 items-center justify-between border-b bg-white p-4">
// 스크롤 영역 (중요!)
<div
className="flex-1 overflow-y-scroll bg-gray-50"
style={{
maxHeight: 'calc(100vh - 64px)',
overflowY: 'scroll',
WebkitOverflowScrolling: 'touch'
}}
>
```
### 2. FlowEditor.tsx
```tsx
// 속성 패널 컨테이너 단순화
<div className="h-full w-[350px] border-l bg-white">
<PropertiesPanel />
</div>
```
### 3. TableSourceProperties.tsx / ExternalDBSourceProperties.tsx
```tsx
// ScrollArea 제거, 일반 div 사용
<div className="min-h-full space-y-4 p-4">
{/* 컨텐츠 */}
</div>
```
## 테스트 방법
1. **브라우저 강제 새로고침**
- Windows: `Ctrl + Shift + R` 또는 `Ctrl + F5`
- Mac: `Cmd + Shift + R`
2. **노드 플로우 편집기 열기**
- 관리자 메뉴 > 플로우 관리
3. **테스트 노드 추가**
- 테이블 소스 노드를 캔버스에 드래그
4. **속성 패널 확인**
- 노드 클릭
- 우측에 속성 패널 열림
- **회색 배경 확인** (스크롤 영역)
5. **스크롤 테스트**
- 마우스 휠로 스크롤
- 또는 스크롤바 드래그
- **빨간 박스** → 중간 지점
- **파란 박스** → 맨 아래 (스크롤 성공!)
## 스크롤이 여전히 안 되는 경우
### 체크리스트
1.**브라우저 캐시 완전 삭제**
```
F12 > Network 탭 > "Disable cache" 체크
```
2. ✅ **개발자 도구로 HTML 구조 확인**
```
F12 > Elements 탭
속성 패널의 div 찾기
→ "overflow-y: scroll" 스타일 확인
```
3. ✅ **콘솔 에러 확인**
```
F12 > Console 탭
에러 메시지 확인
```
4. ✅ **브라우저 호환성**
- Chrome/Edge: 권장
- Firefox: 지원
- Safari: 일부 스타일 이슈 가능
### 디버깅 가이드
**단계 1: HTML 구조 확인**
```html
<!-- 올바른 구조 -->
<div class="flex h-full w-full flex-col"> <!-- PropertiesPanel -->
<div class="flex h-16 shrink-0..."> <!-- 헤더 -->
<div class="flex-1 overflow-y-scroll..."> <!-- 스크롤 영역 -->
<div class="min-h-full space-y-4 p-4"> <!-- 속성 컴포넌트 -->
<!-- 긴 컨텐츠 -->
</div>
</div>
</div>
```
**단계 2: CSS 스타일 확인**
```css
/* 스크롤 영역에 있어야 할 스타일 */
overflow-y: scroll;
max-height: calc(100vh - 64px);
flex: 1 1 0%;
```
**단계 3: 컨텐츠 높이 확인**
```
스크롤이 생기려면:
컨텐츠 높이 > 컨테이너 높이
```
## 시각적 표시
현재 테스트용으로 추가된 표시들:
1. **노란색 박스** (맨 위)
- "📏 스크롤 테스트: 이 패널은 스크롤 가능해야 합니다"
2. **회색 배경** (전체 스크롤 영역)
- `bg-gray-50` 클래스
3. **빨간색 박스** (중간)
- "🚨 스크롤 테스트: 이 빨간 박스가 보이면 스크롤이 작동하는 것입니다!"
4. **20개 테스트 항목** (중간 ~ 아래)
- "테스트 항목 1" ~ "테스트 항목 20"
5. **파란색 박스** (맨 아래)
- "🎉 맨 아래 도착! 이 파란 박스가 보이면 스크롤이 완벽하게 작동합니다!"
## 제거할 테스트 코드
스크롤이 확인되면 다음 코드를 제거하세요:
### TableSourceProperties.tsx
```tsx
// 제거할 부분 1 (줄 172-174)
<div className="rounded bg-yellow-50 p-2 text-xs text-yellow-700">
📏 스크롤 테스트: 이 패널은 스크롤 가능해야 합니다
</div>
// 제거할 부분 2 (줄 340-357)
<div className="space-y-2">
<div className="rounded bg-red-50 p-4 text-red-700">
{/* ... */}
</div>
{[...Array(20)].map((_, i) => (/* ... */))}
<div className="rounded bg-blue-50 p-4 text-blue-700">
{/* ... */}
</div>
</div>
```
### PropertiesPanel.tsx
```tsx
// bg-gray-50 제거 (줄 47)
// 변경 전: className="flex-1 overflow-y-scroll bg-gray-50"
// 변경 후: className="flex-1 overflow-y-scroll"
```
## 핵심 원리
```
┌─────────────────────────────────┐
│ FlowEditor (h-full) │
│ ┌─────────────────────────────┐ │
│ │ PropertiesPanel (h-full) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 헤더 (h-16, shrink-0) │ │ │ ← 고정 64px
│ │ └─────────────────────────┘ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 스크롤 영역 │ │ │
│ │ │ (flex-1, overflow-y) │ │ │
│ │ │ │ │ │
│ │ │ ↓ 컨텐츠가 넘치면 │ │ │
│ │ │ ↓ 스크롤바 생성! │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
flex-1 = 남은 공간을 모두 차지
overflow-y: scroll = 세로 스크롤 강제 표시
maxHeight = 넘칠 경우를 대비한 최대 높이
```
## 마지막 체크포인트
스크롤이 작동하는지 확인하는 3가지 방법:
1.**마우스 휠**: 속성 패널 위에서 휠 스크롤
2.**스크롤바**: 우측에 스크롤바가 보이면 드래그
3.**키보드**: Page Up/Down 키 또는 방향키
하나라도 작동하면 성공입니다!
+542
View File
@@ -0,0 +1,542 @@
# ERP-node 시스템 시연 시나리오
## 전체 개요
**주제**: 발주 → 입고 프로세스 자동화
**목표**: 버튼 클릭 한 번으로 발주 데이터가 입고 테이블로 자동 이동하는 것을 보여주기
**총 시간**: 10분
---
## Part 1: 테이블 2개 생성 (2분)
### 1-1. 발주 테이블 생성
**화면 조작**:
1. 테이블 관리 메뉴 접속
2. "새 테이블" 버튼 클릭
3. 테이블 정보 입력:
- **테이블명(영문)**: `purchase_order`
- **테이블명(한글)**: `발주`
- **설명**: `발주 관리`
4. 컬럼 추가 (4개):
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 |
| ------------ | ------------ | ------ | --------- |
| order_no | 발주번호 | text | ✓ |
| item_name | 품목명 | text | ✓ |
| quantity | 수량 | number | ✓ |
| unit_price | 단가 | number | ✓ |
5. "테이블 생성" 버튼 클릭
6. 성공 메시지 확인
---
### 1-2. 입고 테이블 생성
**화면 조작**:
1. "새 테이블" 버튼 클릭
2. 테이블 정보 입력:
- **테이블명(영문)**: `receiving`
- **테이블명(한글)**: `입고`
- **설명**: `입고 관리`
3. 컬럼 추가 (5개):
| 컬럼명(영문) | 컬럼명(한글) | 타입 | 필수 여부 | 비고 |
| -------------- | ------------ | ------ | --------- | ------------------- |
| receiving_no | 입고번호 | text | ✓ | 자동 생성 |
| order_no | 발주번호 | text | ✓ | 발주 테이블 참조 |
| item_name | 품목명 | text | ✓ | |
| quantity | 수량 | number | ✓ | |
| receiving_date | 입고일자 | date | ✓ | 오늘 날짜 자동 입력 |
4. "테이블 생성" 버튼 클릭
5. 성공 메시지 확인
**포인트 강조**:
- 클릭만으로 데이터베이스 테이블 자동 생성
- Input Type에 따라 적절한 UI 자동 설정
---
## Part 2: 메뉴 2개 생성 (1분)
### 2-1. 발주 관리 메뉴 생성
**화면 조작**:
1. 관리자 메뉴 > 메뉴 관리 접속
2. "새 메뉴 추가" 버튼 클릭
3. 메뉴 정보 입력:
- **메뉴명**: `발주 관리`
- **순서**: 1
4. "저장" 클릭
---
### 2-2. 입고 관리 메뉴 생성
**화면 조작**:
1. "새 메뉴 추가" 버튼 클릭
2. 메뉴 정보 입력:
- **메뉴명**: `입고 관리`
- **순서**: 2
3. "저장" 클릭
4. 좌측 메뉴바에서 새로 생성된 메뉴 2개 확인
**포인트 강조**:
- URL 기반 자동 라우팅
- 아이콘으로 직관적인 메뉴 구성
---
## Part 3: 플로우 생성 (2분)
### 3-1. 플로우 생성
**화면 조작**:
1. 제어 관리 메뉴 접속
2. "새 플로우 생성" 버튼 클릭
3. 플로우 생성 모달에서 입력:
- **플로우명**: `발주-입고 프로세스`
- **설명**: `발주에서 입고로 데이터 자동 이동`
4. "생성" 버튼 클릭
5. 플로우 편집 화면(캔버스)으로 자동 이동
---
### 3-2. 노드 구성
**내레이션**:
"플로우는 소스 테이블과 액션 노드로 구성합니다. 발주 테이블에서 입고 테이블로 데이터를 INSERT하는 구조입니다."
**노드 1: 발주 테이블 소스**
**화면 조작**:
1. 캔버스 좌측 팔레트에서 "테이블 소스" 에서 테이블 노드 드래그
2. 캔버스에 드롭
3. 생성된 노드 클릭 → 우측 속성 패널 표시
4. 속성 패널에서 설정:
- **노드명**: `발주 테이블`
- **소스 테이블**: `purchase_order` 선택
- **색상**: 파란색 (#3b82f6)
5. 데이터 소스 타입 컨텍스트 데이터 선택
---
**노드 2: 입고 INSERT 액션**
**화면 조작**:
1. 좌측 팔레트에서 "INSERT 액션" 노드 드래그
2. 캔버스의 발주 테이블 오른쪽에 드롭
3. 노드 클릭 → 우측 속성 패널 표시
4. 속성 패널에서 설정:
- **노드명**: `입고 처리`
- **타겟 테이블**: `receiving`(입고) 선택
- **액션 타입**: INSERT
- **색상**: 초록색 (#22c55e)
---
### 3-3. 노드 연결 및 필드 매핑
**내레이션**:
"소스 테이블과 액션 노드를 연결하고 필드 매핑을 설정합니다."
**화면 조작**:
1. "발주 테이블" 노드의 오른쪽 연결점(핸들)에 마우스 올리기
2. 연결점에서 드래그 시작
3. "입고 처리" 노드의 왼쪽 연결점으로 드래그
4. 연결선 자동 생성됨
5. "입고 처리" (INSERT 액션) 노드 클릭
6. 우측 속성 패널에서 "필드 매핑" 탭 선택
7. 필드 매핑 설정:
| 소스 필드 (발주) | 타겟 필드 (입고) | 비고 |
| ---------------- | ---------------- | ------------- |
| order_no | order_no | 발주번호 복사 |
| item_name | item_name | 품목명 복사 |
| quantity | quantity | 수량 복사 |
| (자동 생성) | receiving_no | 입고번호 |
| (현재 날짜) | receiving_date | 입고일자 |
8. 우측 상단 "저장" 버튼 클릭
9. 성공 메시지: "플로우가 저장되었습니다"
**포인트 강조**:
- 테이블 소스 → 액션 노드 구조
- 필드 매핑으로 데이터 자동 복사 설정
- INSERT 액션으로 새 테이블에 데이터 생성
**참고**:
- `receiving_no``receiving_date`는 자동 생성 필드로 설정
- 같은 이름의 필드는 자동 매핑됨
---
## Part 4: 화면 설계 (2분)
### 4-1. 발주 관리 화면 설계
**화면 조작**:
1. 화면 관리 > 화면 설계 메뉴 접속
2. "발주 관리" 메뉴의 "화면 할당" 클릭
3. "새 화면 생성" 선택
4. 테이블 선택: `purchase_order` (발주)
**화면 구성**:
**전체: 테이블 리스트 컴포넌트 (CRUD 기능 포함)**
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
2. 테이블 설정:
- **연결 테이블**: `purchase_order`
- **컬럼 표시**:
| 컬럼 | 표시 | 정렬 가능 | 너비 |
| ---------- | ---- | --------- | ----- |
| order_no | ✓ | ✓ | 150px |
| item_name | ✓ | ✓ | 200px |
| quantity | ✓ | | 100px |
| unit_price | ✓ | | 120px |
3. 기능 설정:
- **조회**: 활성화
- **등록**: 활성화 (신규 버튼)
- **수정**: 활성화
- **삭제**: 활성화
- **페이징**: 10개씩
- **입고 처리 버튼**: 커스텀 액션 추가
4. 입고 처리 버튼 설정:
- **버튼 라벨**: `입고 처리`
- **버튼 위치**: 행 액션
- **연결 플로우**: `발주-입고 프로세스` 선택
- **플로우 액션**: `입고 처리` (Connection에서 정의한 액션)
5. "화면 저장" 버튼 클릭
---
### 4-2. 입고 관리 화면 설계
**화면 조작**:
1. "입고 관리" 메뉴의 "화면 할당" 클릭
2. "새 화면 생성" 선택
3. 테이블 선택: `receiving` (입고)
**화면 구성**:
**전체: 테이블 리스트 컴포넌트 (조회 전용)**
1. 컴포넌트 팔레트에서 "테이블 리스트" 드래그
2. 테이블 설정:
- **연결 테이블**: `receiving`
- **컬럼 표시**:
| 컬럼 | 표시 | 정렬 가능 | 너비 |
| -------------- | ---- | --------- | ----- |
| receiving_no | ✓ | ✓ | 150px |
| order_no | ✓ | ✓ | 150px |
| item_name | ✓ | ✓ | 200px |
| quantity | ✓ | | 100px |
| receiving_date | ✓ | ✓ | 120px |
3. 기능 설정:
- **조회**: 활성화
- **등록**: 비활성화 (플로우로만 데이터 생성)
- **수정**: 비활성화
- **삭제**: 비활성화
- **페이징**: 20개씩
- **정렬**: 입고일자 내림차순
4. "화면 저장" 버튼 클릭
**포인트 강조**:
- 테이블 리스트 컴포넌트로 CRUD 자동 구성
- 발주 화면에는 "입고 처리" 버튼으로 플로우 실행
- 입고 화면은 조회 전용 (플로우로만 데이터 생성)
---
## Part 5: 실행 및 동작 확인 (3분)
### 5-1. 발주 등록
**화면 조작**:
1. 좌측 메뉴에서 "발주 관리" 클릭
2. 화면 구성 확인:
- 테이블 리스트 컴포넌트 (빈 테이블)
- 상단에 "신규" 버튼
3. "신규" 버튼 클릭
4. 입력 모달 창 표시
5. 데이터 입력:
- **발주번호**: PO-001
- **품목명**: 노트북 (LG Gram 17)
- **수량**: 10
- **단가**: 2,000,000
6. "저장" 버튼 클릭
7. 성공 메시지 확인: "저장되었습니다"
8. 결과 확인:
- 테이블에 새 행 추가됨
- 행 우측에 "입고 처리" 버튼 표시됨
**추가 발주 등록 (옵션)**:
9. "신규" 버튼 클릭
10. 2번째 데이터 입력:
- **발주번호**: PO-002
- **품목명**: 모니터 (삼성 27인치)
- **수량**: 5
- **단가**: 300,000
11. "저장" 클릭
12. 테이블에 2개 행 확인
---
### 5-2. 입고 처리 실행 ⭐ (핵심 데모)
**화면 조작**:
1. 발주 테이블에서 첫 번째 행(PO-001 노트북) 확인
2. 행 우측의 **"입고 처리"** 버튼 클릭
3. 확인 대화상자:
- "이 발주를 입고 처리하시겠습니까?"
- **"예"** 클릭
4. 성공 메시지: "입고 처리되었습니다"
---
### 5-3. 자동 데이터 이동 확인 ⭐⭐⭐
**실시간 변화 확인**:
**1) 발주 테이블 자동 업데이트**
- PO-001 항목이 테이블에서 **즉시 사라짐**
- PO-002만 남아있음 (추가로 등록했다면)
**2) 입고 관리 화면으로 이동**
1. 좌측 메뉴에서 **"입고 관리"** 클릭
2. 입고 테이블에 **자동으로 데이터 생성됨**:
| 입고번호 | 발주번호 | 품목명 | 수량 | 입고일자 |
| ---------------- | -------- | ------------------- | ---- | ---------- |
| RCV-20250124-001 | PO-001 | 노트북 (LG Gram 17) | 10 | 2025-01-24 |
3. **데이터 자동 생성 확인**:
- 입고번호: 자동 생성됨 (RCV-20250124-001)
- 발주번호: PO-001 복사됨
- 품목명: 노트북 (LG Gram 17) 복사됨
- 수량: 10 복사됨
- 입고일자: 오늘 날짜 자동 입력
**3) 다시 발주 관리로 돌아가기**
1. 좌측 메뉴 "발주 관리" 클릭
2. PO-001은 여전히 사라진 상태 확인
3. PO-002만 남아있음
**4) 제어 관리에서 확인**
1. 제어 관리 > 플로우 목록 접속
2. "발주-입고 프로세스" 클릭
3. 플로우 현황 확인:
- **발주 완료**: 1건 (PO-002)
- **입고 완료**: 1건 (PO-001)
---
### 5-4. 추가 입고 처리 (옵션)
**화면 조작**:
1. "발주 관리" 화면에서 PO-002 (모니터) 선택
2. "입고 처리" 버튼 클릭
3. 확인 후 입고 완료
4. 최종 확인:
- 발주 관리: 0건 (모두 입고 처리됨)
- 입고 관리: 2건 (PO-001, PO-002)
- 제어 관리 플로우:
- **발주 완료: 0건**
- **입고 완료: 2건**
---
## 시연 마무리 (30초)
**화면 정리 및 요약**:
**보여준 핵심 기능**:
-**코딩 없이 테이블 생성**: 클릭만으로 DB 테이블 자동 생성
-**시각적 플로우 구성**: 드래그앤드롭으로 업무 흐름 설계
-**자동 데이터 이동**: 버튼 클릭 한 번으로 테이블 간 데이터 자동 복사 및 이동
-**실시간 상태 추적**: 제어 관리에서 플로우 현황 확인
-**빠른 화면 구성**: 테이블 리스트 컴포넌트로 CRUD 자동 완성
**마지막 화면**:
- 대시보드 또는 시스템 전체 구성도
- 로고 및 연락처 정보
**자막**:
"개발자 없이도 비즈니스 담당자가 직접 업무 시스템을 구축할 수 있습니다."
---
## 시간 배분 요약
| 파트 | 시간 | 주요 내용 |
| -------- | ---------- | ---------------------------- |
| Part 1 | 2분 | 테이블 2개 생성 (발주, 입고) |
| Part 2 | 1분 | 메뉴 2개 생성 |
| Part 3 | 2분 | 플로우 구성 및 연결 설정 |
| Part 4 | 2분 | 화면 2개 디자인 |
| Part 5 | 3분 | 발주 등록 → 입고 처리 실행 |
| 마무리 | 0.5분 | 요약 및 정리 |
| **합계** | **10.5분** | |
---
## 시연 준비사항
### 사전 설정
1. 개발 서버 실행: `http://localhost:9771`
2. 로그인 정보: `wace / qlalfqjsgh11`
3. 데이터베이스 초기화 (테스트 데이터 제거)
### 녹화 설정
- **해상도**: 1920x1080 (Full HD)
- **프레임**: 30fps
- **마우스 효과**: 클릭 하이라이트 활성화
- **배경음악**: 부드러운 BGM (옵션)
- **자막**: 주요 포인트마다 표시
### 시연 팁
- 각 단계마다 2-3초 대기 (시청자 이해 시간)
- 중요한 버튼 클릭 시 화면 확대 효과
- 플로우 위젯 카운트 변화는 빨간색 박스로 강조
- 성공 메시지는 충분히 길게 보여주기 (최소 3초)
- 입고 테이블에 데이터 들어오는 순간 화면 확대
---
## 시연 스크립트 (참고용)
### 오프닝 (10초)
"안녕하세요. 오늘은 ERP-node 시스템의 핵심 기능을 시연하겠습니다. 발주에서 입고까지 데이터가 자동으로 이동하는 과정을 보여드립니다."
### Part 1 (2분)
"먼저 발주와 입고를 관리할 테이블을 생성합니다. 코딩 없이 클릭만으로 데이터베이스 테이블이 자동으로 만들어집니다."
### Part 2 (1분)
"이제 사용자가 접근할 메뉴를 추가합니다. URL만 지정하면 자동으로 라우팅이 연결됩니다."
### Part 3 (2분)
"발주에서 입고로 데이터가 이동하는 흐름을 제어 플로우로 정의합니다. 두 테이블을 연결하고 버튼을 누르면 자동으로 데이터가 복사 및 이동하도록 설정합니다."
### Part 4 (2분)
"실제 사용자가 볼 화면을 디자인합니다. 테이블 리스트 컴포넌트를 사용하면 CRUD 기능이 자동으로 구성되고, 각 행에 입고 처리 버튼을 추가하여 플로우를 실행할 수 있습니다."
### Part 5 (3분)
"이제 실제로 작동하는 모습을 보겠습니다. 발주를 등록하고... (데이터 입력) 저장하면 테이블에 추가됩니다. 입고 처리 버튼을 누르면... (클릭) 발주 테이블에서 데이터가 사라지고 입고 테이블에 자동으로 생성됩니다!"
### 클로징 (10초)
"이처럼 ERP-node는 코딩 없이 비즈니스 로직을 구현할 수 있는 노코드 플랫폼입니다. 감사합니다."
---
## 체크리스트
### 시연 전
- [ ] 개발 서버 실행 확인
- [ ] 로그인 테스트
- [ ] 기존 테스트 데이터 삭제
- [ ] 브라우저 창 크기 조정 (1920x1080)
- [ ] 녹화 프로그램 설정
- [ ] 마이크 테스트
- [ ] 시나리오 1회 이상 리허설
### 시연 중
- [ ] 천천히 명확하게 진행
- [ ] 각 단계마다 결과 확인
- [ ] 플로우 위젯 카운트 강조
- [ ] 입고 테이블 데이터 자동 생성 강조
### 시연 후
- [ ] 녹화 파일 확인
- [ ] 자막 추가 (필요 시)
- [ ] 배경음악 삽입 (옵션)
- [ ] 인트로/아웃트로 편집
- [ ] 최종 영상 검수
---
## 추가 개선 아이디어
### 시연 버전 2 (고급)
- 발주 승인 단계 추가 (발주 요청 → 승인 → 입고)
- 입고 수량 불일치 처리 (일부 입고)
- 대시보드에서 통계 차트 표시
### 시연 버전 3 (실전)
- 실제 업무: 구매 요청 → 견적 → 발주 → 입고 → 검수
- 권한 관리: 요청자, 승인자, 구매담당자 역할 분리
- 알림: 각 단계 변경 시 담당자에게 알림
---
**작성일**: 2025-01-24
**버전**: 1.0
**작성자**: AI Assistant
@@ -0,0 +1,293 @@
# 외부호출 데이터 매핑 시스템 설계서
## 1. 개요
외부 API 호출 시 데이터를 송수신하고, 이를 내부 테이블과 매핑하는 시스템을 구현합니다.
## 2. 현재 상황 분석
### 2.1 기존 기능
- ✅ REST API 호출 기본 기능
- ✅ 인증 처리 (API Key, Basic, Bearer 등)
- ✅ 요청/응답 테스트 기능
- ✅ 외부호출 설정 저장
### 2.2 필요한 확장 기능
- 🔄 GET 요청 시 응답 데이터를 내부 테이블에 저장
- 🔄 POST 요청 시 내부 테이블 데이터를 외부로 전송
- 🔄 필드 매핑 설정 (외부 필드 ↔ 내부 필드)
- 🔄 데이터 변환 및 검증
## 3. 시스템 아키텍처
### 3.1 데이터 플로우
```
GET 요청 플로우:
내부 이벤트 → 외부 API 호출 → 응답 데이터 → 필드 매핑 → 내부 테이블 저장
POST 요청 플로우:
내부 이벤트 → 내부 테이블 조회 → 필드 매핑 → 외부 API 전송 → 응답 처리
```
### 3.2 컴포넌트 구조
```
ExternalCallPanel
├── RestApiSettings (기존)
├── DataMappingSettings (신규)
│ ├── SourceTableSelector
│ ├── TargetTableSelector
│ ├── FieldMappingEditor
│ └── DataTransformEditor
└── ExternalCallTestPanel (확장)
```
## 4. 데이터베이스 스키마 확장
### 4.1 external_call_configs 테이블 확장
```sql
ALTER TABLE external_call_configs ADD COLUMN IF NOT EXISTS data_mapping_config JSONB;
```
### 4.2 data_mapping_config JSON 구조
```typescript
interface DataMappingConfig {
direction: "inbound" | "outbound" | "bidirectional";
// GET 요청용 - 외부 → 내부
inboundMapping?: {
targetTable: string;
targetSchema?: string;
fieldMappings: FieldMapping[];
insertMode: "insert" | "upsert" | "update";
keyFields?: string[]; // upsert/update 시 키 필드
};
// POST 요청용 - 내부 → 외부
outboundMapping?: {
sourceTable: string;
sourceSchema?: string;
sourceFilter?: string; // WHERE 조건
fieldMappings: FieldMapping[];
};
}
interface FieldMapping {
sourceField: string; // 외부 API 필드명 또는 내부 테이블 컬럼명
targetField: string; // 내부 테이블 컬럼명 또는 외부 API 필드명
dataType: "string" | "number" | "boolean" | "date" | "json";
transform?: {
type: "none" | "constant" | "format" | "function";
value?: any;
format?: string; // 날짜 포맷 등
functionName?: string; // 커스텀 변환 함수
};
required?: boolean;
defaultValue?: any;
}
```
## 5. 프론트엔드 컴포넌트 설계
### 5.1 DataMappingSettings.tsx
```typescript
interface DataMappingSettingsProps {
config: DataMappingConfig;
onConfigChange: (config: DataMappingConfig) => void;
httpMethod: string;
availableTables: TableInfo[];
}
// 주요 기능:
// - 방향 선택 (inbound/outbound/bidirectional)
// - 소스/타겟 테이블 선택
// - 필드 매핑 에디터
// - 데이터 변환 설정
```
### 5.2 FieldMappingEditor.tsx
```typescript
interface FieldMappingEditorProps {
mappings: FieldMapping[];
sourceFields: FieldInfo[];
targetFields: FieldInfo[];
onMappingsChange: (mappings: FieldMapping[]) => void;
}
// 주요 기능:
// - 드래그 앤 드롭으로 필드 매핑
// - 데이터 타입 자동 추론
// - 변환 함수 설정
// - 필수 필드 검증
```
### 5.3 DataTransformEditor.tsx
```typescript
// 데이터 변환 규칙 설정
// - 상수값 할당
// - 날짜 포맷 변환
// - 문자열 변환 (대소문자, 트림 등)
// - 커스텀 함수 적용
```
## 6. 백엔드 서비스 확장
### 6.1 ExternalCallExecutor 확장
```typescript
class ExternalCallExecutor {
async executeWithDataMapping(
config: ExternalCallConfig,
triggerData?: any
): Promise<ExternalCallResult> {
const result = await this.executeApiCall(config);
if (result.success && config.dataMappingConfig) {
if (config.restApiSettings.httpMethod === "GET") {
await this.processInboundData(result, config.dataMappingConfig);
}
}
return result;
}
private async processInboundData(
result: ExternalCallResult,
mappingConfig: DataMappingConfig
) {
// 1. 응답 데이터 파싱
// 2. 필드 매핑 적용
// 3. 데이터 변환
// 4. 데이터베이스 저장
}
private async prepareOutboundData(
mappingConfig: DataMappingConfig,
triggerData?: any
): Promise<any> {
// 1. 소스 테이블 조회
// 2. 필드 매핑 적용
// 3. 데이터 변환
// 4. API 요청 바디 생성
}
}
```
### 6.2 DataMappingService.ts (신규)
```typescript
class DataMappingService {
async mapInboundData(
sourceData: any,
mapping: InboundMapping
): Promise<any[]> {
// 외부 데이터 → 내부 테이블 매핑
}
async mapOutboundData(
sourceTable: string,
mapping: OutboundMapping,
filter?: any
): Promise<any> {
// 내부 테이블 → 외부 API 매핑
}
private transformFieldValue(value: any, transform: FieldTransform): any {
// 필드 변환 로직
}
}
```
## 7. 구현 단계
### Phase 1: 기본 매핑 시스템 (1-2주)
1. 데이터베이스 스키마 확장
2. DataMappingSettings 컴포넌트 개발
3. 기본 필드 매핑 기능
4. GET 요청 응답 데이터 저장
### Phase 2: 고급 매핑 기능 (1-2주)
1. POST 요청 데이터 송신
2. 필드 변환 기능
3. upsert/update 모드
4. 배치 처리
### Phase 3: UI/UX 개선 (1주)
1. 드래그 앤 드롭 매핑 에디터
2. 실시간 미리보기
3. 매핑 템플릿
4. 에러 처리 및 로깅
## 8. 사용 시나리오
### 8.1 외부 API에서 데이터 가져오기 (GET)
```
고객사 API → 우리 customer 테이블
- 고객 정보 동기화
- 주문 정보 수집
- 재고 정보 업데이트
```
### 8.2 외부 API로 데이터 보내기 (POST)
```
우리 order 테이블 → 배송사 API
- 주문 정보 전달
- 재고 변동 알림
- 상태 업데이트 전송
```
## 9. 기술적 고려사항
### 9.1 데이터 일관성
- 트랜잭션 처리
- 롤백 메커니즘
- 중복 데이터 처리
### 9.2 성능 최적화
- 배치 처리
- 비동기 처리
- 캐싱 전략
### 9.3 보안
- 데이터 검증
- SQL 인젝션 방지
- 민감 데이터 마스킹
### 9.4 모니터링
- 매핑 실행 로그
- 에러 추적
- 성능 메트릭
## 10. 성공 지표
- ✅ 외부 API 응답 데이터를 내부 테이블에 정확히 저장
- ✅ 내부 테이블 데이터를 외부 API로 정확히 전송
- ✅ 필드 매핑 설정이 직관적이고 사용하기 쉬움
- ✅ 데이터 변환이 정확하고 안정적
- ✅ 에러 발생 시 적절한 처리 및 알림
## 11. 다음 단계
1. **우선순위 결정**: GET/POST 중 어느 것부터 구현할지
2. **테이블 선택**: 매핑할 주요 테이블들 식별
3. **프로토타입**: 간단한 매핑 시나리오로 POC 개발
4. **점진적 확장**: 기본 → 고급 기능 순서로 개발
이 설계서를 바탕으로 단계별로 구현해 나가면 됩니다. 어떤 부분부터 시작하고 싶으신가요?
@@ -0,0 +1,500 @@
# 제어관리 데이터 소스 확장 가이드
## 개요
제어관리(플로우) 실행 시 사용할 수 있는 데이터 소스가 확장되었습니다. 이제 **폼 데이터**, **테이블 선택 항목**, **테이블 전체 데이터**, **플로우 선택 항목**, **플로우 스텝 전체 데이터** 등 다양한 소스에서 데이터를 가져와 제어를 실행할 수 있습니다.
## 지원 데이터 소스
### 1. `form` - 폼 데이터
- **설명**: 현재 화면의 폼 입력값을 사용합니다.
- **사용 시나리오**: 단일 레코드 생성/수정 시
- **데이터 형태**: 단일 객체
```typescript
{
name: "홍길동",
age: 30,
email: "test@example.com"
}
```
### 2. `table-selection` - 테이블 선택 항목
- **설명**: 테이블에서 사용자가 선택한 행의 데이터를 사용합니다.
- **사용 시나리오**: 선택된 항목에 대한 일괄 처리
- **데이터 형태**: 배열 (선택된 행들)
```typescript
[
{ id: 1, name: "항목1", status: "대기" },
{ id: 2, name: "항목2", status: "대기" }
]
```
### 3. `table-all` - 테이블 전체 데이터 🆕
- **설명**: 테이블의 **모든 데이터**를 사용합니다 (페이징 무관).
- **사용 시나리오**:
- 전체 데이터에 대한 일괄 처리
- 통계/집계 작업
- 대량 데이터 마이그레이션
- **데이터 형태**: 배열 (전체 행)
- **주의사항**: 데이터가 많을 경우 성능 이슈가 있을 수 있습니다.
```typescript
[
{ id: 1, name: "항목1", status: "대기" },
{ id: 2, name: "항목2", status: "진행중" },
{ id: 3, name: "항목3", status: "완료" },
// ... 수천 개의 행
]
```
### 4. `flow-selection` - 플로우 선택 항목
- **설명**: 플로우 위젯에서 사용자가 선택한 데이터를 사용합니다.
- **사용 시나리오**: 플로우 단계별로 선택된 항목 처리
- **데이터 형태**: 배열 (선택된 행들)
```typescript
[
{ id: 10, taskName: "작업1", stepId: 2 },
{ id: 11, taskName: "작업2", stepId: 2 }
]
```
### 5. `flow-step-all` - 플로우 스텝 전체 데이터 🆕
- **설명**: 현재 선택된 플로우 단계의 **모든 데이터**를 사용합니다.
- **사용 시나리오**:
- 특정 단계의 모든 항목 일괄 처리
- 단계별 완료율 계산
- 단계 이동 시 전체 데이터 마이그레이션
- **데이터 형태**: 배열 (해당 스텝의 전체 행)
```typescript
[
{ id: 10, taskName: "작업1", stepId: 2, assignee: "홍길동" },
{ id: 11, taskName: "작업2", stepId: 2, assignee: "김철수" },
{ id: 12, taskName: "작업3", stepId: 2, assignee: "이영희" },
// ... 해당 스텝의 모든 데이터
]
```
### 6. `both` - 폼 + 테이블 선택
- **설명**: 폼 데이터와 테이블 선택 항목을 결합하여 사용합니다.
- **사용 시나리오**: 폼의 공통 정보 + 개별 항목 처리
- **데이터 형태**: 배열 (폼 데이터 + 선택된 행들)
```typescript
[
{ name: "홍길동", age: 30 }, // 폼 데이터
{ id: 1, name: "항목1", status: "대기" },
{ id: 2, name: "항목2", status: "대기" }
]
```
### 7. `all-sources` - 모든 소스 결합 🆕
- **설명**: 폼, 테이블 전체, 플로우 등 **모든 소스의 데이터를 결합**하여 사용합니다.
- **사용 시나리오**:
- 복잡한 데이터 통합 작업
- 다중 소스 동기화
- 전체 시스템 상태 업데이트
- **데이터 형태**: 배열 (모든 소스의 데이터 병합)
- **주의사항**: 매우 많은 데이터가 전달될 수 있으므로 신중히 사용하세요.
```typescript
[
{ name: "홍길동", age: 30 }, // 폼 데이터
{ id: 1, name: "테이블1" }, // 테이블 선택
{ id: 2, name: "테이블2" }, // 테이블 선택
{ id: 3, name: "테이블3" }, // 테이블 전체
{ id: 10, taskName: "작업1" }, // 플로우 선택
// ... 모든 소스의 데이터
]
```
## 설정 방법
### 1. 버튼 상세 설정에서 데이터 소스 선택
1. 화면 디자이너에서 버튼 선택
2. 우측 패널 > **상세 설정**
3. **제어관리 활성화** 체크
4. **제어 데이터 소스** 드롭다운에서 원하는 소스 선택
### 2. 데이터 소스 옵션
```
┌─────────────────────────────────────┐
│ 제어 데이터 소스 │
├─────────────────────────────────────┤
│ 📄 폼 데이터 │
│ 📊 테이블 선택 항목 │
│ 📊 테이블 전체 데이터 🆕 │
│ 🔄 플로우 선택 항목 │
│ 🔄 플로우 스텝 전체 데이터 🆕 │
│ 📋 폼 + 테이블 선택 │
│ 🌐 모든 소스 결합 🆕 │
└─────────────────────────────────────┘
```
## 실제 사용 예시
### 예시 1: 테이블 전체 데이터로 일괄 상태 업데이트
```typescript
// 제어 설정
{
controlDataSource: "table-all",
flowConfig: {
flowId: 10,
flowName: "전체 항목 승인 처리",
executionTiming: "replace"
}
}
// 실행 시 전달되는 데이터
{
buttonId: "btn_approve_all",
sourceData: [
{ id: 1, name: "항목1", status: "대기" },
{ id: 2, name: "항목2", status: "대기" },
{ id: 3, name: "항목3", status: "대기" },
// ... 테이블의 모든 행 (1000개)
]
}
```
### 예시 2: 플로우 스텝 전체를 다음 단계로 이동
```typescript
// 제어 설정
{
controlDataSource: "flow-step-all",
flowConfig: {
flowId: 15,
flowName: "단계 일괄 이동",
executionTiming: "replace"
}
}
// 실행 시 전달되는 데이터
{
buttonId: "btn_move_all",
flowStepId: 2,
sourceData: [
{ id: 10, taskName: "작업1", stepId: 2 },
{ id: 11, taskName: "작업2", stepId: 2 },
{ id: 12, taskName: "작업3", stepId: 2 },
// ... 해당 스텝의 모든 데이터
]
}
```
### 예시 3: 선택된 항목만 처리
```typescript
// 제어 설정
{
controlDataSource: "table-selection",
flowConfig: {
flowId: 5,
flowName: "선택 항목 승인",
executionTiming: "replace"
}
}
// 실행 시 전달되는 데이터 (사용자가 2개 선택한 경우)
{
buttonId: "btn_approve_selected",
sourceData: [
{ id: 1, name: "항목1", status: "대기" },
{ id: 5, name: "항목5", status: "대기" }
]
}
```
## 데이터 로딩 방식
### 자동 로딩 vs 수동 로딩
1. **테이블 선택 항목** (`table-selection`)
- ✅ 자동 로딩: 사용자가 이미 선택한 데이터 사용
- 별도 로딩 불필요
2. **테이블 전체 데이터** (`table-all`)
- ⚡ 지연 로딩: 버튼 클릭 시 필요한 경우만 로드
- 부모 컴포넌트에서 `onRequestTableAllData` 콜백 제공 필요
3. **플로우 스텝 전체 데이터** (`flow-step-all`)
- ⚡ 지연 로딩: 버튼 클릭 시 필요한 경우만 로드
- 부모 컴포넌트에서 `onRequestFlowStepAllData` 콜백 제공 필요
### 부모 컴포넌트 구현 예시
```tsx
<OptimizedButtonComponent
component={buttonComponent}
selectedRowsData={selectedRowsData}
// 테이블 전체 데이터 로드 콜백
onRequestTableAllData={async () => {
const response = await fetch(`/api/data/table/${tableId}?all=true`);
const data = await response.json();
return data.records;
}}
// 플로우 스텝 전체 데이터 로드 콜백
onRequestFlowStepAllData={async (stepId) => {
const response = await fetch(`/api/flow/step/${stepId}/all-data`);
const data = await response.json();
return data.records;
}}
/>
```
## 성능 고려사항
### 1. 대량 데이터 처리
- **테이블 전체 데이터**: 수천 개의 행이 있을 경우 메모리 및 네트워크 부담
- **해결 방법**:
- 배치 처리 사용
- 페이징 처리
- 서버 사이드 처리
### 2. 로딩 시간
```typescript
// ❌ 나쁜 예: 모든 데이터를 항상 미리 로드
useEffect(() => {
loadTableAllData(); // 버튼을 누르지 않아도 로드됨
}, []);
// ✅ 좋은 예: 필요할 때만 로드 (지연 로딩)
const onRequestTableAllData = async () => {
return await loadTableAllData(); // 버튼 클릭 시에만 로드
};
```
### 3. 캐싱
```typescript
// 전체 데이터를 캐싱하여 재사용
const [cachedTableAllData, setCachedTableAllData] = useState<any[]>([]);
const onRequestTableAllData = async () => {
if (cachedTableAllData.length > 0) {
console.log("캐시된 데이터 사용");
return cachedTableAllData;
}
const data = await loadTableAllData();
setCachedTableAllData(data);
return data;
};
```
## 노드 플로우에서 데이터 사용
### contextData 구조
노드 플로우 실행 시 전달되는 `contextData`는 다음과 같은 구조를 가집니다:
```typescript
{
buttonId: "btn_approve",
screenId: 123,
companyCode: "DEFAULT",
userId: "user001",
controlDataSource: "table-all",
// 공통 데이터
formData: { name: "홍길동" },
// 소스별 데이터
selectedRowsData: [...], // table-selection
tableAllData: [...], // table-all
flowSelectedData: [...], // flow-selection
flowStepAllData: [...], // flow-step-all
flowStepId: 2, // 현재 플로우 스텝 ID
// 통합 데이터 (모든 노드에서 사용 가능)
sourceData: [...] // controlDataSource에 따라 결정된 데이터
}
```
### 노드에서 데이터 접근
```typescript
// External Call 노드
{
nodeType: "external-call",
config: {
url: "https://api.example.com/bulk-approve",
method: "POST",
body: {
// sourceData를 사용하여 데이터 전달
items: "{{sourceData}}",
approver: "{{formData.approver}}"
}
}
}
// DDL 노드
{
nodeType: "ddl",
config: {
sql: `
UPDATE tasks
SET status = 'approved'
WHERE id IN ({{sourceData.map(d => d.id).join(',')}})
`
}
}
```
## 디버깅 및 로그
### 콘솔 로그 확인
버튼 클릭 시 다음과 같은 로그가 출력됩니다:
```
📊 데이터 소스 모드: {
controlDataSource: "table-all",
hasFormData: true,
hasTableSelection: false,
hasFlowSelection: false
}
📊 테이블 전체 데이터 로드 중...
✅ 테이블 전체 데이터 1,234건 로드 완료
🚀 노드 플로우 실행 시작: {
flowId: 10,
flowName: "전체 항목 승인",
timing: "replace",
sourceDataCount: 1234
}
```
### 에러 처리
```typescript
// 데이터 로드 실패 시
실패: Network error
🔔 Toast: "테이블 전체 데이터를 불러오지 못했습니다"
// 플로우 실행 실패 시
실패: 조건
🔔 Toast: "테이블 전체 조건 불만족: status === 'pending' (실제값: approved)"
```
## 마이그레이션 가이드
### 기존 설정에서 업그레이드
기존에 `table-selection`을 사용하던 버튼을 `table-all`로 변경하는 경우:
1. **버튼 설정 변경**: `table-selection``table-all`
2. **부모 컴포넌트 업데이트**: `onRequestTableAllData` 콜백 추가
3. **노드 플로우 업데이트**: 대량 데이터 처리 로직 추가
4. **테스트**: 소량 데이터로 먼저 테스트 후 전체 적용
### 하위 호환성
- ✅ 기존 `form`, `table-selection`, `both` 설정은 그대로 동작
- ✅ 새로운 데이터 소스는 선택적으로 사용 가능
- ✅ 기존 노드 플로우는 수정 없이 동작
## 베스트 프랙티스
### 1. 적절한 데이터 소스 선택
| 시나리오 | 권장 데이터 소스 |
|---------|----------------|
| 단일 레코드 생성/수정 | `form` |
| 선택된 항목 일괄 처리 | `table-selection` |
| 전체 항목 일괄 처리 | `table-all` |
| 플로우 단계별 선택 처리 | `flow-selection` |
| 플로우 단계 전체 이동 | `flow-step-all` |
| 복잡한 통합 작업 | `all-sources` |
### 2. 성능 최적화
```typescript
// ✅ 좋은 예: 배치 처리
const batchSize = 100;
for (let i = 0; i < sourceData.length; i += batchSize) {
const batch = sourceData.slice(i, i + batchSize);
await processBatch(batch);
}
// ❌ 나쁜 예: 동기 처리
for (const item of sourceData) {
await processItem(item); // 1000개면 1000번 API 호출
}
```
### 3. 사용자 피드백
```typescript
// 대량 데이터 처리 시 진행률 표시
toast.info(`${processed}/${total} 항목 처리 중...`, {
id: "batch-progress"
});
```
## 문제 해결
### Q1: 테이블 전체 데이터가 로드되지 않습니다
**A**: 부모 컴포넌트에 `onRequestTableAllData` 콜백이 구현되어 있는지 확인하세요.
```tsx
// InteractiveScreenViewer.tsx 확인
<OptimizedButtonComponent
onRequestTableAllData={async () => {
// 이 함수가 구현되어 있어야 함
return await fetchAllData();
}}
/>
```
### Q2: 플로우 스텝 전체 데이터가 빈 배열입니다
**A**:
1. 플로우 스텝이 선택되어 있는지 확인
2. `flowSelectedStepId`가 올바르게 전달되는지 확인
3. `onRequestFlowStepAllData` 콜백이 구현되어 있는지 확인
### Q3: 데이터가 너무 많아 브라우저가 느려집니다
**A**:
1. 서버 사이드 처리 고려
2. 배치 처리 사용
3. 페이징 적용
4. `table-selection` 사용 권장 (전체 대신 선택)
## 관련 파일
### 타입 정의
- `frontend/types/control-management.ts` - `ControlDataSource` 타입
### 핵심 로직
- `frontend/lib/utils/nodeFlowButtonExecutor.ts` - 데이터 준비 및 전달
- `frontend/components/screen/OptimizedButtonComponent.tsx` - 버튼 컴포넌트
### UI 설정
- `frontend/components/screen/config-panels/ButtonDataflowConfigPanel.tsx` - 설정 패널
### 서비스
- `frontend/lib/services/optimizedButtonDataflowService.ts` - 데이터 검증 및 처리
## 업데이트 이력
- **2025-01-24**: 초기 문서 작성
- `table-all` 데이터 소스 추가
- `flow-step-all` 데이터 소스 추가
- `all-sources` 데이터 소스 추가
- 지연 로딩 메커니즘 구현
@@ -0,0 +1,469 @@
# 🔄 제어관리 시스템 개선 계획서
## 📋 개요
데이터 매핑 시스템이 추가되면서 기존 제어관리 로직과 버튼 연동 방식의 개선이 필요합니다.
## 🎯 주요 개선사항
### 1. 명칭 변경: "관계도" → "관계"
#### 1.1 변경 이유
- **기존**: "관계도"는 다이어그램 전체를 의미하는 용어
- **현재**: 실제로는 개별 "관계"를 설정하고 관리
- **개선**: 사용자 이해도 향상 및 용어 일관성 확보
#### 1.2 변경 대상 파일들
```typescript
// UI 컴포넌트들
frontend / components / screen / config -
panels / ButtonDataflowConfigPanel.tsx;
frontend / components / dataflow / DataFlowDesigner.tsx;
frontend / components / dataflow / SaveDiagramModal.tsx;
frontend / components / dataflow / RelationshipListModal.tsx;
frontend /
components /
dataflow /
connection /
redesigned /
RightPanel /
ConnectionStep.tsx;
// API 및 서비스
frontend / lib / api / dataflow.ts;
frontend / hooks / useDataFlowDesigner.ts;
// 타입 정의
frontend / types / control - management.ts;
```
### 2. 버튼 제어관리 로직 개선
#### 2.1 현재 문제점
```typescript
// 🔴 기존: 복잡한 관계도 선택 방식
interface ButtonDataflowConfig {
controlMode: "simple" | "advanced";
selectedDiagramId?: number; // 관계도 전체 선택
selectedRelationshipId?: string; // 개별 관계 선택
// ...
}
```
#### 2.2 개선 방향
```typescript
// 🟢 개선: 단순화된 관계 직접 선택
interface ButtonDataflowConfig {
controlMode: "relationship" | "external_call" | "custom";
// 관계 기반 제어
relationshipConfig?: {
relationshipId: string; // 관계 직접 선택
relationshipName: string; // 관계명 표시
executionTiming: "before" | "after" | "replace";
contextData?: Record<string, any>; // 실행 시 전달할 컨텍스트
};
// 외부호출 제어
externalCallConfig?: {
configId: string; // external_call_configs ID
configName: string; // 설정명 표시
executionTiming: "before" | "after" | "replace";
dataMappingEnabled: boolean; // 데이터 매핑 사용 여부
};
// 커스텀 제어
customConfig?: {
actionType: string;
parameters: Record<string, any>;
};
}
```
### 3. 외부호출 연동 개선
#### 3.1 현재 외부호출 설정 방식
```typescript
// 🔴 현재: 복잡한 설정 구조
interface ExternalCallConfig {
callType: "rest-api";
restApiSettings: {
apiUrl: string;
httpMethod: string;
// ... 많은 설정들
};
}
```
#### 3.2 개선된 연동 방식
```typescript
// 🟢 개선: 단순화된 참조 구조
interface ButtonExternalCallConfig {
// 1단계: 저장된 외부호출 설정 선택
externalCallConfigId: string; // external_call_configs 테이블 ID
configName: string; // 설정명 (UI 표시용)
// 2단계: 실행 시점 설정
executionTiming: "before" | "after" | "replace";
// 3단계: 데이터 전달 방식
dataMapping: {
enabled: boolean; // 데이터 매핑 사용 여부
sourceMode: "form" | "table" | "custom"; // 데이터 소스
sourceConfig?: {
tableName?: string; // table 모드용
customData?: Record<string, any>; // custom 모드용
};
};
// 4단계: 실행 옵션
executionOptions: {
rollbackOnError: boolean; // 실패 시 롤백
showLoadingIndicator: boolean; // 로딩 표시
successMessage?: string; // 성공 메시지
errorMessage?: string; // 실패 메시지
};
}
```
### 4. 버튼 액션 실행 로직 개선
#### 4.1 현재 실행 플로우
```mermaid
graph TD
A[버튼 클릭] --> B[기존 액션 실행]
B --> C[제어관리 확인]
C --> D[관계도 조회]
D --> E[관계 찾기]
E --> F[조건 검증]
F --> G[액션 실행]
```
#### 4.2 개선된 실행 플로우
```mermaid
graph TD
A[버튼 클릭] --> B[제어 설정 확인]
B --> C{제어 타입}
C -->|관계| D[관계 직접 실행]
C -->|외부호출| E[외부호출 실행]
C -->|없음| F[기존 액션만 실행]
D --> G[조건 검증]
G --> H[관계 액션 실행]
E --> I[데이터 매핑]
I --> J[외부 API 호출]
J --> K[응답 처리]
H --> L[완료]
K --> L
F --> L
```
#### 4.3 개선된 ButtonActionExecutor
```typescript
export class ButtonActionExecutor {
/**
* 🔥 개선된 버튼 액션 실행
*/
static async executeButtonAction(
buttonConfig: ExtendedButtonTypeConfig,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ButtonExecutionResult> {
const executionPlan = this.createExecutionPlan(buttonConfig);
const results: ExecutionResult[] = [];
try {
// 1. Before 타이밍 제어 실행
if (executionPlan.beforeControls.length > 0) {
const beforeResults = await this.executeControls(
executionPlan.beforeControls,
formData,
context
);
results.push(...beforeResults);
}
// 2. 메인 액션 실행 (replace가 아닌 경우에만)
if (!executionPlan.hasReplaceControl) {
const mainResult = await this.executeMainAction(
buttonConfig,
formData,
context
);
results.push(mainResult);
}
// 3. After 타이밍 제어 실행
if (executionPlan.afterControls.length > 0) {
const afterResults = await this.executeControls(
executionPlan.afterControls,
formData,
context
);
results.push(...afterResults);
}
return {
success: true,
results,
executionTime: Date.now() - context.startTime,
};
} catch (error) {
// 롤백 처리
await this.handleExecutionError(error, results, buttonConfig);
throw error;
}
}
/**
* 🔥 제어 실행 (관계 또는 외부호출)
*/
private static async executeControls(
controls: ControlConfig[],
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult[]> {
const results: ExecutionResult[] = [];
for (const control of controls) {
switch (control.type) {
case "relationship":
const relationshipResult = await this.executeRelationship(
control.relationshipConfig!,
formData,
context
);
results.push(relationshipResult);
break;
case "external_call":
const externalCallResult = await this.executeExternalCall(
control.externalCallConfig!,
formData,
context
);
results.push(externalCallResult);
break;
}
}
return results;
}
/**
* 🔥 관계 실행
*/
private static async executeRelationship(
config: RelationshipConfig,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
// 1. 관계 정보 조회
const relationship = await RelationshipAPI.getRelationshipById(
config.relationshipId
);
// 2. 컨텍스트 데이터 준비
const contextData = {
...formData,
...config.contextData,
buttonId: context.buttonId,
screenId: context.screenId,
userId: context.userId,
companyCode: context.companyCode,
};
// 3. 관계 실행
return await EventTriggerService.executeSpecificRelationship(
relationship,
contextData,
context.companyCode
);
}
/**
* 🔥 외부호출 실행
*/
private static async executeExternalCall(
config: ExternalCallConfig,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
// 1. 외부호출 설정 조회
const externalCallConfig = await ExternalCallConfigAPI.getConfigById(
config.configId
);
// 2. 데이터 매핑 처리
let mappedData = formData;
if (config.dataMappingEnabled && externalCallConfig.dataMappingConfig) {
mappedData = await DataMappingService.processOutboundData(
externalCallConfig.dataMappingConfig.outboundMapping,
formData
);
}
// 3. 외부 API 호출
const callResult = await ExternalCallService.executeWithDataMapping(
externalCallConfig.configData,
externalCallConfig.dataMappingConfig,
mappedData
);
// 4. 응답 데이터 처리 (Inbound 매핑)
if (
callResult.success &&
config.dataMappingEnabled &&
externalCallConfig.dataMappingConfig?.direction === "inbound"
) {
await DataMappingService.processInboundData(
callResult.response,
externalCallConfig.dataMappingConfig.inboundMapping!
);
}
return {
success: callResult.success,
message: callResult.success ? "외부호출 성공" : callResult.error,
executionTime: callResult.executionTime,
data: callResult,
};
}
}
```
### 5. UI/UX 개선 사항
#### 5.1 버튼 설정 패널 개선
```typescript
// 🟢 단순화된 제어 설정 UI
const ButtonControlConfigPanel = () => {
return (
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<Tabs value={controlType} onValueChange={setControlType}>
<TabsList>
<TabsTrigger value="none"> </TabsTrigger>
<TabsTrigger value="relationship"> </TabsTrigger>
<TabsTrigger value="external_call"> </TabsTrigger>
</TabsList>
<TabsContent value="relationship">
<RelationshipSelector
selectedRelationshipId={config.relationshipId}
onSelect={handleRelationshipSelect}
/>
</TabsContent>
<TabsContent value="external_call">
<ExternalCallSelector
selectedConfigId={config.externalCallConfigId}
onSelect={handleExternalCallSelect}
/>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
};
```
#### 5.2 관계 선택 컴포넌트
```typescript
const RelationshipSelector = ({ onSelect }) => {
const [relationships, setRelationships] = useState([]);
useEffect(() => {
// 전체 관계 목록 로드 (관계도별 구분 없이)
loadAllRelationships();
}, []);
return (
<div className="space-y-4">
<Label> </Label>
<Select onValueChange={onSelect}>
<SelectTrigger>
<SelectValue placeholder="관계를 선택하세요" />
</SelectTrigger>
<SelectContent>
{relationships.map((rel) => (
<SelectItem key={rel.id} value={rel.id}>
<div className="flex flex-col">
<span className="font-medium">{rel.name}</span>
<span className="text-xs text-muted-foreground">
{rel.sourceTable} {rel.targetTable}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
```
### 6. 구현 우선순위
#### Phase 1: 명칭 변경 (1일)
1. **UI 텍스트 변경**: "관계도" → "관계"
2. **변수명 정리**: `diagram``relationship` 관련
3. **API 엔드포인트 정리**: 일관성 있는 명명
#### Phase 2: 버튼 제어 로직 개선 (2-3일)
1. **ButtonDataflowConfig 타입 개선**
2. **RelationshipSelector 컴포넌트 개발**
3. **ExternalCallSelector 컴포넌트 개발**
4. **ButtonActionExecutor 로직 개선**
#### Phase 3: 외부호출 통합 (1-2일)
1. **외부호출 설정 참조 방식 개선**
2. **데이터 매핑 통합**
3. **실행 플로우 최적화**
#### Phase 4: 테스트 및 최적화 (1일)
1. **전체 플로우 테스트**
2. **성능 최적화**
3. **사용자 가이드 업데이트**
### 7. 기대 효과
#### 7.1 사용자 경험 개선
- **직관적인 용어**: "관계도" → "관계"로 이해도 향상
- **단순화된 설정**: 복잡한 관계도 탐색 → 직접 관계 선택
- **통합된 제어**: 관계 실행과 외부호출을 동일한 방식으로 관리
#### 7.2 개발 효율성 향상
- **명확한 책임 분리**: 관계 관리와 외부호출 관리 분리
- **재사용성 증대**: 외부호출 설정의 재사용성 향상
- **유지보수성 개선**: 단순화된 로직으로 디버깅 용이
#### 7.3 시스템 확장성
- **새로운 제어 타입 추가 용이**: 플러그인 방식으로 확장 가능
- **데이터 매핑 시스템 완전 활용**: 외부 시스템과의 유연한 연동
- **모니터링 및 로깅 강화**: 각 단계별 상세한 실행 로그
이 개선 계획을 통해 제어관리 시스템이 더욱 직관적이고 강력한 기능을 제공할 수 있을 것입니다.
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More