ERP-node/PHASE_FLOW_MANAGEMENT_SYSTE...

1139 lines
29 KiB
Markdown
Raw Normal View History

2025-10-20 10:55:33 +09:00
# 플로우 관리 시스템 구현 계획서
## 1. 개요
### 1.1 목적
제품의 수명주기나 업무 프로세스를 시각적인 플로우로 정의하고 관리하는 시스템을 구축합니다.
각 플로우 단계는 데이터베이스 테이블의 레코드 조건으로 정의되며, 데이터를 플로우 단계 간 이동시키고 이력을 관리할 수 있습니다.
### 1.2 주요 기능
- 플로우 정의 및 시각적 편집
- 플로우 단계별 조건 설정
- 화면관리 시스템과 연동하여 플로우 위젯 배치
- 플로우 단계별 데이터 카운트 및 리스트 조회
- 데이터의 플로우 단계 이동
- 상태 변경 이력 관리 (오딧 로그)
### 1.3 사용 예시
**DTG 제품 수명주기 관리**
- 플로우 이름: "DTG 제품 라이프사이클"
- 연결 테이블: `product_dtg`
- 플로우 단계:
1. 구매 (조건: `status = '구매완료' AND install_date IS NULL`)
2. 설치 (조건: `status = '설치완료' AND disposal_date IS NULL`)
3. 폐기 (조건: `status = '폐기완료'`)
---
## 2. 데이터베이스 스키마
### 2.1 flow_definition (플로우 정의)
```sql
CREATE TABLE flow_definition (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL, -- 플로우 이름
description TEXT, -- 플로우 설명
table_name VARCHAR(200) NOT NULL, -- 연결된 테이블명
is_active BOOLEAN DEFAULT true, -- 활성화 여부
created_by VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_flow_definition_table ON flow_definition(table_name);
```
### 2.2 flow_step (플로우 단계)
```sql
CREATE TABLE flow_step (
id SERIAL PRIMARY KEY,
flow_definition_id INTEGER NOT NULL REFERENCES flow_definition(id) ON DELETE CASCADE,
step_name VARCHAR(200) NOT NULL, -- 단계 이름
step_order INTEGER NOT NULL, -- 단계 순서
condition_json JSONB, -- 조건 설정 (JSON 형태)
color VARCHAR(50) DEFAULT '#3B82F6', -- 단계 표시 색상
position_x INTEGER DEFAULT 0, -- 캔버스 상의 X 좌표
position_y INTEGER DEFAULT 0, -- 캔버스 상의 Y 좌표
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_flow_step_definition ON flow_step(flow_definition_id);
```
**condition_json 예시:**
```json
{
"type": "AND",
"conditions": [
{
"column": "status",
"operator": "equals",
"value": "구매완료"
},
{
"column": "install_date",
"operator": "is_null",
"value": null
}
]
}
```
### 2.3 flow_step_connection (플로우 단계 연결)
```sql
CREATE TABLE flow_step_connection (
id SERIAL PRIMARY KEY,
flow_definition_id INTEGER NOT NULL REFERENCES flow_definition(id) ON DELETE CASCADE,
from_step_id INTEGER NOT NULL REFERENCES flow_step(id) ON DELETE CASCADE,
to_step_id INTEGER NOT NULL REFERENCES flow_step(id) ON DELETE CASCADE,
label VARCHAR(200), -- 연결선 라벨 (선택사항)
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_flow_connection_definition ON flow_step_connection(flow_definition_id);
```
### 2.4 flow_data_status (데이터의 현재 플로우 상태)
```sql
CREATE TABLE flow_data_status (
id SERIAL PRIMARY KEY,
flow_definition_id INTEGER NOT NULL REFERENCES flow_definition(id) ON DELETE CASCADE,
table_name VARCHAR(200) NOT NULL, -- 원본 테이블명
record_id VARCHAR(100) NOT NULL, -- 원본 테이블의 레코드 ID
current_step_id INTEGER REFERENCES flow_step(id) ON DELETE SET NULL,
updated_by VARCHAR(100),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(flow_definition_id, table_name, record_id)
);
CREATE INDEX idx_flow_data_status_record ON flow_data_status(table_name, record_id);
CREATE INDEX idx_flow_data_status_step ON flow_data_status(current_step_id);
```
### 2.5 flow_audit_log (플로우 상태 변경 이력)
```sql
CREATE TABLE flow_audit_log (
id SERIAL PRIMARY KEY,
flow_definition_id INTEGER NOT NULL REFERENCES flow_definition(id) ON DELETE CASCADE,
table_name VARCHAR(200) NOT NULL,
record_id VARCHAR(100) NOT NULL,
from_step_id INTEGER REFERENCES flow_step(id) ON DELETE SET NULL,
to_step_id INTEGER REFERENCES flow_step(id) ON DELETE SET NULL,
changed_by VARCHAR(100),
changed_at TIMESTAMP DEFAULT NOW(),
note TEXT -- 변경 사유
);
CREATE INDEX idx_flow_audit_record ON flow_audit_log(table_name, record_id, changed_at DESC);
CREATE INDEX idx_flow_audit_definition ON flow_audit_log(flow_definition_id);
```
---
## 3. 백엔드 API 설계
### 3.1 플로우 정의 관리 API
#### 3.1.1 플로우 생성
```
POST /api/flow/definitions
Body: {
name: string;
description?: string;
tableName: string;
}
Response: { success: boolean; data: FlowDefinition }
```
#### 3.1.2 플로우 목록 조회
```
GET /api/flow/definitions
Query: { tableName?: string; isActive?: boolean }
Response: { success: boolean; data: FlowDefinition[] }
```
#### 3.1.3 플로우 상세 조회
```
GET /api/flow/definitions/:id
Response: {
success: boolean;
data: {
definition: FlowDefinition;
steps: FlowStep[];
connections: FlowStepConnection[];
}
}
```
#### 3.1.4 플로우 수정
```
PUT /api/flow/definitions/:id
Body: { name?: string; description?: string; isActive?: boolean }
Response: { success: boolean; data: FlowDefinition }
```
#### 3.1.5 플로우 삭제
```
DELETE /api/flow/definitions/:id
Response: { success: boolean }
```
### 3.2 플로우 단계 관리 API
#### 3.2.1 단계 추가
```
POST /api/flow/definitions/:flowId/steps
Body: {
stepName: string;
stepOrder: number;
conditionJson?: object;
color?: string;
positionX?: number;
positionY?: number;
}
Response: { success: boolean; data: FlowStep }
```
#### 3.2.2 단계 수정
```
PUT /api/flow/steps/:stepId
Body: {
stepName?: string;
stepOrder?: number;
conditionJson?: object;
color?: string;
positionX?: number;
positionY?: number;
}
Response: { success: boolean; data: FlowStep }
```
#### 3.2.3 단계 삭제
```
DELETE /api/flow/steps/:stepId
Response: { success: boolean }
```
### 3.3 플로우 연결 관리 API
#### 3.3.1 단계 연결 생성
```
POST /api/flow/connections
Body: {
flowDefinitionId: number;
fromStepId: number;
toStepId: number;
label?: string;
}
Response: { success: boolean; data: FlowStepConnection }
```
#### 3.3.2 연결 삭제
```
DELETE /api/flow/connections/:connectionId
Response: { success: boolean }
```
### 3.4 플로우 실행 API
#### 3.4.1 단계별 데이터 카운트 조회
```
GET /api/flow/:flowId/step/:stepId/count
Response: {
success: boolean;
data: { count: number }
}
```
#### 3.4.2 단계별 데이터 리스트 조회
```
GET /api/flow/:flowId/step/:stepId/data
Query: { page?: number; pageSize?: number }
Response: {
success: boolean;
data: {
records: any[];
total: number;
page: number;
pageSize: number;
}
}
```
#### 3.4.3 데이터를 다음 단계로 이동
```
POST /api/flow/move
Body: {
flowId: number;
recordId: string;
toStepId: number;
note?: string;
}
Response: { success: boolean }
```
#### 3.4.4 데이터의 플로우 이력 조회
```
GET /api/flow/audit/:flowId/:recordId
Response: {
success: boolean;
data: FlowAuditLog[]
}
```
---
## 4. 프론트엔드 구조
### 4.1 플로우 관리 화면 (`/flow-management`)
#### 4.1.1 파일 구조
```
frontend/src/app/flow-management/
├── page.tsx # 메인 페이지
├── components/
│ ├── FlowList.tsx # 플로우 목록
│ ├── FlowEditor.tsx # 플로우 편집기 (React Flow)
│ ├── FlowStepPanel.tsx # 단계 속성 편집 패널
│ ├── FlowConditionBuilder.tsx # 조건 설정 빌더
│ └── FlowPreview.tsx # 플로우 미리보기
```
#### 4.1.2 주요 컴포넌트
**FlowEditor.tsx**
- React Flow 라이브러리 사용
- 플로우 단계를 노드로 표시
- 단계 간 연결선 표시
- 드래그앤드롭으로 노드 위치 조정
- 노드 클릭 시 FlowStepPanel 표시
**FlowConditionBuilder.tsx**
- 테이블 컬럼 선택
- 연산자 선택 (equals, not_equals, in, not_in, greater_than, less_than, is_null, is_not_null)
- 값 입력
- AND/OR 조건 그룹핑
- 조건 추가/제거
### 4.2 화면관리 연동
#### 4.2.1 새로운 컴포넌트 타입 추가
**types/screen.ts에 추가:**
```typescript
export interface FlowWidgetComponent extends BaseComponent {
type: "flow-widget";
flowId?: number;
layout?: "horizontal" | "vertical";
cardWidth?: string;
cardHeight?: string;
showCount?: boolean;
showConnections?: boolean;
}
export type ComponentData =
| ContainerComponent
| WidgetComponent
| GroupComponent
| DataTableComponent
| ButtonComponent
| SplitPanelComponent
| RepeaterComponent
| FlowWidgetComponent; // 추가
```
#### 4.2.2 FlowWidgetConfigPanel.tsx 생성
```typescript
// 플로우 위젯 설정 패널
interface FlowWidgetConfigPanelProps {
component: FlowWidgetComponent;
onUpdateProperty: (property: string, value: any) => void;
}
export function FlowWidgetConfigPanel({
component,
onUpdateProperty,
}: FlowWidgetConfigPanelProps) {
const [flows, setFlows] = useState<FlowDefinition[]>([]);
useEffect(() => {
// 플로우 목록 불러오기
loadFlows();
}, []);
return (
<div className="space-y-4">
<div>
<Label>플로우 선택</Label>
<Select
value={component.flowId?.toString()}
onValueChange={(v) => onUpdateProperty("flowId", parseInt(v))}
>
{flows.map((flow) => (
<SelectItem key={flow.id} value={flow.id.toString()}>
{flow.name}
</SelectItem>
))}
</Select>
</div>
<div>
<Label>레이아웃</Label>
<Select
value={component.layout}
onValueChange={(v) => onUpdateProperty("layout", v)}
>
<SelectItem value="horizontal">가로</SelectItem>
<SelectItem value="vertical">세로</SelectItem>
</Select>
</div>
<div>
<Label>카드 너비</Label>
<Input
value={component.cardWidth || "200px"}
onChange={(e) => onUpdateProperty("cardWidth", e.target.value)}
/>
</div>
<div>
<Label>데이터 카운트 표시</Label>
<Checkbox
checked={component.showCount ?? true}
onCheckedChange={(checked) => onUpdateProperty("showCount", checked)}
/>
</div>
<div>
<Label>연결선 표시</Label>
<Checkbox
checked={component.showConnections ?? true}
onCheckedChange={(checked) =>
onUpdateProperty("showConnections", checked)
}
/>
</div>
</div>
);
}
```
#### 4.2.3 RealtimePreview.tsx 수정
```typescript
// flow-widget 타입 렌더링 추가
if (component.type === "flow-widget" && component.flowId) {
return <FlowWidgetPreview component={component} />;
}
```
#### 4.2.4 FlowWidgetPreview.tsx 생성
```typescript
interface FlowWidgetPreviewProps {
component: FlowWidgetComponent;
interactive?: boolean; // InteractiveScreenViewer에서 true
}
export function FlowWidgetPreview({
component,
interactive,
}: FlowWidgetPreviewProps) {
const [flowData, setFlowData] = useState<any>(null);
const [stepCounts, setStepCounts] = useState<Record<number, number>>({});
useEffect(() => {
if (component.flowId) {
loadFlowData(component.flowId);
}
}, [component.flowId]);
const loadFlowData = async (flowId: number) => {
const response = await fetch(`/api/flow/definitions/${flowId}`);
const result = await response.json();
if (result.success) {
setFlowData(result.data);
// 각 단계별 데이터 카운트 조회
loadStepCounts(result.data.steps);
}
};
const loadStepCounts = async (steps: FlowStep[]) => {
const counts: Record<number, number> = {};
for (const step of steps) {
const response = await fetch(
`/api/flow/${component.flowId}/step/${step.id}/count`
);
const result = await response.json();
if (result.success) {
counts[step.id] = result.data.count;
}
}
setStepCounts(counts);
};
const handleStepClick = async (stepId: number) => {
if (!interactive) return;
// 단계 클릭 시 데이터 리스트 모달 표시
// TODO: 구현
};
const layout = component.layout || "horizontal";
const cardWidth = component.cardWidth || "200px";
const cardHeight = component.cardHeight || "120px";
return (
<div
className={`flex gap-4 ${
layout === "vertical" ? "flex-col" : "flex-row"
}`}
>
{flowData?.steps.map((step: FlowStep, index: number) => (
<React.Fragment key={step.id}>
<Card
className="cursor-pointer hover:shadow-lg transition-shadow"
style={{
width: cardWidth,
height: cardHeight,
borderColor: step.color,
}}
onClick={() => handleStepClick(step.id)}
>
<CardHeader>
<CardTitle className="text-sm">{step.stepName}</CardTitle>
</CardHeader>
<CardContent>
{component.showCount && (
<div className="text-2xl font-bold">
{stepCounts[step.id] || 0}
</div>
)}
</CardContent>
</Card>
{component.showConnections && index < flowData.steps.length - 1 && (
<div
className={`flex items-center justify-center ${
layout === "vertical" ? "h-8" : "w-8"
}`}
>
{layout === "vertical" ? "↓" : "→"}
</div>
)}
</React.Fragment>
))}
</div>
);
}
```
### 4.3 데이터 리스트 모달
#### 4.3.1 FlowStepDataModal.tsx 생성
```typescript
interface FlowStepDataModalProps {
flowId: number;
stepId: number;
stepName: string;
isOpen: boolean;
onClose: () => void;
}
export function FlowStepDataModal({
flowId,
stepId,
stepName,
isOpen,
onClose,
}: FlowStepDataModalProps) {
const [data, setData] = useState<any[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
useEffect(() => {
if (isOpen) {
loadStepData();
}
}, [isOpen, stepId]);
const loadStepData = async () => {
const response = await fetch(`/api/flow/${flowId}/step/${stepId}/data`);
const result = await response.json();
if (result.success) {
setData(result.data.records);
}
};
const handleMoveToNextStep = async () => {
// 선택된 레코드들을 다음 단계로 이동
// TODO: 구현
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-5xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>{stepName} - 데이터 목록</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
{/* 데이터 테이블 */}
<DataTable data={data} onSelectionChange={setSelectedRows} />
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
닫기
</Button>
<Button
onClick={handleMoveToNextStep}
disabled={selectedRows.size === 0}
>
다음 단계로 이동
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
### 4.4 오딧 로그 화면
#### 4.4.1 FlowAuditLog.tsx 생성
```typescript
interface FlowAuditLogProps {
flowId: number;
recordId: string;
}
export function FlowAuditLog({ flowId, recordId }: FlowAuditLogProps) {
const [logs, setLogs] = useState<FlowAuditLog[]>([]);
useEffect(() => {
loadAuditLogs();
}, [flowId, recordId]);
const loadAuditLogs = async () => {
const response = await fetch(`/api/flow/audit/${flowId}/${recordId}`);
const result = await response.json();
if (result.success) {
setLogs(result.data);
}
};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">상태 변경 이력</h3>
<div className="space-y-2">
{logs.map((log) => (
<Card key={log.id}>
<CardContent className="pt-4">
<div className="flex items-center gap-2">
<Badge>{log.fromStepName || "시작"}</Badge>
<span></span>
<Badge variant="outline">{log.toStepName}</Badge>
</div>
<div className="text-sm text-gray-500 mt-2">
<div>변경자: {log.changedBy}</div>
<div>변경일시: {new Date(log.changedAt).toLocaleString()}</div>
{log.note && <div>변경사유: {log.note}</div>}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
```
---
## 5. 백엔드 구현
### 5.1 서비스 파일 구조
```
backend-node/src/services/
├── flowDefinitionService.ts # 플로우 정의 관리
├── flowStepService.ts # 플로우 단계 관리
├── flowConnectionService.ts # 플로우 연결 관리
├── flowExecutionService.ts # 플로우 실행 (카운트, 데이터 조회)
├── flowDataMoveService.ts # 데이터 이동 및 오딧 로그
└── flowConditionParser.ts # 조건 JSON을 SQL WHERE절로 변환
```
### 5.2 flowConditionParser.ts 핵심 로직
```typescript
export interface FlowCondition {
column: string;
operator:
| "equals"
| "not_equals"
| "in"
| "not_in"
| "greater_than"
| "less_than"
| "is_null"
| "is_not_null";
value: any;
}
export interface FlowConditionGroup {
type: "AND" | "OR";
conditions: FlowCondition[];
}
export class FlowConditionParser {
/**
* 조건 JSON을 SQL WHERE 절로 변환
*/
static toSqlWhere(conditionGroup: FlowConditionGroup): {
where: string;
params: any[];
} {
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
for (const condition of conditionGroup.conditions) {
const column = this.sanitizeColumnName(condition.column);
switch (condition.operator) {
case "equals":
conditions.push(`${column} = $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "not_equals":
conditions.push(`${column} != $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "in":
if (Array.isArray(condition.value) && condition.value.length > 0) {
const placeholders = condition.value
.map(() => `$${paramIndex++}`)
.join(", ");
conditions.push(`${column} IN (${placeholders})`);
params.push(...condition.value);
}
break;
case "not_in":
if (Array.isArray(condition.value) && condition.value.length > 0) {
const placeholders = condition.value
.map(() => `$${paramIndex++}`)
.join(", ");
conditions.push(`${column} NOT IN (${placeholders})`);
params.push(...condition.value);
}
break;
case "greater_than":
conditions.push(`${column} > $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "less_than":
conditions.push(`${column} < $${paramIndex}`);
params.push(condition.value);
paramIndex++;
break;
case "is_null":
conditions.push(`${column} IS NULL`);
break;
case "is_not_null":
conditions.push(`${column} IS NOT NULL`);
break;
}
}
const joinOperator = conditionGroup.type === "OR" ? " OR " : " AND ";
const where = conditions.length > 0 ? conditions.join(joinOperator) : "1=1";
return { where, params };
}
/**
* SQL 인젝션 방지를 위한 컬럼명 검증
*/
private static sanitizeColumnName(columnName: string): string {
// 알파벳, 숫자, 언더스코어만 허용
if (!/^[a-zA-Z0-9_]+$/.test(columnName)) {
throw new Error(`Invalid column name: ${columnName}`);
}
return columnName;
}
}
```
### 5.3 flowExecutionService.ts 핵심 로직
```typescript
export class FlowExecutionService {
/**
* 특정 플로우 단계에 해당하는 데이터 카운트
*/
async getStepDataCount(flowId: number, stepId: number): Promise<number> {
// 1. 플로우 정의 조회
const flowDef = await this.getFlowDefinition(flowId);
// 2. 플로우 단계 조회
const step = await this.getFlowStep(stepId);
// 3. 조건 JSON을 SQL WHERE절로 변환
const { where, params } = FlowConditionParser.toSqlWhere(
step.conditionJson
);
// 4. 카운트 쿼리 실행
const query = `SELECT COUNT(*) as count FROM ${flowDef.tableName} WHERE ${where}`;
const result = await db.query(query, params);
return parseInt(result.rows[0].count);
}
/**
* 특정 플로우 단계에 해당하는 데이터 리스트
*/
async getStepDataList(
flowId: number,
stepId: number,
page: number = 1,
pageSize: number = 20
): Promise<{ records: any[]; total: number }> {
const flowDef = await this.getFlowDefinition(flowId);
const step = await this.getFlowStep(stepId);
const { where, params } = FlowConditionParser.toSqlWhere(
step.conditionJson
);
const offset = (page - 1) * pageSize;
// 전체 카운트
const countQuery = `SELECT COUNT(*) as count FROM ${flowDef.tableName} WHERE ${where}`;
const countResult = await db.query(countQuery, params);
const total = parseInt(countResult.rows[0].count);
// 데이터 조회
const dataQuery = `
SELECT * FROM ${flowDef.tableName}
WHERE ${where}
ORDER BY id DESC
LIMIT $${params.length + 1} OFFSET $${params.length + 2}
`;
const dataResult = await db.query(dataQuery, [...params, pageSize, offset]);
return {
records: dataResult.rows,
total,
};
}
}
```
### 5.4 flowDataMoveService.ts 핵심 로직
```typescript
export class FlowDataMoveService {
/**
* 데이터를 다음 플로우 단계로 이동
*/
async moveDataToStep(
flowId: number,
recordId: string,
toStepId: number,
userId: string,
note?: string
): Promise<void> {
const client = await db.getClient();
try {
await client.query("BEGIN");
// 1. 현재 상태 조회
const currentStatus = await this.getCurrentStatus(
client,
flowId,
recordId
);
const fromStepId = currentStatus?.currentStepId || null;
// 2. flow_data_status 업데이트 또는 삽입
if (currentStatus) {
await client.query(
`
UPDATE flow_data_status
SET current_step_id = $1, updated_by = $2, updated_at = NOW()
WHERE flow_definition_id = $3 AND record_id = $4
`,
[toStepId, userId, flowId, recordId]
);
} else {
const flowDef = await this.getFlowDefinition(flowId);
await client.query(
`
INSERT INTO flow_data_status
(flow_definition_id, table_name, record_id, current_step_id, updated_by)
VALUES ($1, $2, $3, $4, $5)
`,
[flowId, flowDef.tableName, recordId, toStepId, userId]
);
}
// 3. 오딧 로그 기록
await client.query(
`
INSERT INTO flow_audit_log
(flow_definition_id, table_name, record_id, from_step_id, to_step_id, changed_by, note)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`,
[
flowId,
currentStatus?.tableName || recordId,
recordId,
fromStepId,
toStepId,
userId,
note,
]
);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
/**
* 데이터의 플로우 이력 조회
*/
async getAuditLogs(flowId: number, recordId: string): Promise<any[]> {
const query = `
SELECT
fal.*,
fs_from.step_name as from_step_name,
fs_to.step_name as to_step_name
FROM flow_audit_log fal
LEFT JOIN flow_step fs_from ON fal.from_step_id = fs_from.id
LEFT JOIN flow_step fs_to ON fal.to_step_id = fs_to.id
WHERE fal.flow_definition_id = $1 AND fal.record_id = $2
ORDER BY fal.changed_at DESC
`;
const result = await db.query(query, [flowId, recordId]);
return result.rows;
}
}
```
---
## 6. 구현 단계
### Phase 1: 기본 구조 (1주)
- [ ] 데이터베이스 마이그레이션 생성 및 실행
- [ ] 백엔드 서비스 기본 구조 생성
- [ ] 플로우 정의 CRUD API 구현
- [ ] 플로우 단계 CRUD API 구현
- [ ] 플로우 연결 API 구현
### Phase 2: 플로우 편집기 (1주)
- [ ] React Flow 라이브러리 설치 및 설정
- [ ] FlowEditor 컴포넌트 구현
- [ ] FlowList 컴포넌트 구현
- [ ] FlowStepPanel 구현 (단계 속성 편집)
- [ ] FlowConditionBuilder 구현 (조건 설정 UI)
- [ ] 플로우 저장/불러오기 기능
### Phase 3: 화면관리 연동 (3일)
- [ ] FlowWidgetComponent 타입 정의
- [ ] FlowWidgetConfigPanel 구현
- [ ] FlowWidgetPreview 구현
- [ ] RealtimePreview에 flow-widget 렌더링 추가
- [ ] InteractiveScreenViewer에 flow-widget 렌더링 추가
### Phase 4: 플로우 실행 기능 (1주)
- [ ] FlowConditionParser 구현 (조건 → SQL 변환)
- [ ] FlowExecutionService 구현 (카운트, 데이터 조회)
- [ ] 단계별 데이터 카운트 API
- [ ] 단계별 데이터 리스트 API
- [ ] FlowStepDataModal 구현
- [ ] 데이터 선택 및 표시 기능
### Phase 5: 데이터 이동 및 오딧 (3일)
- [ ] FlowDataMoveService 구현
- [ ] 데이터 이동 API
- [ ] 오딧 로그 API
- [ ] FlowAuditLog 컴포넌트 구현
- [ ] 데이터 이동 시 트랜잭션 처리
### Phase 6: 테스트 및 최적화 (3일)
- [ ] 단위 테스트 작성
- [ ] 통합 테스트 작성
- [ ] 성능 최적화 (인덱스, 쿼리 최적화)
- [ ] 사용자 테스트 및 피드백 반영
---
## 7. 기술 스택
### 7.1 프론트엔드
- **React Flow**: 플로우 시각화 및 편집
- **Shadcn/ui**: UI 컴포넌트
- **TanStack Query**: 데이터 페칭 및 캐싱
### 7.2 백엔드
- **Node.js + Express**: API 서버
- **PostgreSQL**: 데이터베이스
- **TypeScript**: 타입 안전성
---
## 8. 주요 고려사항
### 8.1 성능
- 플로우 단계별 데이터 카운트 조회 시 인덱스 활용
- 대용량 데이터 처리 시 페이징 필수
- 조건 파싱 결과 캐싱
### 8.2 보안
- SQL 인젝션 방지: 파라미터 바인딩 사용
- 컬럼명 검증: 알파벳, 숫자, 언더스코어만 허용
- 사용자 권한 확인
### 8.3 확장성
- 복잡한 조건 (중첩 AND/OR) 지원 가능하도록 설계
- 플로우 분기 (조건부 분기) 지원 가능하도록 설계
- 자동 플로우 이동 (트리거) 추후 추가 가능
### 8.4 사용성
- 플로우 편집기의 직관적인 UI
- 드래그앤드롭으로 쉬운 조작
- 실시간 데이터 카운트 표시
- 오딧 로그를 통한 추적 가능성
---
## 9. 예상 일정
| Phase | 기간 | 담당 |
| ---------------------------- | ---------- | ------------------ |
| Phase 1: 기본 구조 | 1주 | Backend |
| Phase 2: 플로우 편집기 | 1주 | Frontend |
| Phase 3: 화면관리 연동 | 3일 | Frontend |
| Phase 4: 플로우 실행 | 1주 | Backend + Frontend |
| Phase 5: 데이터 이동 및 오딧 | 3일 | Backend + Frontend |
| Phase 6: 테스트 및 최적화 | 3일 | 전체 |
| **총 예상 기간** | **약 4주** | |
---
## 10. 향후 확장 계획
### 10.1 자동 플로우 이동
- 특정 조건 충족 시 자동으로 다음 단계로 이동
- 예: 결재 승인 시 자동으로 '승인완료' 단계로 이동
### 10.2 플로우 분기
- 조건에 따라 다른 경로로 분기
- 예: 금액에 따라 '일반 승인' 또는 '특별 승인' 경로
### 10.3 플로우 알림
- 특정 단계 진입 시 담당자에게 알림
- 이메일, 시스템 알림 등
### 10.4 플로우 템플릿
- 자주 사용하는 플로우 패턴을 템플릿으로 저장
- 템플릿에서 새 플로우 생성
### 10.5 플로우 통계 대시보드
- 단계별 체류 시간 분석
- 병목 구간 식별
- 처리 속도 통계
---
## 11. 참고 자료
### 11.1 React Flow
- 공식 문서: https://reactflow.dev/
- 예제: https://reactflow.dev/examples
### 11.2 유사 시스템
- Jira Workflow
- Trello Board
- GitHub Projects
---
## 12. 결론
이 플로우 관리 시스템은 제품 수명주기, 업무 프로세스, 승인 프로세스 등 다양한 비즈니스 워크플로우를 시각적으로 정의하고 관리할 수 있는 강력한 도구입니다.
화면관리 시스템과의 긴밀한 통합을 통해 사용자는 코드 없이도 복잡한 워크플로우를 구축하고 실행할 수 있으며, 오딧 로그를 통해 모든 상태 변경을 추적할 수 있습니다.
단계별 구현을 통해 안정적으로 개발하고, 향후 확장 가능성을 염두에 두어 장기적으로 유지보수하기 쉬운 시스템을 구축할 수 있습니다.