# 플로우 하이브리드 모드 사용 가이드 ## 개요 플로우 관리 시스템은 세 가지 데이터 이동 방식을 지원합니다: 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 = ?)); ``` ## 결론 하이브리드 플로우 시스템은 다양한 비즈니스 요구사항에 유연하게 대응할 수 있습니다: - 간단한 상태 관리부터 - 복잡한 다단계 프로세스까지 - 하나의 시스템으로 통합 관리 가능