# 플로우 하이브리드 모드 사용 가이드

## 개요

플로우 관리 시스템은 세 가지 데이터 이동 방식을 지원합니다:

1. **상태 변경 방식(status)**: 같은 테이블 내에서 상태 컬럼만 업데이트
2. **테이블 이동 방식(table)**: 완전히 다른 테이블로 데이터 복사 및 이동
3. **하이브리드 방식(both)**: 두 가지 모두 수행

## 1. 상태 변경 방식 (Status Mode)

### 사용 시나리오

- 같은 엔티티가 여러 단계를 거치는 경우
- 예: 승인 프로세스 (대기 → 검토 → 승인 → 완료)

### 설정 방법

```sql
-- 플로우 정의 생성
INSERT INTO flow_definition (name, description, table_name, is_active)
VALUES ('문서 승인', '문서 승인 프로세스', 'documents', true);

-- 단계 생성 (상태 변경 방식)
INSERT INTO flow_step (
  flow_definition_id, step_name, step_order,
  table_name, move_type, status_column, status_value,
  condition_json
) VALUES
(1, '대기', 1, 'documents', 'status', 'approval_status', 'pending',
 '{"operator":"AND","conditions":[{"column":"approval_status","operator":"=","value":"pending"}]}'::jsonb),

(1, '검토중', 2, 'documents', 'status', 'approval_status', 'reviewing',
 '{"operator":"AND","conditions":[{"column":"approval_status","operator":"=","value":"reviewing"}]}'::jsonb),

(1, '승인됨', 3, 'documents', 'status', 'approval_status', 'approved',
 '{"operator":"AND","conditions":[{"column":"approval_status","operator":"=","value":"approved"}]}'::jsonb);
```

### 테이블 구조

```sql
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200),
  content TEXT,
  approval_status VARCHAR(50) DEFAULT 'pending', -- 상태 컬럼
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
```

### 데이터 이동

```typescript
// 프론트엔드에서 호출
await moveData(flowId, currentStepId, nextStepId, documentId);

// 백엔드에서 처리
// documents 테이블의 approval_status가 'pending' → 'reviewing'으로 변경됨
```

## 2. 테이블 이동 방식 (Table Mode)

### 사용 시나리오

- 완전히 다른 엔티티를 다루는 경우
- 예: 제품 수명주기 (구매 주문 → 설치 작업 → 폐기 신청)

### 설정 방법

```sql
-- 플로우 정의 생성
INSERT INTO flow_definition (name, description, table_name, is_active)
VALUES ('제품 수명주기', '구매→설치→폐기 프로세스', 'purchase_orders', true);

-- 단계 생성 (테이블 이동 방식)
INSERT INTO flow_step (
  flow_definition_id, step_name, step_order,
  table_name, move_type, target_table,
  field_mappings, required_fields
) VALUES
(2, '구매', 1, 'purchase_orders', 'table', 'installations',
 '{"order_id":"purchase_order_id","product_name":"product_name","product_code":"product_code"}'::jsonb,
 '["product_name","purchase_date","purchase_price"]'::jsonb),

(2, '설치', 2, 'installations', 'table', 'disposals',
 '{"installation_id":"installation_id","product_name":"product_name","product_code":"product_code"}'::jsonb,
 '["installation_date","installation_location","technician"]'::jsonb),

(2, '폐기', 3, 'disposals', 'table', NULL,
 NULL,
 '["disposal_date","disposal_method","disposal_cost"]'::jsonb);
```

### 테이블 구조

```sql
-- 단계 1: 구매 주문 테이블
CREATE TABLE purchase_orders (
  id SERIAL PRIMARY KEY,
  order_id VARCHAR(50) UNIQUE,
  product_name VARCHAR(200),
  product_code VARCHAR(50),
  purchase_date DATE,
  purchase_price DECIMAL(15,2),
  vendor_name VARCHAR(200),
  created_at TIMESTAMP DEFAULT NOW()
);

-- 단계 2: 설치 작업 테이블
CREATE TABLE installations (
  id SERIAL PRIMARY KEY,
  purchase_order_id VARCHAR(50), -- 매핑 필드
  product_name VARCHAR(200),
  product_code VARCHAR(50),
  installation_date DATE,
  installation_location TEXT,
  technician VARCHAR(100),
  created_at TIMESTAMP DEFAULT NOW()
);

-- 단계 3: 폐기 신청 테이블
CREATE TABLE disposals (
  id SERIAL PRIMARY KEY,
  installation_id INTEGER, -- 매핑 필드
  product_name VARCHAR(200),
  product_code VARCHAR(50),
  disposal_date DATE,
  disposal_method VARCHAR(100),
  disposal_cost DECIMAL(15,2),
  created_at TIMESTAMP DEFAULT NOW()
);
```

### 데이터 이동

```typescript
// 구매 → 설치 단계로 이동
const result = await moveData(
  flowId,
  purchaseStepId,
  installationStepId,
  purchaseOrderId
);

// 결과:
// 1. purchase_orders 테이블에서 데이터 조회
// 2. field_mappings에 따라 필드 매핑
// 3. installations 테이블에 새 레코드 생성
// 4. flow_data_mapping 테이블에 매핑 정보 저장
// 5. flow_audit_log에 이동 이력 기록
```

### 매핑 정보 조회

```sql
-- 플로우 전체 이력 조회
SELECT * FROM flow_data_mapping
WHERE flow_definition_id = 2;

-- 결과 예시:
-- {
--   "current_step_id": 2,
--   "step_data_map": {
--     "1": "123",  -- 구매 주문 ID
--     "2": "456"   -- 설치 작업 ID
--   }
-- }
```

## 3. 하이브리드 방식 (Both Mode)

### 사용 시나리오

- 상태도 변경하면서 다른 테이블로도 이동해야 하는 경우
- 예: 검토 완료 후 승인 테이블로 이동하면서 원본 테이블의 상태도 변경

### 설정 방법

```sql
INSERT INTO flow_step (
  flow_definition_id, step_name, step_order,
  table_name, move_type,
  status_column, status_value, -- 상태 변경용
  target_table, field_mappings, -- 테이블 이동용
  required_fields
) VALUES
(3, '검토 완료', 1, 'review_queue', 'both',
 'status', 'reviewed',
 'approved_items',
 '{"item_id":"source_item_id","item_name":"name","review_score":"score"}'::jsonb,
 '["review_date","reviewer_id","review_comment"]'::jsonb);
```

### 동작

1. **상태 변경**: review_queue 테이블의 status를 'reviewed'로 업데이트
2. **테이블 이동**: approved_items 테이블에 새 레코드 생성
3. **매핑 저장**: flow_data_mapping에 양쪽 ID 기록

## 4. 프론트엔드 구현

### FlowWidget에서 데이터 이동

```typescript
// frontend/components/screen/widgets/FlowWidget.tsx

const handleMoveToNext = async () => {
  // ... 선택된 데이터 준비 ...

  for (const data of selectedData) {
    // Primary Key 추출 (첫 번째 컬럼 또는 'id' 컬럼)
    const dataId = data.id || data[stepDataColumns[0]];

    // API 호출
    const response = await moveData(flowId, currentStepId, nextStepId, dataId);

    if (!response.success) {
      toast.error(`이동 실패: ${response.message}`);
      continue;
    }

    // 성공 시 targetDataId 확인 가능
    if (response.data?.targetDataId) {
      console.log(`새 테이블 ID: ${response.data.targetDataId}`);
    }
  }

  // 데이터 새로고침
  await refreshStepData();
};
```

### 추가 데이터 전달

```typescript
// 다음 단계로 이동하면서 추가 데이터 입력
const additionalData = {
  installation_date: "2025-10-20",
  technician: "John Doe",
  installation_notes: "Installed successfully",
};

const response = await fetch(`/api/flow/${flowId}/move`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    fromStepId: currentStepId,
    toStepId: nextStepId,
    dataId: dataId,
    additionalData: additionalData,
  }),
});
```

## 5. 감사 로그 조회

### 특정 데이터의 이력 조회

```typescript
const auditLogs = await getFlowAuditLogs(flowId, dataId);

// 결과:
[
  {
    id: 1,
    moveType: "table",
    sourceTable: "purchase_orders",
    targetTable: "installations",
    sourceDataId: "123",
    targetDataId: "456",
    fromStepName: "구매",
    toStepName: "설치",
    changedBy: "system",
    changedAt: "2025-10-20T10:30:00",
  },
  {
    id: 2,
    moveType: "table",
    sourceTable: "installations",
    targetTable: "disposals",
    sourceDataId: "456",
    targetDataId: "789",
    fromStepName: "설치",
    toStepName: "폐기",
    changedBy: "user123",
    changedAt: "2025-10-21T14:20:00",
  },
];
```

## 6. 모범 사례

### 상태 변경 방식 사용 시

✅ **권장**:

- 단일 엔티티의 생명주기 관리
- 간단한 승인 프로세스
- 빠른 상태 조회가 필요한 경우

❌ **비권장**:

- 각 단계마다 완전히 다른 데이터 구조가 필요한 경우

### 테이블 이동 방식 사용 시

✅ **권장**:

- 각 단계가 독립적인 엔티티
- 단계별로 다른 팀/부서에서 관리
- 각 단계의 데이터 구조가 완전히 다른 경우

❌ **비권장**:

- 단순한 상태 변경만 필요한 경우 (오버엔지니어링)
- 실시간 조회 성능이 중요한 경우 (JOIN 비용)

### 하이브리드 방식 사용 시

✅ **권장**:

- 원본 데이터는 보존하면서 처리된 데이터는 별도 저장
- 이중 추적이 필요한 경우

## 7. 주의사항

1. **필드 매핑 주의**: `field_mappings`의 소스/타겟 필드가 정확해야 함
2. **필수 필드 검증**: `required_fields`에 명시된 필드는 반드시 입력
3. **트랜잭션**: 모든 이동은 트랜잭션으로 처리되어 원자성 보장
4. **Primary Key**: 테이블 이동 시 소스 데이터의 Primary Key가 명확해야 함
5. **순환 참조 방지**: 플로우 연결 시 사이클이 발생하지 않도록 주의

## 8. 트러블슈팅

### Q1: "데이터를 찾을 수 없습니다" 오류

- 원인: Primary Key가 잘못되었거나 데이터가 이미 이동됨
- 해결: `flow_audit_log`에서 이동 이력 확인

### Q2: "매핑할 데이터가 없습니다" 오류

- 원인: `field_mappings`가 비어있거나 소스 필드가 없음
- 해결: 소스 테이블에 매핑 필드가 존재하는지 확인

### Q3: 테이블 이동 후 원본 데이터 처리

- 원본 데이터는 자동으로 삭제되지 않음
- 필요시 별도 로직으로 처리하거나 `is_archived` 플래그 사용

## 9. 성능 최적화

1. **인덱스 생성**: 상태 컬럼에 인덱스 필수

```sql
CREATE INDEX idx_documents_status ON documents(approval_status);
```

2. **배치 이동**: 대량 데이터는 배치 API 사용

```typescript
await moveBatchData(flowId, fromStepId, toStepId, dataIds);
```

3. **매핑 테이블 정리**: 주기적으로 완료된 플로우의 매핑 데이터 아카이빙

```sql
DELETE FROM flow_data_mapping
WHERE created_at < NOW() - INTERVAL '1 year'
AND current_step_id IN (SELECT id FROM flow_step WHERE step_order = (SELECT MAX(step_order) FROM flow_step WHERE flow_definition_id = ?));
```

## 결론

하이브리드 플로우 시스템은 다양한 비즈니스 요구사항에 유연하게 대응할 수 있습니다:

- 간단한 상태 관리부터
- 복잡한 다단계 프로세스까지
- 하나의 시스템으로 통합 관리 가능
