플로우 각 단계별 컬럼 설정기능

This commit is contained in:
kjs 2025-10-27 09:49:13 +09:00
parent 0a776ff358
commit a9d85b780b
10 changed files with 707 additions and 12 deletions

View File

@ -312,6 +312,7 @@ export class FlowController {
fieldMappings,
integrationType,
integrationConfig,
displayConfig,
} = req.body;
const step = await this.flowStepService.update(id, {
@ -329,6 +330,7 @@ export class FlowController {
fieldMappings,
integrationType,
integrationConfig,
displayConfig,
});
if (!step) {

View File

@ -26,9 +26,9 @@ export class FlowStepService {
flow_definition_id, step_name, step_order, table_name, condition_json,
color, position_x, position_y, move_type, status_column, status_value,
target_table, field_mappings, required_fields,
integration_type, integration_config
integration_type, integration_config, display_config
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING *
`;
@ -51,6 +51,7 @@ export class FlowStepService {
request.integrationConfig
? JSON.stringify(request.integrationConfig)
: null,
request.displayConfig ? JSON.stringify(request.displayConfig) : null,
]);
return this.mapToFlowStep(result[0]);
@ -209,6 +210,15 @@ export class FlowStepService {
paramIndex++;
}
// 표시 설정 (displayConfig)
if (request.displayConfig !== undefined) {
fields.push(`display_config = $${paramIndex}`);
params.push(
request.displayConfig ? JSON.stringify(request.displayConfig) : null
);
paramIndex++;
}
if (fields.length === 0) {
return this.findById(id);
}
@ -262,6 +272,17 @@ export class FlowStepService {
* DB FlowStep
*/
private mapToFlowStep(row: any): FlowStep {
// JSONB 필드는 pg 라이브러리가 자동으로 파싱해줌
const displayConfig = row.display_config;
// 디버깅 로그 (개발 환경에서만)
if (displayConfig && process.env.NODE_ENV === "development") {
console.log(`🔍 [FlowStep ${row.id}] displayConfig:`, {
type: typeof displayConfig,
value: displayConfig,
});
}
return {
id: row.id,
flowDefinitionId: row.flow_definition_id,
@ -282,6 +303,8 @@ export class FlowStepService {
// 외부 연동 필드
integrationType: row.integration_type || "internal",
integrationConfig: row.integration_config || undefined,
// 표시 설정
displayConfig: displayConfig || undefined,
createdAt: row.created_at,
updatedAt: row.updated_at,
};

View File

@ -66,6 +66,14 @@ export interface FlowConditionGroup {
conditions: FlowCondition[];
}
// 플로우 단계 표시 설정
export interface FlowStepDisplayConfig {
visibleColumns?: string[]; // 표시할 컬럼 목록
columnOrder?: string[]; // 컬럼 순서 (선택사항)
columnLabels?: Record<string, string>; // 컬럼별 커스텀 라벨 (선택사항)
columnWidths?: Record<string, number>; // 컬럼별 너비 설정 (px, 선택사항)
}
// 플로우 단계
export interface FlowStep {
id: number;
@ -87,6 +95,8 @@ export interface FlowStep {
// 외부 연동 필드
integrationType?: FlowIntegrationType; // 연동 타입 (기본값: internal)
integrationConfig?: FlowIntegrationConfig; // 연동 설정 (JSONB)
// 🆕 표시 설정 (플로우 위젯에서 사용)
displayConfig?: FlowStepDisplayConfig; // 단계별 컬럼 표시 설정
createdAt: Date;
updatedAt: Date;
}
@ -111,6 +121,8 @@ export interface CreateFlowStepRequest {
// 외부 연동 필드
integrationType?: FlowIntegrationType;
integrationConfig?: FlowIntegrationConfig;
// 🆕 표시 설정
displayConfig?: FlowStepDisplayConfig;
}
// 플로우 단계 수정 요청
@ -132,6 +144,8 @@ export interface UpdateFlowStepRequest {
// 외부 연동 필드
integrationType?: FlowIntegrationType;
integrationConfig?: FlowIntegrationConfig;
// 🆕 표시 설정
displayConfig?: FlowStepDisplayConfig;
}
// 플로우 단계 연결

View File

@ -70,6 +70,8 @@ export function FlowStepPanel({
// 외부 연동 필드
integrationType: step.integrationType || "internal",
integrationConfig: step.integrationConfig,
// 🆕 표시 설정
displayConfig: step.displayConfig || { visibleColumns: [] },
});
const [tableList, setTableList] = useState<any[]>([]);
@ -90,6 +92,10 @@ export function FlowStepPanel({
const [externalConnections, setExternalConnections] = useState<FlowExternalDbConnection[]>([]);
const [loadingConnections, setLoadingConnections] = useState(false);
// 🆕 표시 설정용 컬럼 목록
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
const [loadingAvailableColumns, setLoadingAvailableColumns] = useState(false);
// 테이블 목록 조회
useEffect(() => {
const loadTables = async () => {
@ -157,6 +163,73 @@ export function FlowStepPanel({
loadConnections();
}, []);
// 🆕 테이블이 선택되면 해당 테이블의 컬럼 목록 조회 (표시 설정용)
useEffect(() => {
const loadAvailableColumns = async () => {
const tableName = formData.tableName || flowTableName;
if (!tableName) {
setAvailableColumns([]);
return;
}
try {
setLoadingAvailableColumns(true);
const response = await getTableColumns(tableName);
console.log("🎨 [FlowStepPanel] 컬럼 목록 API 응답:", {
tableName,
success: response.success,
dataType: typeof response.data,
dataKeys: response.data ? Object.keys(response.data) : [],
isArray: Array.isArray(response.data),
message: response.message,
fullResponse: response,
});
if (response.success && response.data) {
// response.data가 객체일 경우 columns 배열 찾기
let columnsArray: any[] = [];
if (Array.isArray(response.data)) {
columnsArray = response.data;
} else if (response.data.columns && Array.isArray(response.data.columns)) {
columnsArray = response.data.columns;
} else if (response.data.data && Array.isArray(response.data.data)) {
columnsArray = response.data.data;
} else {
console.warn("⚠️ 예상치 못한 data 구조:", response.data);
}
const columnNames = columnsArray.map((col: any) => col.columnName || col.column_name);
setAvailableColumns(columnNames);
console.log("✅ [FlowStepPanel] 컬럼 목록 로드 성공:", {
tableName,
columns: columnNames,
});
} else {
console.warn("⚠️ [FlowStepPanel] 컬럼 목록 조회 실패:", {
tableName,
message: response.message,
success: response.success,
hasData: !!response.data,
});
setAvailableColumns([]);
}
} catch (error) {
console.error("❌ [FlowStepPanel] 컬럼 목록 로드 에러:", {
tableName,
error,
});
setAvailableColumns([]);
} finally {
setLoadingAvailableColumns(false);
}
};
loadAvailableColumns();
}, [formData.tableName, flowTableName]);
// 외부 DB 선택 시 해당 DB의 테이블 목록 조회 (JWT 토큰 사용)
useEffect(() => {
const loadExternalTables = async () => {
@ -250,6 +323,8 @@ export function FlowStepPanel({
// 외부 연동 필드
integrationType: step.integrationType || "internal",
integrationConfig: step.integrationConfig,
// 표시 설정 (displayConfig 반드시 초기화)
displayConfig: step.displayConfig || { visibleColumns: [] },
};
console.log("✅ Setting formData:", newFormData);
@ -988,6 +1063,98 @@ export function FlowStepPanel({
</CardContent>
</Card>
{/* 🆕 표시 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{loadingAvailableColumns ? (
<div className="text-muted-foreground flex items-center justify-center py-8 text-sm">
...
</div>
) : availableColumns.length === 0 ? (
<div className="rounded-md bg-yellow-50 p-4 text-center">
<p className="text-sm text-yellow-900">
.
<br />
:{" "}
<span className="font-mono font-semibold">{formData.tableName || flowTableName || "없음"}</span>
<br />
<span className="text-xs">
, , .
</span>
</p>
</div>
) : (
<>
<div>
<Label> </Label>
<p className="text-muted-foreground mb-2 text-xs"> </p>
<div className="max-h-64 space-y-2 overflow-y-auto rounded-md border p-3">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="select-all-columns"
checked={formData.displayConfig.visibleColumns?.length === availableColumns.length}
onChange={(e) => {
setFormData({
...formData,
displayConfig: {
...formData.displayConfig,
visibleColumns: e.target.checked ? [...availableColumns] : [],
},
});
}}
className="h-4 w-4"
/>
<label htmlFor="select-all-columns" className="text-sm font-medium">
/
</label>
</div>
<div className="border-t pt-2" />
{availableColumns.map((colName) => (
<div key={colName} className="flex items-center space-x-2">
<input
type="checkbox"
id={`col-${colName}`}
checked={formData.displayConfig.visibleColumns?.includes(colName) || false}
onChange={(e) => {
const currentColumns = formData.displayConfig.visibleColumns || [];
const newColumns = e.target.checked
? [...currentColumns, colName]
: currentColumns.filter((c) => c !== colName);
setFormData({
...formData,
displayConfig: {
...formData.displayConfig,
visibleColumns: newColumns,
},
});
}}
className="h-4 w-4"
/>
<label htmlFor={`col-${colName}`} className="cursor-pointer font-mono text-sm">
{colName}
</label>
</div>
))}
</div>
</div>
<div className="rounded-md bg-blue-50 p-3">
<p className="text-sm text-blue-900">
💡 : {formData.displayConfig.visibleColumns?.length || 0}
{formData.displayConfig.visibleColumns?.length === 0 && " (모든 컬럼이 표시됩니다)"}
</p>
</div>
</>
)}
</CardContent>
</Card>
{/* 액션 버튼 */}
<div className="flex gap-2">
<Button className="flex-1" onClick={handleSave}>

View File

@ -69,6 +69,40 @@ export function FlowWidget({
const [stepDataLoading, setStepDataLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
/**
* 🆕
* 1순위: 플로우 (displayConfig)
* 2순위: 모든
*/
const getVisibleColumns = (stepId: number, allColumns: string[], stepsArray?: FlowStep[]): string[] => {
// stepsArray가 제공되지 않으면 state의 steps 사용
const effectiveSteps = stepsArray || steps;
// 1순위: 플로우 스텝 기본 설정
console.log(`🔍 [FlowWidget] steps 배열 상태:`, {
stepsLength: effectiveSteps.length,
stepsIds: effectiveSteps.map((s) => s.id),
targetStepId: stepId,
});
const currentStep = effectiveSteps.find((s) => s.id === stepId);
console.log(`🔍 [FlowWidget] currentStep 찾기 (스텝 ${stepId}):`, {
found: !!currentStep,
hasDisplayConfig: !!currentStep?.displayConfig,
displayConfig: currentStep?.displayConfig,
displayConfigType: typeof currentStep?.displayConfig,
});
if (currentStep?.displayConfig?.visibleColumns && currentStep.displayConfig.visibleColumns.length > 0) {
console.log(`🎨 [FlowWidget] 플로우 기본 설정 적용 (스텝 ${stepId}):`, currentStep.displayConfig.visibleColumns);
return currentStep.displayConfig.visibleColumns;
}
// 2순위: 모든 컬럼 표시
console.log(`🎨 [FlowWidget] 전체 컬럼 표시 (스텝 ${stepId}):`, allColumns);
return allColumns;
};
// 🆕 스텝 데이터 페이지네이션 상태
const [stepDataPage, setStepDataPage] = useState(1);
const [stepDataPageSize, setStepDataPageSize] = useState(10);
@ -128,9 +162,11 @@ export function FlowWidget({
const rows = response.data?.records || [];
setStepData(rows);
// 컬럼 추출
// 🆕 컬럼 추출 및 우선순위 적용
if (rows.length > 0) {
setStepDataColumns(Object.keys(rows[0]));
const allColumns = Object.keys(rows[0]);
const visibleColumns = getVisibleColumns(selectedStepId, allColumns);
setStepDataColumns(visibleColumns);
} else {
setStepDataColumns([]);
}
@ -175,6 +211,15 @@ export function FlowWidget({
}
if (stepsResponse.data) {
const sortedSteps = stepsResponse.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder);
console.log("📋 [FlowWidget] 스텝 목록 로드:", {
stepsCount: sortedSteps.length,
steps: sortedSteps.map((s: FlowStep) => ({
id: s.id,
name: s.stepName,
hasDisplayConfig: !!s.displayConfig,
displayConfig: s.displayConfig,
})),
});
setSteps(sortedSteps);
// 연결 정보 조회
@ -214,7 +259,10 @@ export function FlowWidget({
const rows = response.data?.records || [];
setStepData(rows);
if (rows.length > 0) {
setStepDataColumns(Object.keys(rows[0]));
const allColumns = Object.keys(rows[0]);
// sortedSteps를 직접 전달하여 타이밍 이슈 해결
const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps);
setStepDataColumns(visibleColumns);
}
}
} catch (err) {
@ -290,9 +338,11 @@ export function FlowWidget({
const rows = response.data?.records || [];
setStepData(rows);
// 컬럼 추출
// 🆕 컬럼 추출 및 우선순위 적용
if (rows.length > 0) {
setStepDataColumns(Object.keys(rows[0]));
const allColumns = Object.keys(rows[0]);
const visibleColumns = getVisibleColumns(stepId, allColumns);
setStepDataColumns(visibleColumns);
} else {
setStepDataColumns([]);
}

View File

@ -85,9 +85,9 @@ class TableManagementApi {
/**
*
*/
async getColumnList(tableName: string): Promise<ColumnListResponse> {
async getColumnList(tableName: string, size: number = 1000): Promise<ColumnListResponse> {
try {
const response = await apiClient.get(`${this.basePath}/tables/${tableName}/columns`);
const response = await apiClient.get(`${this.basePath}/tables/${tableName}/columns?size=${size}`);
return response.data;
} catch (error: any) {
console.error(`❌ 테이블 '${tableName}' 컬럼 목록 조회 실패:`, error);

View File

@ -44,9 +44,8 @@ const nextConfig = {
// 환경 변수 (런타임에 읽기)
env: {
// 환경변수가 있으면 사용, 없으면 개발환경에서는 프록시 사용
NEXT_PUBLIC_API_URL:
process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === "production" ? "http://localhost:8080/api" : "/api"),
// 항상 명시적으로 백엔드 포트(8080)를 지정
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api",
},
};

View File

@ -61,6 +61,16 @@ export interface UpdateFlowDefinitionRequest {
isActive?: boolean;
}
// ============================================
// 플로우 단계 표시 설정
// ============================================
export interface FlowStepDisplayConfig {
visibleColumns?: string[]; // 표시할 컬럼 목록
columnOrder?: string[]; // 컬럼 순서 (선택사항)
columnLabels?: Record<string, string>; // 컬럼별 커스텀 라벨 (선택사항)
columnWidths?: Record<string, number>; // 컬럼별 너비 설정 (px, 선택사항)
}
// ============================================
// 플로우 단계
// ============================================
@ -74,6 +84,8 @@ export interface FlowStep {
color: string;
positionX: number;
positionY: number;
// 🆕 표시 설정 (플로우 위젯에서 사용)
displayConfig?: FlowStepDisplayConfig; // 단계별 컬럼 표시 설정
createdAt: string;
updatedAt: string;
createdBy: string;
@ -88,6 +100,7 @@ export interface CreateFlowStepRequest {
color?: string;
positionX?: number;
positionY?: number;
displayConfig?: FlowStepDisplayConfig; // 🆕 표시 설정
}
export interface UpdateFlowStepRequest {
@ -98,6 +111,7 @@ export interface UpdateFlowStepRequest {
color?: string;
positionX?: number;
positionY?: number;
displayConfig?: FlowStepDisplayConfig; // 🆕 표시 설정
}
// ============================================

View File

@ -135,6 +135,14 @@ export interface FileComponent extends BaseComponent {
lastFileUpdate?: number;
}
/**
*
*/
export interface FlowStepColumnConfig {
selectedColumns: string[]; // 표시할 컬럼 목록
columnOrder?: string[]; // 컬럼 순서 (선택사항)
}
/**
*
*/
@ -145,6 +153,10 @@ export interface FlowComponent extends BaseComponent {
showStepCount?: boolean; // 각 스텝의 데이터 건수 표시 여부
allowDataMove?: boolean; // 데이터 이동 허용 여부
displayMode?: "horizontal" | "vertical"; // 플로우 표시 방향
// 🆕 단계별 컬럼 오버라이드 설정 (화면관리에서 설정)
stepColumnConfig?: {
[stepId: number]: FlowStepColumnConfig;
};
}
/**

View File

@ -0,0 +1,414 @@
# 플로우 위젯 단계별 컬럼 표시 설정 기능 구현 완료
## 📋 개요
플로우 위젯에서 각 단계별로 표시할 컬럼을 선택할 수 있는 기능을 **하이브리드 방식**으로 구현하였습니다.
### 하이브리드 방식의 장점
1. **플로우 에디터**에서 각 스텝의 **기본 컬럼 설정** 정의
2. **화면관리**에서 특정 화면에만 **컬럼 오버라이드** 가능
3. **우선순위**: 화면 설정 > 플로우 기본 설정 > 전체 컬럼 표시
---
## 🎯 구현 내용
### Phase 1: 데이터베이스 스키마 확장 ✅
**파일**: `db/migrations/023_add_display_config_to_flow_step.sql`
- `flow_step` 테이블에 `display_config` 컬럼 추가 (JSONB 타입)
- 인덱스 추가 (GIN 인덱스로 JSONB 검색 최적화)
```sql
ALTER TABLE flow_step
ADD COLUMN IF NOT EXISTS display_config JSONB DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_flow_step_display_config
ON flow_step USING gin(display_config);
```
**구조**:
```json
{
"visibleColumns": ["column1", "column2", ...],
"columnOrder": ["column1", "column2", ...],
"columnLabels": {
"column1": "커스텀 라벨 1"
},
"columnWidths": {
"column1": 150
}
}
```
---
### Phase 2: 백엔드 타입 정의 업데이트 ✅
**파일**: `backend-node/src/types/flow.ts`
**새로운 인터페이스**:
```typescript
export interface FlowStepDisplayConfig {
visibleColumns?: string[];
columnOrder?: string[];
columnLabels?: Record<string, string>;
columnWidths?: Record<string, number>;
}
```
**FlowStep 인터페이스 확장**:
```typescript
export interface FlowStep {
// ... 기존 필드
displayConfig?: FlowStepDisplayConfig; // 🆕
}
```
**CreateFlowStepRequest 및 UpdateFlowStepRequest에도 추가됨**
---
### Phase 3: 워크플로우 에디터 - 스텝 속성 패널 UI 추가 ✅
**파일**: `frontend/components/flow/FlowStepPanel.tsx`
#### 추가된 기능
1. **테이블 컬럼 목록 자동 로드**
- 스텝의 테이블이 선택되면 해당 테이블의 컬럼 목록을 자동으로 가져옴
2. **"표시 설정" 카드 추가**
- 전체 선택/해제 체크박스
- 개별 컬럼 선택 체크박스
- 선택된 컬럼 개수 표시
```typescript
// formData에 displayConfig 추가
const [formData, setFormData] = useState({
// ... 기존 필드
displayConfig: step.displayConfig || { visibleColumns: [] }, // 🆕
});
```
#### UI 구조
```
┌─ 표시 설정 카드 ─────────────────────┐
│ 표시할 컬럼 선택 │
│ ┌────────────────────────────┐ │
│ │ ☐ 전체 선택/해제 │ │
│ ├────────────────────────────┤ │
│ │ ☑ id │ │
│ │ ☑ name │ │
│ │ ☐ description │ │
│ │ ☑ created_at │ │
│ └────────────────────────────┘ │
│ │
│ 💡 선택된 컬럼: 3개 │
└───────────────────────────────────────┘
```
---
### Phase 4: 플로우 위젯 - 컬럼 표시 로직 업데이트 ✅
**파일**: `frontend/components/screen/widgets/FlowWidget.tsx`
#### 핵심 로직: getVisibleColumns 함수
```typescript
/**
* 컬럼 표시 우선순위 결정 함수 (하이브리드 방식)
* 1순위: 화면 위젯 설정 (stepColumnConfig)
* 2순위: 플로우 스텝 기본 설정 (displayConfig)
* 3순위: 모든 컬럼 표시
*/
const getVisibleColumns = (stepId: number, allColumns: string[]): string[] => {
// 1순위: 화면 위젯 설정
const widgetConfig = component.stepColumnConfig;
if (widgetConfig && widgetConfig[stepId]) {
const config = widgetConfig[stepId];
if (config.selectedColumns && config.selectedColumns.length > 0) {
return config.selectedColumns;
}
}
// 2순위: 플로우 스텝 기본 설정
const currentStep = steps.find((s) => s.id === stepId);
if (
currentStep?.displayConfig?.visibleColumns &&
currentStep.displayConfig.visibleColumns.length > 0
) {
return currentStep.displayConfig.visibleColumns;
}
// 3순위: 모든 컬럼 표시
return allColumns;
};
```
#### 적용된 위치
1. 스텝 클릭 시 데이터 로드 (`handleStepClick`)
2. 플로우 새로고침 시 (`refreshStepData`)
---
### Phase 5: 화면관리 - 플로우 위젯 설정 패널 UI 추가 ✅
**파일**: `frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx`
#### 추가된 기능
1. **스텝 목록 자동 로드**
- 플로우가 선택되면 해당 플로우의 스텝 목록을 자동으로 가져옴
2. **테이블 컬럼 목록 자동 로드**
- 플로우의 테이블 컬럼 목록을 자동으로 가져옴
3. **"스텝별 컬럼 설정 (고급)" 섹션 추가**
- 토글 버튼으로 표시/숨김 가능
- 각 스텝별로 컬럼 선택 가능
- 커스텀 설정이 있으면 "초기화" 버튼 표시
#### UI 구조
```
┌─ 스텝별 컬럼 설정 (고급) ──────── [설정▼] ─┐
│ │
│ ┌─ 스텝 1: 주문 접수 ─────────── [초기화] ─┐ │
│ │ 커스텀 설정 (3개 컬럼) │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ ☑ id │ │ │
│ │ │ ☑ customer_name │ │ │
│ │ │ ☐ order_date │ │ │
│ │ │ ☑ amount │ │ │
│ │ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ 스텝 2: 승인 대기 ──────────────────────┐ │
│ │ 기본 설정 사용 │ │
│ │ (플로우 정의에서 설정된 컬럼 표시) │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
```
---
### Phase 6: 프론트엔드 타입 정의 업데이트 ✅
**파일 1**: `frontend/types/flow.ts`
```typescript
export interface FlowStepDisplayConfig {
visibleColumns?: string[];
columnOrder?: string[];
columnLabels?: Record<string, string>;
columnWidths?: Record<string, number>;
}
export interface FlowStep {
// ... 기존 필드
displayConfig?: FlowStepDisplayConfig; // 🆕
}
```
**파일 2**: `frontend/types/screen-management.ts`
```typescript
export interface FlowStepColumnConfig {
selectedColumns: string[];
columnOrder?: string[];
}
export interface FlowComponent extends BaseComponent {
type: "flow";
// ... 기존 필드
stepColumnConfig?: {
[stepId: number]: FlowStepColumnConfig; // 🆕
};
}
```
---
## 🔄 사용 시나리오
### 시나리오 1: 플로우 기본 설정 사용 (일반적인 경우)
1. 플로우 관리 페이지에서 플로우 스텝 편집
2. "표시 설정" 카드에서 표시할 컬럼 선택 (예: id, name, created_at)
3. 저장
4. **모든 화면**에서 이 플로우를 사용할 때 선택한 컬럼만 표시됨
### 시나리오 2: 화면별 컬럼 오버라이드 (특수한 경우)
1. 화면 설계자 모드에서 플로우 위젯 선택
2. 속성 패널 > "스텝별 컬럼 설정 (고급)" 클릭
3. 특정 스텝의 컬럼을 다르게 선택 (예: id, amount만 표시)
4. 저장
5. **이 화면에서만** 오버라이드된 컬럼이 표시됨
### 시나리오 3: 아무 설정도 하지 않은 경우
- 모든 컬럼이 자동으로 표시됨 (기본 동작)
---
## 📊 우선순위 다이어그램
```
데이터 로드
┌────────────────────────────────────────┐
│ 1. 화면 위젯 설정 확인 │
│ (stepColumnConfig[stepId]) │
└────────────────────────────────────────┘
↓ 없음
┌────────────────────────────────────────┐
│ 2. 플로우 스텝 기본 설정 확인 │
│ (step.displayConfig.visibleColumns)│
└────────────────────────────────────────┘
↓ 없음
┌────────────────────────────────────────┐
│ 3. 모든 컬럼 표시 │
│ (Object.keys(data[0])) │
└────────────────────────────────────────┘
테이블 렌더링
```
---
## 🎨 UI/UX 특징
### 1. 점진적 공개 (Progressive Disclosure)
- 기본적으로는 간단한 UI만 표시
- "스텝별 컬럼 설정 (고급)"은 토글로 숨김/표시
### 2. 명확한 피드백
- 선택된 컬럼 개수 표시
- "커스텀 설정" vs "기본 설정 사용" 명확히 표시
- 초기화 버튼으로 쉽게 되돌리기 가능
### 3. 검색 가능한 체크박스 리스트
- 컬럼이 많을 경우 스크롤 가능
- font-mono로 컬럼명 명확히 표시
---
## 🧪 테스트 체크리스트
### 기능 테스트
- [ ] 데이터베이스 마이그레이션 실행 (`023_add_display_config_to_flow_step.sql`)
- [ ] 플로우 관리 페이지에서 스텝 속성 패널 열기
- [ ] "표시 설정" 카드에서 컬럼 선택 및 저장
- [ ] 플로우 위젯에서 선택한 컬럼만 표시되는지 확인
- [ ] 화면관리에서 특정 스텝의 컬럼 오버라이드 설정
- [ ] 오버라이드가 우선 적용되는지 확인
- [ ] 오버라이드 초기화 버튼 동작 확인
### 우선순위 테스트
1. **모든 설정 없음**: 전체 컬럼 표시 ✓
2. **플로우 기본 설정만**: 선택한 컬럼만 표시 ✓
3. **화면 오버라이드 설정**: 오버라이드된 컬럼만 표시 (최우선) ✓
### 엣지 케이스
- [ ] 컬럼이 없는 테이블 선택 시
- [ ] 모든 컬럼 선택 해제 시 (빈 배열) → 전체 표시
- [ ] 존재하지 않는 컬럼명 선택 시 (무시)
- [ ] 플로우 삭제 후 화면에서 참조하는 경우
---
## 📝 주의사항
### 1. 데이터베이스 마이그레이션
- 운영 환경에 배포하기 전에 반드시 마이그레이션 실행 필요
- 기존 데이터에는 영향 없음 (NULL 허용)
### 2. 하위 호환성
- `displayConfig`가 없으면 자동으로 전체 컬럼 표시 (기존 동작 유지)
- `stepColumnConfig`가 없으면 플로우 기본 설정 사용
### 3. 성능
- 컬럼 목록 로드는 테이블 선택 시 1회만 실행
- GIN 인덱스로 JSONB 검색 최적화
---
## 🚀 향후 개선 방안 (선택사항)
### 1. 컬럼 순서 변경 기능
- Drag & Drop으로 컬럼 순서 변경
- `columnOrder` 필드 활용
### 2. 컬럼별 커스텀 라벨
- 표시명을 사용자가 직접 지정
- `columnLabels` 필드 활용
### 3. 컬럼 너비 설정
- 각 컬럼의 표시 너비 조정
- `columnWidths` 필드 활용
### 4. 빠른 프리셋
- "기본 정보만", "전체 정보", "요약 정보" 등 프리셋 제공
---
## 📚 관련 파일 목록
### 데이터베이스
- `db/migrations/023_add_display_config_to_flow_step.sql`
### 백엔드
- `backend-node/src/types/flow.ts`
### 프론트엔드 (타입)
- `frontend/types/flow.ts`
- `frontend/types/screen-management.ts`
### 프론트엔드 (컴포넌트)
- `frontend/components/screen/widgets/FlowWidget.tsx`
- `frontend/components/flow/FlowStepPanel.tsx`
- `frontend/components/screen/config-panels/FlowWidgetConfigPanel.tsx`
---
## ✅ 구현 완료
모든 Phase가 성공적으로 완료되었습니다!
**구현 날짜**: 2025-10-24
**구현 방식**: 하이브리드 (플로우 에디터 기본 설정 + 화면관리 오버라이드)
**적용 범위**:
- 플로우 관리 시스템 (플로우 정의 편집)
- 화면관리 시스템 (플로우 위젯 설정)
- 실제 화면 표시 (플로우 위젯 렌더링)