# 플로우 데이터 구조 설계 가이드 ## 개요 플로우 관리 시스템에서 각 단계별로 테이블 구조가 다른 경우의 데이터 관리 방법 ## 추천 아키텍처: 하이브리드 접근 ### 1. 메인 데이터 테이블 (상태 기반) 각 플로우의 핵심 데이터를 담는 메인 테이블에 `flow_status` 컬럼을 추가합니다. ```sql -- 예시: 제품 수명주기 관리 CREATE TABLE product_lifecycle ( id SERIAL PRIMARY KEY, product_code VARCHAR(50) UNIQUE NOT NULL, product_name VARCHAR(200) NOT NULL, flow_status VARCHAR(50) NOT NULL, -- 'purchase', 'installation', 'disposal' -- 공통 필드 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by VARCHAR(50), -- 단계별 핵심 정보 (NULL 허용) purchase_date DATE, purchase_price DECIMAL(15,2), installation_date DATE, installation_location VARCHAR(200), disposal_date DATE, disposal_method VARCHAR(100), -- 인덱스 INDEX idx_flow_status (flow_status), INDEX idx_product_code (product_code) ); ``` ### 2. 단계별 상세 정보 테이블 (선택적) 각 단계에서 필요한 상세 정보는 별도 테이블에 저장합니다. ```sql -- 구매 단계 상세 정보 CREATE TABLE product_purchase_detail ( id SERIAL PRIMARY KEY, product_id INTEGER REFERENCES product_lifecycle(id), vendor_name VARCHAR(200), vendor_contact VARCHAR(100), purchase_order_no VARCHAR(50), warranty_period INTEGER, -- 월 단위 warranty_end_date DATE, specifications JSONB, -- 유연한 사양 정보 created_at TIMESTAMP DEFAULT NOW() ); -- 설치 단계 상세 정보 CREATE TABLE product_installation_detail ( id SERIAL PRIMARY KEY, product_id INTEGER REFERENCES product_lifecycle(id), technician_name VARCHAR(100), installation_address TEXT, installation_notes TEXT, installation_photos JSONB, -- [{url, description}] created_at TIMESTAMP DEFAULT NOW() ); -- 폐기 단계 상세 정보 CREATE TABLE product_disposal_detail ( id SERIAL PRIMARY KEY, product_id INTEGER REFERENCES product_lifecycle(id), disposal_company VARCHAR(200), disposal_certificate_no VARCHAR(100), environmental_compliance BOOLEAN, disposal_cost DECIMAL(15,2), created_at TIMESTAMP DEFAULT NOW() ); ``` ### 3. 플로우 단계 설정 테이블 수정 `flow_step` 테이블에 단계별 필드 매핑 정보를 추가합니다. ```sql ALTER TABLE flow_step ADD COLUMN status_value VARCHAR(50), -- 이 단계의 상태값 ADD COLUMN required_fields JSONB, -- 필수 입력 필드 목록 ADD COLUMN detail_table_name VARCHAR(200), -- 상세 정보 테이블명 (선택적) ADD COLUMN field_mappings JSONB; -- 메인 테이블과 상세 테이블 필드 매핑 -- 예시 데이터 INSERT INTO flow_step (flow_definition_id, step_name, step_order, table_name, status_value, required_fields, detail_table_name) VALUES (1, '구매', 1, 'product_lifecycle', 'purchase', '["product_code", "product_name", "purchase_date", "purchase_price"]'::jsonb, 'product_purchase_detail'), (1, '설치', 2, 'product_lifecycle', 'installation', '["installation_date", "installation_location"]'::jsonb, 'product_installation_detail'), (1, '폐기', 3, 'product_lifecycle', 'disposal', '["disposal_date", "disposal_method"]'::jsonb, 'product_disposal_detail'); ``` ## 데이터 이동 로직 ### 백엔드 서비스 수정 ```typescript // backend-node/src/services/flowDataMoveService.ts export class FlowDataMoveService { /** * 다음 단계로 데이터 이동 */ async moveToNextStep( flowId: number, currentStepId: number, nextStepId: number, dataId: any ): Promise { const client = await db.getClient(); try { await client.query("BEGIN"); // 1. 현재 단계와 다음 단계 정보 조회 const currentStep = await this.getStepInfo(currentStepId); const nextStep = await this.getStepInfo(nextStepId); if (!currentStep || !nextStep) { throw new Error("유효하지 않은 단계입니다"); } // 2. 메인 테이블의 상태 업데이트 const updateQuery = ` UPDATE ${currentStep.table_name} SET flow_status = $1, updated_at = NOW() WHERE id = $2 AND flow_status = $3 `; const result = await client.query(updateQuery, [ nextStep.status_value, dataId, currentStep.status_value, ]); if (result.rowCount === 0) { throw new Error("데이터를 찾을 수 없거나 이미 이동되었습니다"); } // 3. 감사 로그 기록 await this.logDataMove(client, { flowId, fromStepId: currentStepId, toStepId: nextStepId, dataId, tableName: currentStep.table_name, statusFrom: currentStep.status_value, statusTo: nextStep.status_value, }); await client.query("COMMIT"); return true; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } private async getStepInfo(stepId: number) { const query = ` SELECT id, table_name, status_value, detail_table_name, required_fields FROM flow_step WHERE id = $1 `; const result = await db.query(query, [stepId]); return result[0]; } private async logDataMove(client: any, params: any) { const query = ` INSERT INTO flow_audit_log ( flow_definition_id, from_step_id, to_step_id, data_id, table_name, status_from, status_to, moved_at, moved_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), 'system') `; await client.query(query, [ params.flowId, params.fromStepId, params.toStepId, params.dataId, params.tableName, params.statusFrom, params.statusTo, ]); } } ``` ## 플로우 조건 설정 각 단계의 조건은 `flow_status` 컬럼을 기준으로 설정합니다: ```json // 구매 단계 조건 { "operator": "AND", "conditions": [ { "column": "flow_status", "operator": "=", "value": "purchase" } ] } // 설치 단계 조건 { "operator": "AND", "conditions": [ { "column": "flow_status", "operator": "=", "value": "installation" } ] } ``` ## 프론트엔드 구현 ### 단계별 폼 렌더링 각 단계에서 필요한 필드를 동적으로 렌더링합니다. ```typescript // 단계 정보에서 필수 필드 가져오기 const requiredFields = step.required_fields; // ["purchase_date", "purchase_price"] // 동적 폼 생성 { requiredFields.map((fieldName) => ( )); } ``` ## 장점 1. **단순한 데이터 이동**: 상태값만 업데이트 2. **유연한 구조**: 단계별 상세 정보는 별도 테이블 3. **완벽한 이력 추적**: 감사 로그로 모든 이동 기록 4. **쿼리 효율**: 단일 테이블 조회로 각 단계 데이터 확인 5. **확장성**: 새로운 단계 추가 시 컬럼 추가 또는 상세 테이블 생성 ## 마이그레이션 스크립트 ```sql -- 1. 기존 테이블에 flow_status 컬럼 추가 ALTER TABLE product_lifecycle ADD COLUMN flow_status VARCHAR(50) DEFAULT 'purchase'; -- 2. 인덱스 생성 CREATE INDEX idx_product_lifecycle_status ON product_lifecycle(flow_status); -- 3. flow_step 테이블 확장 ALTER TABLE flow_step ADD COLUMN status_value VARCHAR(50), ADD COLUMN required_fields JSONB, ADD COLUMN detail_table_name VARCHAR(200); -- 4. 기존 데이터 마이그레이션 UPDATE flow_step SET status_value = CASE step_order WHEN 1 THEN 'purchase' WHEN 2 THEN 'installation' WHEN 3 THEN 'disposal' END WHERE flow_definition_id = 1; ``` ## 결론 이 하이브리드 접근 방식을 사용하면: - 각 단계의 데이터는 같은 메인 테이블에서 `flow_status`로 구분 - 단계별 추가 정보는 별도 상세 테이블에 저장 (선택적) - 데이터 이동은 상태값 업데이트만으로 간단하게 처리 - 완전한 감사 로그와 이력 추적 가능