303 lines
8.1 KiB
Markdown
303 lines
8.1 KiB
Markdown
|
|
# 플로우 데이터 구조 설계 가이드
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
플로우 관리 시스템에서 각 단계별로 테이블 구조가 다른 경우의 데이터 관리 방법
|
||
|
|
|
||
|
|
## 추천 아키텍처: 하이브리드 접근
|
||
|
|
|
||
|
|
### 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<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` 컬럼을 기준으로 설정합니다:
|
||
|
|
|
||
|
|
```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) => (
|
||
|
|
<FormField
|
||
|
|
key={fieldName}
|
||
|
|
name={fieldName}
|
||
|
|
label={getFieldLabel(fieldName)}
|
||
|
|
type={getFieldType(fieldName)}
|
||
|
|
required={true}
|
||
|
|
/>
|
||
|
|
));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 장점
|
||
|
|
|
||
|
|
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`로 구분
|
||
|
|
- 단계별 추가 정보는 별도 상세 테이블에 저장 (선택적)
|
||
|
|
- 데이터 이동은 상태값 업데이트만으로 간단하게 처리
|
||
|
|
- 완전한 감사 로그와 이력 추적 가능
|