Files
vexplor/docs/FLOW_DATA_STRUCTURE_GUIDE.md
2025-10-20 15:53:00 +09:00

8.1 KiB

플로우 데이터 구조 설계 가이드

개요

플로우 관리 시스템에서 각 단계별로 테이블 구조가 다른 경우의 데이터 관리 방법

추천 아키텍처: 하이브리드 접근

1. 메인 데이터 테이블 (상태 기반)

각 플로우의 핵심 데이터를 담는 메인 테이블에 flow_status 컬럼을 추가합니다.

-- 예시: 제품 수명주기 관리
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. 단계별 상세 정보 테이블 (선택적)

각 단계에서 필요한 상세 정보는 별도 테이블에 저장합니다.

-- 구매 단계 상세 정보
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 테이블에 단계별 필드 매핑 정보를 추가합니다.

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');

데이터 이동 로직

백엔드 서비스 수정

// backend-node/src/services/flowDataMoveService.ts

export class FlowDataMoveService {
  /**
   * 다음 단계로 데이터 이동
   */
  async moveToNextStep(
    flowId: number,
    currentStepId: number,
    nextStepId: number,
    dataId: any
  ): Promise<boolean> {
    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 컬럼을 기준으로 설정합니다:

// 구매 단계 조건
{
  "operator": "AND",
  "conditions": [
    {
      "column": "flow_status",
      "operator": "=",
      "value": "purchase"
    }
  ]
}

// 설치 단계 조건
{
  "operator": "AND",
  "conditions": [
    {
      "column": "flow_status",
      "operator": "=",
      "value": "installation"
    }
  ]
}

프론트엔드 구현

단계별 폼 렌더링

각 단계에서 필요한 필드를 동적으로 렌더링합니다.

// 단계 정보에서 필수 필드 가져오기
const requiredFields = step.required_fields; // ["purchase_date", "purchase_price"]

// 동적 폼 생성
{
  requiredFields.map((fieldName) => (
    <FormField
      key={fieldName}
      name={fieldName}
      label={getFieldLabel(fieldName)}
      type={getFieldType(fieldName)}
      required={true}
    />
  ));
}

장점

  1. 단순한 데이터 이동: 상태값만 업데이트
  2. 유연한 구조: 단계별 상세 정보는 별도 테이블
  3. 완벽한 이력 추적: 감사 로그로 모든 이동 기록
  4. 쿼리 효율: 단일 테이블 조회로 각 단계 데이터 확인
  5. 확장성: 새로운 단계 추가 시 컬럼 추가 또는 상세 테이블 생성

마이그레이션 스크립트

-- 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로 구분
  • 단계별 추가 정보는 별도 상세 테이블에 저장 (선택적)
  • 데이터 이동은 상태값 업데이트만으로 간단하게 처리
  • 완전한 감사 로그와 이력 추적 가능