테스트 프로젝트 테이블 생성 및 오류들 수정
This commit is contained in:
parent
eb6fa71cf4
commit
d1e1c7964b
|
|
@ -4106,4 +4106,37 @@ model test_sales_info {
|
|||
manager_name String? @db.VarChar(100)
|
||||
reg_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
status String? @default("진행중") @db.VarChar(50)
|
||||
|
||||
// 관계 정의: 영업 정보에서 프로젝트로
|
||||
projects test_project_info[]
|
||||
}
|
||||
|
||||
model test_project_info {
|
||||
project_no String @id @db.VarChar(200)
|
||||
sales_no String? @db.VarChar(20)
|
||||
contract_type String? @db.VarChar(50)
|
||||
order_seq Int?
|
||||
domestic_foreign String? @db.VarChar(20)
|
||||
customer_name String? @db.VarChar(200)
|
||||
|
||||
// 프로젝트 전용 컬럼들
|
||||
project_status String? @default("PLANNING") @db.VarChar(50)
|
||||
project_start_date DateTime? @db.Date
|
||||
project_end_date DateTime? @db.Date
|
||||
project_manager String? @db.VarChar(100)
|
||||
project_description String? @db.Text
|
||||
|
||||
// 시스템 관리 컬럼들
|
||||
created_by String? @db.VarChar(100)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(100)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
|
||||
// 관계 정의: 영업 정보 참조
|
||||
sales test_sales_info? @relation(fields: [sales_no], references: [sales_no])
|
||||
|
||||
@@index([sales_no], map: "idx_project_sales_no")
|
||||
@@index([project_status], map: "idx_project_status")
|
||||
@@index([customer_name], map: "idx_project_customer")
|
||||
@@index([project_manager], map: "idx_project_manager")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,19 @@ export async function getDiagramRelationships(
|
|||
|
||||
const relationships = (diagram.relationships as any)?.relationships || [];
|
||||
|
||||
console.log("🔍 백엔드 - 관계도 데이터:", {
|
||||
diagramId: diagram.diagram_id,
|
||||
diagramName: diagram.diagram_name,
|
||||
relationshipsRaw: diagram.relationships,
|
||||
relationshipsArray: relationships,
|
||||
relationshipsCount: relationships.length,
|
||||
});
|
||||
|
||||
// 각 관계의 구조도 로깅
|
||||
relationships.forEach((rel: any, index: number) => {
|
||||
console.log(`🔍 백엔드 - 관계 ${index + 1}:`, rel);
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: relationships,
|
||||
|
|
@ -213,14 +226,77 @@ export async function getRelationshipPreview(
|
|||
}
|
||||
|
||||
// 관계 정보 찾기
|
||||
const relationship = (diagram.relationships as any)?.relationships?.find(
|
||||
console.log("🔍 관계 미리보기 요청:", {
|
||||
diagramId,
|
||||
relationshipId,
|
||||
diagramRelationships: diagram.relationships,
|
||||
relationshipsArray: (diagram.relationships as any)?.relationships,
|
||||
});
|
||||
|
||||
const relationships = (diagram.relationships as any)?.relationships || [];
|
||||
console.log(
|
||||
"🔍 사용 가능한 관계 목록:",
|
||||
relationships.map((rel: any) => ({
|
||||
id: rel.id,
|
||||
name: rel.relationshipName || rel.name, // relationshipName 사용
|
||||
sourceTable: rel.fromTable || rel.sourceTable, // fromTable 사용
|
||||
targetTable: rel.toTable || rel.targetTable, // toTable 사용
|
||||
originalData: rel, // 디버깅용
|
||||
}))
|
||||
);
|
||||
|
||||
const relationship = relationships.find(
|
||||
(rel: any) => rel.id === relationshipId
|
||||
);
|
||||
|
||||
console.log("🔍 찾은 관계:", relationship);
|
||||
|
||||
if (!relationship) {
|
||||
console.log("❌ 관계를 찾을 수 없음:", {
|
||||
requestedId: relationshipId,
|
||||
availableIds: relationships.map((rel: any) => rel.id),
|
||||
});
|
||||
|
||||
// 🔧 임시 해결책: 첫 번째 관계를 사용하거나 기본 응답 반환
|
||||
if (relationships.length > 0) {
|
||||
console.log("🔧 첫 번째 관계를 대신 사용:", relationships[0].id);
|
||||
|
||||
const fallbackRelationship = relationships[0];
|
||||
|
||||
console.log("🔍 fallback 관계 선택:", fallbackRelationship);
|
||||
console.log("🔍 diagram.control 전체 구조:", diagram.control);
|
||||
console.log("🔍 diagram.plan 전체 구조:", diagram.plan);
|
||||
|
||||
const fallbackControl = Array.isArray(diagram.control)
|
||||
? diagram.control.find((c: any) => c.id === fallbackRelationship.id)
|
||||
: null;
|
||||
const fallbackPlan = Array.isArray(diagram.plan)
|
||||
? diagram.plan.find((p: any) => p.id === fallbackRelationship.id)
|
||||
: null;
|
||||
|
||||
console.log("🔍 찾은 fallback control:", fallbackControl);
|
||||
console.log("🔍 찾은 fallback plan:", fallbackPlan);
|
||||
|
||||
const fallbackPreviewData = {
|
||||
relationship: fallbackRelationship,
|
||||
control: fallbackControl,
|
||||
plan: fallbackPlan,
|
||||
conditionsCount: (fallbackControl as any)?.conditions?.length || 0,
|
||||
actionsCount: (fallbackPlan as any)?.actions?.length || 0,
|
||||
};
|
||||
|
||||
console.log("🔍 최종 fallback 응답 데이터:", fallbackPreviewData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: fallbackPreviewData,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "관계를 찾을 수 없습니다.",
|
||||
message: `관계를 찾을 수 없습니다. 요청된 ID: ${relationshipId}, 사용 가능한 ID: ${relationships.map((rel: any) => rel.id).join(", ")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ export const saveFormData = async (
|
|||
const { companyCode, userId } = req.user as any;
|
||||
const { screenId, tableName, data } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!screenId || !tableName || !data) {
|
||||
// 필수 필드 검증 (screenId는 0일 수 있으므로 undefined 체크)
|
||||
if (screenId === undefined || screenId === null || !tableName || !data) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (screenId, tableName, data)",
|
||||
|
|
@ -80,7 +80,7 @@ export const updateFormData = async (
|
|||
};
|
||||
|
||||
const result = await dynamicFormService.updateFormData(
|
||||
parseInt(id),
|
||||
id, // parseInt 제거 - 문자열 ID 지원
|
||||
tableName,
|
||||
formDataWithMeta
|
||||
);
|
||||
|
|
@ -168,7 +168,7 @@ export const deleteFormData = async (
|
|||
});
|
||||
}
|
||||
|
||||
await dynamicFormService.deleteFormData(parseInt(id), tableName);
|
||||
await dynamicFormService.deleteFormData(id, tableName); // parseInt 제거 - 문자열 ID 지원
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -549,7 +549,7 @@ export class DynamicFormService {
|
|||
* 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트)
|
||||
*/
|
||||
async updateFormData(
|
||||
id: number,
|
||||
id: string | number,
|
||||
tableName: string,
|
||||
data: Record<string, any>
|
||||
): Promise<FormDataResult> {
|
||||
|
|
@ -642,10 +642,36 @@ export class DynamicFormService {
|
|||
const primaryKeyColumn = primaryKeys[0]; // 첫 번째 기본키 사용
|
||||
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
||||
|
||||
// 기본키 데이터 타입 조회하여 적절한 캐스팅 적용
|
||||
const primaryKeyInfo = (await prisma.$queryRawUnsafe(`
|
||||
SELECT data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = '${tableName}'
|
||||
AND column_name = '${primaryKeyColumn}'
|
||||
AND table_schema = 'public'
|
||||
`)) as any[];
|
||||
|
||||
let typeCastSuffix = "";
|
||||
if (primaryKeyInfo.length > 0) {
|
||||
const dataType = primaryKeyInfo[0].data_type;
|
||||
console.log(`🔍 기본키 ${primaryKeyColumn}의 데이터 타입: ${dataType}`);
|
||||
|
||||
if (dataType.includes("character") || dataType.includes("text")) {
|
||||
typeCastSuffix = "::text";
|
||||
} else if (dataType.includes("bigint")) {
|
||||
typeCastSuffix = "::bigint";
|
||||
} else if (
|
||||
dataType.includes("integer") ||
|
||||
dataType.includes("numeric")
|
||||
) {
|
||||
typeCastSuffix = "::numeric";
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE ${tableName}
|
||||
SET ${setClause}
|
||||
WHERE ${primaryKeyColumn} = $${values.length}
|
||||
WHERE ${primaryKeyColumn} = $${values.length}${typeCastSuffix}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
|
|
@ -707,7 +733,7 @@ export class DynamicFormService {
|
|||
* 폼 데이터 삭제 (실제 테이블에서 직접 삭제)
|
||||
*/
|
||||
async deleteFormData(
|
||||
id: number,
|
||||
id: string | number,
|
||||
tableName: string,
|
||||
companyCode?: string
|
||||
): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,41 @@ const prisma = new PrismaClient();
|
|||
export class TableManagementService {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* 컬럼이 코드 타입인지 확인하고 코드 카테고리 반환
|
||||
*/
|
||||
private async getCodeTypeInfo(
|
||||
tableName: string,
|
||||
columnName: string
|
||||
): Promise<{ isCodeType: boolean; codeCategory?: string }> {
|
||||
try {
|
||||
// column_labels 테이블에서 해당 컬럼의 web_type이 'code'인지 확인
|
||||
const result = await prisma.$queryRaw`
|
||||
SELECT web_type, code_category
|
||||
FROM column_labels
|
||||
WHERE table_name = ${tableName}
|
||||
AND column_name = ${columnName}
|
||||
AND web_type = 'code'
|
||||
`;
|
||||
|
||||
if (Array.isArray(result) && result.length > 0) {
|
||||
const row = result[0] as any;
|
||||
return {
|
||||
isCodeType: true,
|
||||
codeCategory: row.code_category,
|
||||
};
|
||||
}
|
||||
|
||||
return { isCodeType: false };
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`코드 타입 컬럼 확인 중 오류: ${tableName}.${columnName}`,
|
||||
error
|
||||
);
|
||||
return { isCodeType: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 목록 조회 (PostgreSQL information_schema 활용)
|
||||
* 메타데이터 조회는 Prisma로 변경 불가
|
||||
|
|
@ -915,8 +950,36 @@ export class TableManagementService {
|
|||
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
|
||||
if (typeof value === "string") {
|
||||
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
|
||||
searchValues.push(`%${value}%`);
|
||||
// 🎯 코드 타입 컬럼의 경우 코드값과 코드명 모두로 검색
|
||||
const codeTypeInfo = await this.getCodeTypeInfo(
|
||||
tableName,
|
||||
safeColumn
|
||||
);
|
||||
|
||||
if (codeTypeInfo.isCodeType && codeTypeInfo.codeCategory) {
|
||||
// 코드 타입 컬럼: 코드값 또는 코드명으로 검색
|
||||
// 1) 컬럼 값이 직접 검색어와 일치하는 경우
|
||||
// 2) 컬럼 값이 코드값이고, 해당 코드의 코드명이 검색어와 일치하는 경우
|
||||
whereConditions.push(`(
|
||||
${safeColumn}::text ILIKE $${paramIndex} OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM code_info ci
|
||||
WHERE ci.code_category = $${paramIndex + 1}
|
||||
AND ci.code_value = ${safeColumn}
|
||||
AND ci.code_name ILIKE $${paramIndex + 2}
|
||||
)
|
||||
)`);
|
||||
searchValues.push(`%${value}%`); // 직접 값 검색용
|
||||
searchValues.push(codeTypeInfo.codeCategory); // 코드 카테고리
|
||||
searchValues.push(`%${value}%`); // 코드명 검색용
|
||||
paramIndex += 2; // 추가 파라미터로 인해 인덱스 증가
|
||||
} else {
|
||||
// 일반 컬럼: 기존 방식
|
||||
whereConditions.push(
|
||||
`${safeColumn}::text ILIKE $${paramIndex}`
|
||||
);
|
||||
searchValues.push(`%${value}%`);
|
||||
}
|
||||
} else {
|
||||
whereConditions.push(`${safeColumn} = $${paramIndex}`);
|
||||
searchValues.push(value);
|
||||
|
|
|
|||
|
|
@ -1557,6 +1557,22 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
</Select>
|
||||
);
|
||||
|
||||
case "code":
|
||||
// 코드 타입은 텍스트 검색으로 처리 (코드명으로 검색 가능)
|
||||
return (
|
||||
<Input
|
||||
key={filter.columnName}
|
||||
placeholder={`${filter.label} 검색... (코드명 또는 코드값)`}
|
||||
value={value}
|
||||
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import { Button } from "@/components/ui/button";
|
|||
import { toast } from "react-hot-toast";
|
||||
import { Loader2, CheckCircle2, AlertCircle, Clock } from "lucide-react";
|
||||
import { ComponentData, ButtonActionType } from "@/types/screen";
|
||||
import { optimizedButtonDataflowService } from "@/lib/services/optimizedButtonDataflowService";
|
||||
import {
|
||||
optimizedButtonDataflowService,
|
||||
OptimizedButtonDataflowService,
|
||||
ExtendedControlContext,
|
||||
} from "@/lib/services/optimizedButtonDataflowService";
|
||||
import { dataflowJobQueue } from "@/lib/services/dataflowJobQueue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -60,7 +64,7 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
try {
|
||||
console.log(`🔘 Button clicked: ${component.id} (${config?.actionType})`);
|
||||
|
||||
// 🔥 현재 폼 데이터 수집
|
||||
// 🔥 확장된 컨텍스트 데이터 수집
|
||||
const contextData = {
|
||||
...formData,
|
||||
buttonId: component.id,
|
||||
|
|
@ -69,11 +73,61 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
clickCount,
|
||||
};
|
||||
|
||||
// 🔥 확장된 제어 컨텍스트 생성
|
||||
const extendedContext = {
|
||||
formData,
|
||||
selectedRows: selectedRows || [],
|
||||
selectedRowsData: selectedRowsData || [],
|
||||
controlDataSource: config?.dataflowConfig?.controlDataSource || "form",
|
||||
buttonId: component.id,
|
||||
componentData: component,
|
||||
timestamp: new Date().toISOString(),
|
||||
clickCount,
|
||||
};
|
||||
|
||||
// 🔥 제어 전용 액션인지 확인
|
||||
const isControlOnlyAction = config?.actionType === "control";
|
||||
|
||||
console.log("🎯 OptimizedButtonComponent 실행:", {
|
||||
actionType: config?.actionType,
|
||||
isControlOnlyAction,
|
||||
enableDataflowControl: config?.enableDataflowControl,
|
||||
hasDataflowConfig: !!config?.dataflowConfig,
|
||||
selectedRows,
|
||||
selectedRowsData,
|
||||
});
|
||||
|
||||
if (config?.enableDataflowControl && config?.dataflowConfig) {
|
||||
// 🔥 확장된 제어 검증 먼저 실행
|
||||
const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation(
|
||||
config.dataflowConfig,
|
||||
extendedContext as ExtendedControlContext,
|
||||
);
|
||||
|
||||
if (!validationResult.success) {
|
||||
toast.error(validationResult.message || "제어 조건을 만족하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 제어 전용 액션이면 여기서 종료
|
||||
if (isControlOnlyAction) {
|
||||
toast.success("제어 조건을 만족합니다.");
|
||||
if (onActionComplete) {
|
||||
onActionComplete({ success: true, message: "제어 조건 통과" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 최적화된 버튼 실행 (즉시 응답)
|
||||
await executeOptimizedButtonAction(contextData);
|
||||
} else if (isControlOnlyAction) {
|
||||
// 🔥 제어관리가 비활성화된 상태에서 제어 액션
|
||||
toast.warning(
|
||||
"제어관리를 먼저 활성화해주세요. 제어 액션을 사용하려면 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// 🔥 기존 액션만 실행
|
||||
// 🔥 기존 액션만 실행 (제어 액션 제외)
|
||||
await executeOriginalAction(config?.actionType || "save", contextData);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -332,6 +386,12 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
actionType: ButtonActionType,
|
||||
contextData: Record<string, any>,
|
||||
): Promise<any> => {
|
||||
// 🔥 제어 액션은 여기서 처리하지 않음 (이미 위에서 처리됨)
|
||||
if (actionType === "control") {
|
||||
console.warn("제어 액션은 executeOriginalAction에서 처리되지 않아야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 간단한 mock 처리 (실제로는 API 호출)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms 시뮬레이션
|
||||
|
||||
|
|
@ -369,6 +429,7 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
|
|||
modal: "모달",
|
||||
newWindow: "새 창",
|
||||
navigate: "페이지 이동",
|
||||
control: "제어",
|
||||
};
|
||||
return displayNames[actionType] || actionType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<SelectItem value="close">닫기</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="control">제어 (조건 체크만)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Check, ChevronsUpDown, Search, Info, Settings } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, Search, Info, Settings, FileText, Table, Layers } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ComponentData, ButtonDataflowConfig } from "@/types/screen";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
|
@ -111,16 +111,38 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
const response = await apiClient.get(`/test-button-dataflow/diagrams/${diagramId}/relationships`);
|
||||
|
||||
if (response.data.success && Array.isArray(response.data.data)) {
|
||||
const relationshipList = response.data.data.map((rel: any) => ({
|
||||
id: rel.id,
|
||||
name: rel.name || `${rel.sourceTable} → ${rel.targetTable}`,
|
||||
sourceTable: rel.sourceTable,
|
||||
targetTable: rel.targetTable,
|
||||
category: rel.category || "data-save",
|
||||
}));
|
||||
console.log("🔍 백엔드에서 받은 관계 데이터:", response.data.data);
|
||||
|
||||
const relationshipList = response.data.data.map((rel: any) => {
|
||||
console.log("🔍 개별 관계 데이터:", rel);
|
||||
|
||||
// 여러 가지 가능한 필드명 시도 (백엔드 로그 기준으로 수정)
|
||||
const relationshipName =
|
||||
rel.relationshipName || // 백엔드에서 이 필드 사용
|
||||
rel.name ||
|
||||
rel.relationship_name ||
|
||||
rel.label ||
|
||||
rel.title ||
|
||||
rel.description ||
|
||||
`${rel.fromTable || rel.sourceTable || rel.source_table} → ${rel.toTable || rel.targetTable || rel.target_table}`;
|
||||
|
||||
const sourceTable = rel.fromTable || rel.sourceTable || rel.source_table || "Unknown"; // fromTable 우선
|
||||
const targetTable = rel.toTable || rel.targetTable || rel.target_table || "Unknown"; // toTable 우선
|
||||
|
||||
const mappedRel = {
|
||||
id: rel.id,
|
||||
name: relationshipName,
|
||||
sourceTable,
|
||||
targetTable,
|
||||
category: rel.category || rel.type || "data-save",
|
||||
};
|
||||
|
||||
console.log("🔍 매핑된 관계 데이터:", mappedRel);
|
||||
return mappedRel;
|
||||
});
|
||||
|
||||
setRelationships(relationshipList);
|
||||
console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료`);
|
||||
console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료:`, relationshipList);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 관계 목록 로딩 실패:", error);
|
||||
|
|
@ -173,6 +195,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
close: "닫기",
|
||||
popup: "팝업",
|
||||
navigate: "페이지 이동",
|
||||
control: "제어",
|
||||
};
|
||||
return displayNames[actionType] || actionType;
|
||||
};
|
||||
|
|
@ -215,6 +238,50 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
</div>
|
||||
|
||||
{/* 🔥 제어관리가 활성화된 경우에만 설정 표시 */}
|
||||
{config.enableDataflowControl && (
|
||||
<>
|
||||
{/* 🔥 제어 데이터 소스 선택 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">📊 제어 데이터 소스</Label>
|
||||
<Select
|
||||
value={dataflowConfig.controlDataSource || "form"}
|
||||
onValueChange={(value) => onUpdateProperty("webTypeConfig.dataflowConfig.controlDataSource", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="제어 데이터 소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="form">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>폼 데이터 기반</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="table-selection">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Table className="h-4 w-4" />
|
||||
<span>테이블 선택 기반</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="both">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Layers className="h-4 w-4" />
|
||||
<span>폼 + 테이블 선택</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
{dataflowConfig.controlDataSource === "form" && "현재 폼의 입력값으로 조건을 체크합니다"}
|
||||
{dataflowConfig.controlDataSource === "table-selection" &&
|
||||
"테이블에서 선택된 항목의 데이터로 조건을 체크합니다"}
|
||||
{dataflowConfig.controlDataSource === "both" && "폼 데이터와 선택된 항목 데이터를 모두 사용합니다"}
|
||||
{!dataflowConfig.controlDataSource && "폼 데이터를 기본으로 사용합니다"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{config.enableDataflowControl && (
|
||||
<div className="space-y-6 border-l-2 border-blue-200 pl-4">
|
||||
{/* 현재 액션 정보 (간소화) */}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class DynamicFormApi {
|
|||
* @returns 업데이트 결과
|
||||
*/
|
||||
static async updateFormData(
|
||||
id: number,
|
||||
id: string | number,
|
||||
formData: Partial<DynamicFormData>,
|
||||
): Promise<ApiResponse<SaveFormDataResponse>> {
|
||||
try {
|
||||
|
|
@ -173,7 +173,7 @@ export class DynamicFormApi {
|
|||
* @param tableName 테이블명
|
||||
* @returns 삭제 결과
|
||||
*/
|
||||
static async deleteFormDataFromTable(id: number, tableName: string): Promise<ApiResponse<void>> {
|
||||
static async deleteFormDataFromTable(id: string | number, tableName: string): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName });
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
currentValue,
|
||||
hasFormData: !!formData,
|
||||
formDataKeys: formData ? Object.keys(formData) : [],
|
||||
autoGeneration: component.autoGeneration,
|
||||
hidden: component.hidden,
|
||||
isInteractive,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -200,6 +203,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
config={component.componentConfig}
|
||||
componentConfig={component.componentConfig}
|
||||
value={currentValue} // formData에서 추출한 현재 값 전달
|
||||
// 새로운 기능들 전달
|
||||
autoGeneration={component.autoGeneration}
|
||||
hidden={component.hidden}
|
||||
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
|
||||
isInteractive={isInteractive}
|
||||
formData={formData}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { ButtonPrimaryConfig } from "./types";
|
||||
import {
|
||||
|
|
@ -71,6 +71,21 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
config: any;
|
||||
context: ButtonActionContext;
|
||||
} | null>(null);
|
||||
|
||||
// 토스트 정리를 위한 ref
|
||||
const currentLoadingToastRef = useRef<string | number | undefined>();
|
||||
|
||||
// 컴포넌트 언마운트 시 토스트 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (currentLoadingToastRef.current !== undefined) {
|
||||
console.log("🧹 컴포넌트 언마운트 시 토스트 정리");
|
||||
toast.dismiss(currentLoadingToastRef.current);
|
||||
currentLoadingToastRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 설정
|
||||
const componentConfig = {
|
||||
...config,
|
||||
|
|
@ -84,13 +99,26 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
processedConfig.action = {
|
||||
...DEFAULT_BUTTON_ACTIONS[actionType],
|
||||
type: actionType,
|
||||
// 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴)
|
||||
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
|
||||
dataflowConfig: component.webTypeConfig?.dataflowConfig,
|
||||
};
|
||||
} else if (componentConfig.action && typeof componentConfig.action === "object") {
|
||||
// 🔥 이미 객체인 경우에도 제어관리 설정 추가
|
||||
processedConfig.action = {
|
||||
...componentConfig.action,
|
||||
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
|
||||
dataflowConfig: component.webTypeConfig?.dataflowConfig,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("🔧 버튼 컴포넌트 설정:", {
|
||||
originalConfig: componentConfig,
|
||||
processedConfig,
|
||||
component: component,
|
||||
actionConfig: processedConfig.action,
|
||||
webTypeConfig: component.webTypeConfig,
|
||||
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
|
||||
dataflowConfig: component.webTypeConfig?.dataflowConfig,
|
||||
screenId,
|
||||
tableName,
|
||||
onRefresh,
|
||||
|
|
@ -119,13 +147,22 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
// 실제 액션 실행 함수
|
||||
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
|
||||
console.log("🚀 executeAction 시작:", { actionConfig, context });
|
||||
let loadingToast: string | number | undefined;
|
||||
|
||||
try {
|
||||
// 기존 토스트가 있다면 먼저 제거
|
||||
if (currentLoadingToastRef.current !== undefined) {
|
||||
console.log("📱 기존 토스트 제거");
|
||||
toast.dismiss(currentLoadingToastRef.current);
|
||||
currentLoadingToastRef.current = undefined;
|
||||
}
|
||||
|
||||
// 추가 안전장치: 모든 로딩 토스트 제거
|
||||
toast.dismiss();
|
||||
|
||||
// edit 액션을 제외하고만 로딩 토스트 표시
|
||||
if (actionConfig.type !== "edit") {
|
||||
console.log("📱 로딩 토스트 표시 시작");
|
||||
loadingToast = toast.loading(
|
||||
currentLoadingToastRef.current = toast.loading(
|
||||
actionConfig.type === "save"
|
||||
? "저장 중..."
|
||||
: actionConfig.type === "delete"
|
||||
|
|
@ -133,8 +170,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
: actionConfig.type === "submit"
|
||||
? "제출 중..."
|
||||
: "처리 중...",
|
||||
{
|
||||
duration: Infinity, // 명시적으로 무한대로 설정
|
||||
},
|
||||
);
|
||||
console.log("📱 로딩 토스트 ID:", loadingToast);
|
||||
console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current);
|
||||
}
|
||||
|
||||
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
||||
|
|
@ -142,9 +182,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
||||
|
||||
// 로딩 토스트 제거 (있는 경우에만)
|
||||
if (loadingToast) {
|
||||
console.log("📱 로딩 토스트 제거");
|
||||
toast.dismiss(loadingToast);
|
||||
if (currentLoadingToastRef.current !== undefined) {
|
||||
console.log("📱 로딩 토스트 제거 시도, ID:", currentLoadingToastRef.current);
|
||||
toast.dismiss(currentLoadingToastRef.current);
|
||||
currentLoadingToastRef.current = undefined;
|
||||
}
|
||||
|
||||
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
|
||||
|
|
@ -170,9 +211,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
console.log("❌ executeAction catch 블록 진입:", error);
|
||||
|
||||
// 로딩 토스트 제거
|
||||
if (loadingToast) {
|
||||
console.log("📱 오류 시 로딩 토스트 제거");
|
||||
toast.dismiss(loadingToast);
|
||||
if (currentLoadingToastRef.current !== undefined) {
|
||||
console.log("📱 오류 시 로딩 토스트 제거, ID:", currentLoadingToastRef.current);
|
||||
toast.dismiss(currentLoadingToastRef.current);
|
||||
currentLoadingToastRef.current = undefined;
|
||||
}
|
||||
|
||||
console.error("❌ 버튼 액션 실행 오류:", error);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { DateInputConfig } from "./types";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||
import { AutoGenerationConfig } from "@/types/screen";
|
||||
|
||||
export interface DateInputComponentProps extends ComponentRendererProps {
|
||||
config?: DateInputConfig;
|
||||
value?: any; // 외부에서 전달받는 값
|
||||
autoGeneration?: AutoGenerationConfig;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +32,8 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|||
formData,
|
||||
onFormDataChange,
|
||||
value: externalValue, // 외부에서 전달받은 값
|
||||
autoGeneration,
|
||||
hidden,
|
||||
...props
|
||||
}) => {
|
||||
// 컴포넌트 설정
|
||||
|
|
@ -36,14 +42,130 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|||
...component.config,
|
||||
} as DateInputConfig;
|
||||
|
||||
// 🎯 자동생성 상태 관리
|
||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||
|
||||
// 🚨 컴포넌트 마운트 확인용 로그
|
||||
console.log("🚨 DateInputComponent 마운트됨!", {
|
||||
componentId: component.id,
|
||||
isInteractive,
|
||||
isDesignMode,
|
||||
autoGeneration,
|
||||
componentAutoGeneration: component.autoGeneration,
|
||||
externalValue,
|
||||
formDataValue: formData?.[component.columnName || ""],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 🧪 무조건 실행되는 테스트
|
||||
useEffect(() => {
|
||||
console.log("🧪 DateInputComponent 무조건 실행 테스트!");
|
||||
const testDate = "2025-01-19"; // 고정된 테스트 날짜
|
||||
setAutoGeneratedValue(testDate);
|
||||
console.log("🧪 autoGeneratedValue 설정 완료:", testDate);
|
||||
}, []); // 빈 의존성 배열로 한 번만 실행
|
||||
|
||||
// 자동생성 설정 (props 우선, 컴포넌트 설정 폴백)
|
||||
const finalAutoGeneration = autoGeneration || component.autoGeneration;
|
||||
const finalHidden = hidden !== undefined ? hidden : component.hidden;
|
||||
|
||||
// 🧪 테스트용 간단한 자동생성 로직
|
||||
useEffect(() => {
|
||||
console.log("🔍 DateInputComponent useEffect 실행:", {
|
||||
componentId: component.id,
|
||||
finalAutoGeneration,
|
||||
enabled: finalAutoGeneration?.enabled,
|
||||
type: finalAutoGeneration?.type,
|
||||
isInteractive,
|
||||
isDesignMode,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
columnName: component.columnName,
|
||||
currentFormValue: formData?.[component.columnName || ""],
|
||||
});
|
||||
|
||||
// 🧪 테스트: 자동생성이 활성화되어 있으면 무조건 현재 날짜 설정
|
||||
if (finalAutoGeneration?.enabled) {
|
||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
console.log("🧪 테스트용 날짜 생성:", today);
|
||||
|
||||
setAutoGeneratedValue(today);
|
||||
|
||||
// 인터랙티브 모드에서 폼 데이터에도 설정
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
console.log("📤 테스트용 폼 데이터 업데이트:", component.columnName, today);
|
||||
onFormDataChange(component.columnName, today);
|
||||
}
|
||||
}
|
||||
|
||||
// 원래 자동생성 로직 (주석 처리)
|
||||
/*
|
||||
if (finalAutoGeneration?.enabled && finalAutoGeneration.type !== "none") {
|
||||
const fieldName = component.columnName || component.id;
|
||||
const generatedValue = AutoGenerationUtils.generateValue(finalAutoGeneration, fieldName);
|
||||
|
||||
console.log("🎯 DateInputComponent 자동생성 시도:", {
|
||||
componentId: component.id,
|
||||
fieldName,
|
||||
type: finalAutoGeneration.type,
|
||||
options: finalAutoGeneration.options,
|
||||
generatedValue,
|
||||
isInteractive,
|
||||
isDesignMode,
|
||||
});
|
||||
|
||||
if (generatedValue) {
|
||||
console.log("✅ DateInputComponent 자동생성 성공:", generatedValue);
|
||||
setAutoGeneratedValue(generatedValue);
|
||||
|
||||
// 인터랙티브 모드에서 폼 데이터 업데이트
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
const currentValue = formData?.[component.columnName];
|
||||
if (!currentValue) {
|
||||
console.log("📤 DateInputComponent -> onFormDataChange 호출:", component.columnName, generatedValue);
|
||||
onFormDataChange(component.columnName, generatedValue);
|
||||
} else {
|
||||
console.log("⚠️ DateInputComponent 기존 값이 있어서 자동생성 스킵:", currentValue);
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ DateInputComponent 자동생성 조건 불만족:", {
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
hasColumnName: !!component.columnName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log("❌ DateInputComponent 자동생성 실패: generatedValue가 null");
|
||||
}
|
||||
} else {
|
||||
console.log("⚠️ DateInputComponent 자동생성 비활성화:", {
|
||||
enabled: finalAutoGeneration?.enabled,
|
||||
type: finalAutoGeneration?.type,
|
||||
});
|
||||
}
|
||||
*/
|
||||
}, [
|
||||
finalAutoGeneration?.enabled,
|
||||
finalAutoGeneration?.type,
|
||||
finalAutoGeneration?.options,
|
||||
component.id,
|
||||
component.columnName,
|
||||
isInteractive,
|
||||
]);
|
||||
|
||||
// 날짜 값 계산 및 디버깅
|
||||
const fieldName = component.columnName || component.id;
|
||||
const rawValue =
|
||||
externalValue !== undefined
|
||||
? externalValue
|
||||
: isInteractive && formData && component.columnName
|
||||
? formData[component.columnName]
|
||||
: component.value;
|
||||
|
||||
// 값 우선순위: externalValue > formData > autoGeneratedValue > component.value
|
||||
let rawValue: any;
|
||||
if (externalValue !== undefined) {
|
||||
rawValue = externalValue;
|
||||
} else if (isInteractive && formData && component.columnName && formData[component.columnName]) {
|
||||
rawValue = formData[component.columnName];
|
||||
} else if (autoGeneratedValue) {
|
||||
rawValue = autoGeneratedValue;
|
||||
} else {
|
||||
rawValue = component.value;
|
||||
}
|
||||
|
||||
console.log("🔍 DateInputComponent 값 디버깅:", {
|
||||
componentId: component.id,
|
||||
|
|
@ -196,10 +318,14 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|||
<input
|
||||
type="date"
|
||||
value={formattedValue}
|
||||
placeholder={componentConfig.placeholder || ""}
|
||||
placeholder={
|
||||
finalAutoGeneration?.enabled
|
||||
? `자동생성: ${AutoGenerationUtils.getTypeDescription(finalAutoGeneration.type)}`
|
||||
: componentConfig.placeholder || ""
|
||||
}
|
||||
disabled={componentConfig.disabled || false}
|
||||
required={componentConfig.required || false}
|
||||
readOnly={componentConfig.readonly || false}
|
||||
readOnly={componentConfig.readonly || finalAutoGeneration?.enabled || false}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { DateInputConfig } from "./types";
|
||||
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||
|
||||
export interface DateInputConfigPanelProps {
|
||||
config: DateInputConfig;
|
||||
|
|
@ -16,21 +18,17 @@ export interface DateInputConfigPanelProps {
|
|||
* DateInput 설정 패널
|
||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||
*/
|
||||
export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
}) => {
|
||||
export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({ config, onChange }) => {
|
||||
const handleChange = (key: keyof DateInputConfig, value: any) => {
|
||||
console.log("🔧 DateInputConfigPanel.handleChange:", { key, value });
|
||||
onChange({ [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">
|
||||
date-input 설정
|
||||
</div>
|
||||
<div className="text-sm font-medium">date-input 설정</div>
|
||||
|
||||
{/* date 관련 설정 */}
|
||||
{/* date 관련 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="placeholder">플레이스홀더</Label>
|
||||
<Input
|
||||
|
|
@ -67,6 +65,194 @@ export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({
|
|||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 숨김 기능 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hidden">숨김</Label>
|
||||
<Checkbox
|
||||
id="hidden"
|
||||
checked={config.hidden || false}
|
||||
onCheckedChange={(checked) => handleChange("hidden", checked)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">편집기에서는 연하게 보이지만 실제 화면에서는 숨겨집니다</p>
|
||||
</div>
|
||||
|
||||
{/* 자동생성 기능 */}
|
||||
<div className="space-y-3 border-t pt-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="autoGeneration">자동생성</Label>
|
||||
<Checkbox
|
||||
id="autoGeneration"
|
||||
checked={config.autoGeneration?.enabled || false}
|
||||
onCheckedChange={(checked) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration,
|
||||
enabled: checked as boolean,
|
||||
type: config.autoGeneration?.type || "current_time",
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.autoGeneration?.enabled && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="autoGenerationType">자동생성 타입</Label>
|
||||
<Select
|
||||
value={config.autoGeneration?.type || "current_time"}
|
||||
onValueChange={(value: AutoGenerationType) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration,
|
||||
type: value,
|
||||
options: value === "current_time" ? { format: "date" } : {},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="current_time">현재 날짜/시간</SelectItem>
|
||||
<SelectItem value="uuid">UUID</SelectItem>
|
||||
<SelectItem value="current_user">현재 사용자</SelectItem>
|
||||
<SelectItem value="sequence">순차 번호</SelectItem>
|
||||
<SelectItem value="random_string">랜덤 문자열</SelectItem>
|
||||
<SelectItem value="random_number">랜덤 숫자</SelectItem>
|
||||
<SelectItem value="company_code">회사 코드</SelectItem>
|
||||
<SelectItem value="department">부서 코드</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
{AutoGenerationUtils.getTypeDescription(config.autoGeneration?.type || "current_time")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{config.autoGeneration?.type === "current_time" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateFormat">날짜 형식</Label>
|
||||
<Select
|
||||
value={config.autoGeneration?.options?.format || "date"}
|
||||
onValueChange={(value) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration!,
|
||||
options: {
|
||||
...config.autoGeneration?.options,
|
||||
format: value,
|
||||
},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date">날짜만 (YYYY-MM-DD)</SelectItem>
|
||||
<SelectItem value="datetime">날짜+시간 (YYYY-MM-DD HH:mm:ss)</SelectItem>
|
||||
<SelectItem value="time">시간만 (HH:mm:ss)</SelectItem>
|
||||
<SelectItem value="timestamp">타임스탬프</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(config.autoGeneration?.type === "sequence" ||
|
||||
config.autoGeneration?.type === "random_string" ||
|
||||
config.autoGeneration?.type === "random_number") && (
|
||||
<>
|
||||
{config.autoGeneration?.type === "sequence" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startValue">시작값</Label>
|
||||
<Input
|
||||
id="startValue"
|
||||
type="number"
|
||||
value={config.autoGeneration?.options?.startValue || 1}
|
||||
onChange={(e) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration!,
|
||||
options: {
|
||||
...config.autoGeneration?.options,
|
||||
startValue: parseInt(e.target.value) || 1,
|
||||
},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(config.autoGeneration?.type === "random_string" ||
|
||||
config.autoGeneration?.type === "random_number") && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="length">길이</Label>
|
||||
<Input
|
||||
id="length"
|
||||
type="number"
|
||||
value={
|
||||
config.autoGeneration?.options?.length ||
|
||||
(config.autoGeneration?.type === "random_string" ? 8 : 6)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration!,
|
||||
options: {
|
||||
...config.autoGeneration?.options,
|
||||
length: parseInt(e.target.value) || 8,
|
||||
},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="prefix">접두사</Label>
|
||||
<Input
|
||||
id="prefix"
|
||||
value={config.autoGeneration?.options?.prefix || ""}
|
||||
onChange={(e) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration!,
|
||||
options: {
|
||||
...config.autoGeneration?.options,
|
||||
prefix: e.target.value,
|
||||
},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="suffix">접미사</Label>
|
||||
<Input
|
||||
id="suffix"
|
||||
value={config.autoGeneration?.options?.suffix || ""}
|
||||
onChange={(e) => {
|
||||
const newAutoGeneration: AutoGenerationConfig = {
|
||||
...config.autoGeneration!,
|
||||
options: {
|
||||
...config.autoGeneration?.options,
|
||||
suffix: e.target.value,
|
||||
},
|
||||
};
|
||||
handleChange("autoGeneration", newAutoGeneration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="rounded bg-gray-50 p-2 text-xs">
|
||||
<strong>미리보기:</strong> {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ export class DateInputRenderer extends AutoRegisteringComponentRenderer {
|
|||
static componentDefinition = DateInputDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
console.log("🎯 DateInputRenderer.render() 호출:", {
|
||||
componentId: this.props.component?.id,
|
||||
autoGeneration: this.props.autoGeneration,
|
||||
componentAutoGeneration: this.props.component?.autoGeneration,
|
||||
allProps: Object.keys(this.props),
|
||||
});
|
||||
|
||||
return <DateInputComponent {...this.props} renderer={this} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React from "react";
|
|||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import type { WebType } from "@/types/screen";
|
||||
import { DateInputWrapper } from "./DateInputComponent";
|
||||
import { DateInputComponent } from "./DateInputComponent";
|
||||
import { DateInputConfigPanel } from "./DateInputConfigPanel";
|
||||
import { DateInputConfig } from "./types";
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ export const DateInputDefinition = createComponentDefinition({
|
|||
description: "날짜 선택을 위한 날짜 선택기 컴포넌트",
|
||||
category: ComponentCategory.INPUT,
|
||||
webType: "date",
|
||||
component: DateInputWrapper,
|
||||
component: DateInputComponent,
|
||||
defaultConfig: {
|
||||
placeholder: "입력하세요",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { ComponentConfig } from "@/types/component";
|
||||
import { AutoGenerationConfig } from "@/types/screen";
|
||||
|
||||
/**
|
||||
* DateInput 컴포넌트 설정 타입
|
||||
*/
|
||||
export interface DateInputConfig extends ComponentConfig {
|
||||
// date 관련 설정
|
||||
// date 관련 설정
|
||||
placeholder?: string;
|
||||
|
||||
// 공통 설정
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
helperText?: string;
|
||||
|
||||
// 자동생성 및 숨김 기능
|
||||
autoGeneration?: AutoGenerationConfig;
|
||||
hidden?: boolean;
|
||||
|
||||
// 스타일 관련
|
||||
variant?: "default" | "outlined" | "filled";
|
||||
size?: "sm" | "md" | "lg";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ComponentRendererProps, AutoGenerationConfig } from "@/types/component";
|
||||
import { NumberInputConfig } from "./types";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||
|
||||
export interface NumberInputComponentProps extends ComponentRendererProps {
|
||||
config?: NumberInputConfig;
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
}, [refreshKey]);
|
||||
|
||||
// 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가)
|
||||
// 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능)
|
||||
const visibleColumns = useMemo(() => {
|
||||
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
|
||||
const checkboxConfig = tableConfig.checkbox || {
|
||||
|
|
@ -554,9 +554,27 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (!displayColumns || displayColumns.length === 0) {
|
||||
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
|
||||
if (!tableConfig.columns) return [];
|
||||
columns = tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
columns = tableConfig.columns
|
||||
.filter((col) => {
|
||||
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
||||
if (isDesignMode) {
|
||||
return col.visible; // 디자인 모드에서는 visible만 체크
|
||||
} else {
|
||||
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
} else {
|
||||
columns = displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||
columns = displayColumns
|
||||
.filter((col) => {
|
||||
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
|
||||
if (isDesignMode) {
|
||||
return col.visible; // 디자인 모드에서는 visible만 체크
|
||||
} else {
|
||||
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// 체크박스가 활성화된 경우 체크박스 컬럼을 추가
|
||||
|
|
@ -876,6 +894,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
||||
)}
|
||||
style={{
|
||||
minWidth: `${getColumnWidth(column)}px`,
|
||||
|
|
@ -963,13 +983,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
{columnsByPosition.normal.map((column) => (
|
||||
<th
|
||||
key={`normal-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
style={{
|
||||
minWidth: `${getColumnWidth(column)}px`,
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
verticalAlign: "middle",
|
||||
lineHeight: "1",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-12 border-b px-4 py-3 text-center"
|
||||
: "cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
? "h-12 border-b px-4 py-3 text-center align-middle"
|
||||
: "cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
|
|
@ -1056,6 +1085,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
||||
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
||||
)}
|
||||
style={{
|
||||
minWidth: `${getColumnWidth(column)}px`,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,35 @@ export interface EntityJoinInfo {
|
|||
joinAlias: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동생성 타입 정의
|
||||
*/
|
||||
export type AutoGenerationType =
|
||||
| "uuid" // UUID 생성
|
||||
| "current_user" // 현재 사용자 ID
|
||||
| "current_time" // 현재 시간
|
||||
| "sequence" // 시퀀스 번호
|
||||
| "random_string" // 랜덤 문자열
|
||||
| "random_number" // 랜덤 숫자
|
||||
| "company_code" // 회사 코드
|
||||
| "department" // 부서 코드
|
||||
| "none"; // 자동생성 없음
|
||||
|
||||
/**
|
||||
* 자동생성 설정
|
||||
*/
|
||||
export interface AutoGenerationConfig {
|
||||
type: AutoGenerationType;
|
||||
enabled: boolean;
|
||||
options?: {
|
||||
length?: number; // 랜덤 문자열/숫자 길이
|
||||
prefix?: string; // 접두사
|
||||
suffix?: string; // 접미사
|
||||
format?: string; // 시간 형식 (current_time용)
|
||||
startValue?: number; // 시퀀스 시작값
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 설정
|
||||
*/
|
||||
|
|
@ -31,6 +60,10 @@ export interface ColumnConfig {
|
|||
// 컬럼 고정 관련 속성
|
||||
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
|
||||
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
|
||||
|
||||
// 새로운 기능들
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { AutoGenerationConfig } from "@/types/screen";
|
||||
import { TextInputConfig } from "./types";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||
|
||||
export interface TextInputComponentProps extends ComponentRendererProps {
|
||||
config?: TextInputConfig;
|
||||
|
|
@ -34,18 +36,112 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
...component.config,
|
||||
} as TextInputConfig;
|
||||
|
||||
// 자동생성 설정 (props에서 전달받은 값 우선 사용)
|
||||
const autoGeneration: AutoGenerationConfig = props.autoGeneration ||
|
||||
component.autoGeneration ||
|
||||
componentConfig.autoGeneration || {
|
||||
type: "none",
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
||||
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
||||
|
||||
// 자동생성된 값 상태
|
||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||
|
||||
// 테스트용: 컴포넌트 라벨에 "test"가 포함되면 강제로 UUID 자동생성 활성화
|
||||
const testAutoGeneration = component.label?.toLowerCase().includes("test")
|
||||
? {
|
||||
type: "uuid" as AutoGenerationType,
|
||||
enabled: true,
|
||||
}
|
||||
: autoGeneration;
|
||||
|
||||
console.log("🔧 텍스트 입력 컴포넌트 설정:", {
|
||||
config,
|
||||
componentConfig,
|
||||
component: component,
|
||||
autoGeneration,
|
||||
testAutoGeneration,
|
||||
isTestMode: component.label?.toLowerCase().includes("test"),
|
||||
isHidden,
|
||||
isInteractive,
|
||||
formData,
|
||||
columnName: component.columnName,
|
||||
currentFormValue: formData?.[component.columnName],
|
||||
componentValue: component.value,
|
||||
autoGeneratedValue,
|
||||
});
|
||||
|
||||
// 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시)
|
||||
useEffect(() => {
|
||||
console.log("🔄 자동생성 useEffect 실행:", {
|
||||
enabled: testAutoGeneration.enabled,
|
||||
type: testAutoGeneration.type,
|
||||
isInteractive,
|
||||
columnName: component.columnName,
|
||||
hasFormData: !!formData,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
});
|
||||
|
||||
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
|
||||
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
|
||||
const currentFormValue = formData?.[component.columnName];
|
||||
const currentComponentValue = component.value;
|
||||
|
||||
console.log("🔍 자동생성 조건 확인:", {
|
||||
currentFormValue,
|
||||
currentComponentValue,
|
||||
hasCurrentValue: !!(currentFormValue || currentComponentValue),
|
||||
autoGeneratedValue,
|
||||
});
|
||||
|
||||
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
||||
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
||||
const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
|
||||
console.log("✨ 자동생성된 값:", generatedValue);
|
||||
|
||||
if (generatedValue) {
|
||||
setAutoGeneratedValue(generatedValue);
|
||||
|
||||
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
|
||||
if (isInteractive && onFormDataChange && component.columnName) {
|
||||
console.log("📝 폼 데이터에 자동생성 값 설정:", {
|
||||
columnName: component.columnName,
|
||||
value: generatedValue,
|
||||
});
|
||||
onFormDataChange(component.columnName, generatedValue);
|
||||
}
|
||||
}
|
||||
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
|
||||
// 디자인 모드에서도 미리보기용 자동생성 값 표시
|
||||
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
|
||||
console.log("🎨 디자인 모드 미리보기 값:", previewValue);
|
||||
setAutoGeneratedValue(previewValue);
|
||||
} else {
|
||||
console.log("⏭️ 이미 값이 있어서 자동생성 건너뜀:", {
|
||||
hasAutoGenerated: !!autoGeneratedValue,
|
||||
hasFormValue: !!currentFormValue,
|
||||
hasComponentValue: !!currentComponentValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
||||
|
||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||
const componentStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
...component.style,
|
||||
...style,
|
||||
// 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김
|
||||
...(isHidden && {
|
||||
opacity: isDesignMode ? 0.4 : 0,
|
||||
backgroundColor: isDesignMode ? "#f3f4f6" : "transparent",
|
||||
pointerEvents: isDesignMode ? "auto" : "none",
|
||||
display: isDesignMode ? "block" : "none",
|
||||
}),
|
||||
};
|
||||
|
||||
// 디자인 모드 스타일
|
||||
|
|
@ -105,15 +201,37 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
|||
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
isInteractive && formData && component.columnName
|
||||
? formData[component.columnName] || ""
|
||||
: component.value || ""
|
||||
value={(() => {
|
||||
let displayValue = "";
|
||||
|
||||
if (isInteractive && formData && component.columnName) {
|
||||
// 인터랙티브 모드: formData 우선, 없으면 자동생성 값
|
||||
displayValue = formData[component.columnName] || autoGeneratedValue || "";
|
||||
} else {
|
||||
// 디자인 모드: component.value 우선, 없으면 자동생성 값
|
||||
displayValue = component.value || autoGeneratedValue || "";
|
||||
}
|
||||
|
||||
console.log("📄 Input 값 계산:", {
|
||||
isInteractive,
|
||||
hasFormData: !!formData,
|
||||
columnName: component.columnName,
|
||||
formDataValue: formData?.[component.columnName],
|
||||
componentValue: component.value,
|
||||
autoGeneratedValue,
|
||||
finalDisplayValue: displayValue,
|
||||
});
|
||||
|
||||
return displayValue;
|
||||
})()}
|
||||
placeholder={
|
||||
testAutoGeneration.enabled && testAutoGeneration.type !== "none"
|
||||
? `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}`
|
||||
: componentConfig.placeholder || ""
|
||||
}
|
||||
placeholder={componentConfig.placeholder || ""}
|
||||
disabled={componentConfig.disabled || false}
|
||||
required={componentConfig.required || false}
|
||||
readOnly={componentConfig.readonly || false}
|
||||
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { TextInputConfig } from "./types";
|
||||
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
||||
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
||||
|
||||
export interface TextInputConfigPanelProps {
|
||||
config: TextInputConfig;
|
||||
|
|
@ -16,21 +18,16 @@ export interface TextInputConfigPanelProps {
|
|||
* TextInput 설정 패널
|
||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||
*/
|
||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
}) => {
|
||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange }) => {
|
||||
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
||||
onChange({ [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">
|
||||
text-input 설정
|
||||
</div>
|
||||
<div className="text-sm font-medium">text-input 설정</div>
|
||||
|
||||
{/* 텍스트 관련 설정 */}
|
||||
{/* 텍스트 관련 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="placeholder">플레이스홀더</Label>
|
||||
<Input
|
||||
|
|
@ -77,6 +74,163 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({
|
|||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="mb-3 text-sm font-medium">고급 기능</div>
|
||||
|
||||
{/* 숨김 기능 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hidden">숨김 (편집기에서는 연하게, 실제 화면에서는 숨김)</Label>
|
||||
<Checkbox
|
||||
id="hidden"
|
||||
checked={config.hidden || false}
|
||||
onCheckedChange={(checked) => handleChange("hidden", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 자동생성 기능 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="autoGeneration">자동생성 활성화</Label>
|
||||
<Checkbox
|
||||
id="autoGeneration"
|
||||
checked={config.autoGeneration?.enabled || false}
|
||||
onCheckedChange={(checked) => {
|
||||
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
enabled: checked as boolean,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 자동생성 타입 선택 */}
|
||||
{config.autoGeneration?.enabled && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="autoGenerationType">자동생성 타입</Label>
|
||||
<Select
|
||||
value={config.autoGeneration?.type || "none"}
|
||||
onValueChange={(value: AutoGenerationType) => {
|
||||
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
type: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="자동생성 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">자동생성 없음</SelectItem>
|
||||
<SelectItem value="uuid">UUID 생성</SelectItem>
|
||||
<SelectItem value="current_user">현재 사용자 ID</SelectItem>
|
||||
<SelectItem value="current_time">현재 시간</SelectItem>
|
||||
<SelectItem value="sequence">순차 번호</SelectItem>
|
||||
<SelectItem value="random_string">랜덤 문자열</SelectItem>
|
||||
<SelectItem value="random_number">랜덤 숫자</SelectItem>
|
||||
<SelectItem value="company_code">회사 코드</SelectItem>
|
||||
<SelectItem value="department">부서 코드</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 선택된 타입 설명 */}
|
||||
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 자동생성 옵션 */}
|
||||
{config.autoGeneration?.enabled &&
|
||||
config.autoGeneration?.type &&
|
||||
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
|
||||
<div className="space-y-2">
|
||||
<Label>자동생성 옵션</Label>
|
||||
|
||||
{/* 길이 설정 (랜덤 문자열/숫자용) */}
|
||||
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="autoGenLength" className="text-xs">
|
||||
길이
|
||||
</Label>
|
||||
<Input
|
||||
id="autoGenLength"
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={
|
||||
config.autoGeneration?.options?.length || (config.autoGeneration.type === "random_string" ? 8 : 6)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const currentConfig = config.autoGeneration!;
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
options: {
|
||||
...currentConfig.options,
|
||||
length: parseInt(e.target.value) || 8,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 접두사 */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="autoGenPrefix" className="text-xs">
|
||||
접두사
|
||||
</Label>
|
||||
<Input
|
||||
id="autoGenPrefix"
|
||||
value={config.autoGeneration?.options?.prefix || ""}
|
||||
onChange={(e) => {
|
||||
const currentConfig = config.autoGeneration!;
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
options: {
|
||||
...currentConfig.options,
|
||||
prefix: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 접미사 */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="autoGenSuffix" className="text-xs">
|
||||
접미사
|
||||
</Label>
|
||||
<Input
|
||||
id="autoGenSuffix"
|
||||
value={config.autoGeneration?.options?.suffix || ""}
|
||||
onChange={(e) => {
|
||||
const currentConfig = config.autoGeneration!;
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
options: {
|
||||
...currentConfig.options,
|
||||
suffix: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 미리보기 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">미리보기</Label>
|
||||
<div className="rounded border bg-gray-100 p-2 text-xs">
|
||||
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { ComponentConfig } from "@/types/component";
|
||||
import { AutoGenerationConfig } from "@/types/screen";
|
||||
|
||||
/**
|
||||
* TextInput 컴포넌트 설정 타입
|
||||
*/
|
||||
export interface TextInputConfig extends ComponentConfig {
|
||||
// 텍스트 관련 설정
|
||||
// 텍스트 관련 설정
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
|
|
@ -27,6 +28,10 @@ export interface TextInputConfig extends ComponentConfig {
|
|||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onClick?: () => void;
|
||||
|
||||
// 새로운 기능들
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,6 +27,33 @@ export interface QuickValidationResult {
|
|||
success: boolean;
|
||||
message?: string;
|
||||
canExecuteImmediately: boolean;
|
||||
actions?: any[]; // 조건 만족 시 실행할 액션들
|
||||
}
|
||||
|
||||
/**
|
||||
* 제어 데이터 소스 타입
|
||||
*/
|
||||
export type ControlDataSource = "form" | "table-selection" | "both";
|
||||
|
||||
/**
|
||||
* 확장된 제어 컨텍스트
|
||||
*/
|
||||
export interface ExtendedControlContext {
|
||||
// 기존 폼 데이터
|
||||
formData: Record<string, any>;
|
||||
|
||||
// 테이블 선택 데이터
|
||||
selectedRows?: any[];
|
||||
selectedRowsData?: any[];
|
||||
|
||||
// 제어 데이터 소스 타입
|
||||
controlDataSource: ControlDataSource;
|
||||
|
||||
// 기타 컨텍스트
|
||||
buttonId: string;
|
||||
componentData?: any;
|
||||
timestamp: string;
|
||||
clickCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -289,7 +316,7 @@ export class OptimizedButtonDataflowService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 🔥 빠른 검증 (메모리에서 즉시 처리)
|
||||
* 🔥 빠른 검증 (메모리에서 즉시 처리) - 확장된 버전
|
||||
*/
|
||||
private static async executeQuickValidation(
|
||||
config: ButtonDataflowConfig,
|
||||
|
|
@ -326,6 +353,221 @@ export class OptimizedButtonDataflowService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 확장된 조건 검증 (폼 + 테이블 선택 데이터)
|
||||
*/
|
||||
static async executeExtendedValidation(
|
||||
config: ButtonDataflowConfig,
|
||||
context: ExtendedControlContext,
|
||||
): Promise<QuickValidationResult> {
|
||||
console.log("🔍 executeExtendedValidation 시작:", {
|
||||
controlMode: config.controlMode,
|
||||
controlDataSource: context.controlDataSource,
|
||||
selectedRowsData: context.selectedRowsData,
|
||||
formData: context.formData,
|
||||
directControlConditions: config.directControl?.conditions,
|
||||
selectedDiagramId: config.selectedDiagramId,
|
||||
selectedRelationshipId: config.selectedRelationshipId,
|
||||
fullConfig: config,
|
||||
});
|
||||
|
||||
// 🔥 simple 모드에서도 조건 검증을 수행해야 함
|
||||
// if (config.controlMode === "simple") {
|
||||
// return {
|
||||
// success: true,
|
||||
// canExecuteImmediately: true,
|
||||
// };
|
||||
// }
|
||||
|
||||
let conditions = config.directControl?.conditions || [];
|
||||
|
||||
// 🔥 관계도 방식인 경우 실제 API에서 조건 가져오기
|
||||
if (conditions.length === 0 && config.selectedDiagramId && config.selectedRelationshipId) {
|
||||
console.log("🔍 관계도 방식에서 실제 조건 로딩:", {
|
||||
selectedDiagramId: config.selectedDiagramId,
|
||||
selectedRelationshipId: config.selectedRelationshipId,
|
||||
});
|
||||
|
||||
try {
|
||||
// 관계도에서 실제 조건을 가져오는 API 호출
|
||||
const response = await apiClient.get(
|
||||
`/test-button-dataflow/diagrams/${config.selectedDiagramId}/relationships/${config.selectedRelationshipId}/preview`,
|
||||
);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const previewData = response.data.data;
|
||||
|
||||
// control.conditions에서 실제 조건 추출
|
||||
if (previewData.control && previewData.control.conditions) {
|
||||
conditions = previewData.control.conditions.filter((cond: any) => cond.type === "condition");
|
||||
console.log("✅ 관계도에서 control 조건 로딩 성공:", {
|
||||
totalConditions: previewData.control.conditions.length,
|
||||
filteredConditions: conditions.length,
|
||||
conditions,
|
||||
});
|
||||
} else {
|
||||
console.warn("⚠️ 관계도 control에 조건이 설정되지 않았습니다:", previewData);
|
||||
}
|
||||
|
||||
// 🔧 control.conditions가 비어있으면 plan.actions[].conditions에서 조건 추출
|
||||
if (conditions.length === 0 && previewData.plan && previewData.plan.actions) {
|
||||
console.log("🔍 control 조건이 없어서 액션별 조건 확인:", previewData.plan.actions);
|
||||
|
||||
previewData.plan.actions.forEach((action: any, index: number) => {
|
||||
if (action.conditions && Array.isArray(action.conditions) && action.conditions.length > 0) {
|
||||
conditions.push(...action.conditions);
|
||||
console.log(
|
||||
`✅ 액션 ${index + 1}(${action.name})에서 조건 ${action.conditions.length}개 로딩:`,
|
||||
action.conditions,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (conditions.length > 0) {
|
||||
console.log("✅ 총 액션별 조건 로딩 성공:", {
|
||||
totalConditions: conditions.length,
|
||||
conditions: conditions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// plan.actions에서 실제 액션도 저장 (조건 만족 시 실행용)
|
||||
if (previewData.plan && previewData.plan.actions) {
|
||||
// config에 액션 정보 임시 저장
|
||||
(config as any)._relationshipActions = previewData.plan.actions;
|
||||
console.log("✅ 관계도에서 실제 액션 로딩 성공:", {
|
||||
totalActions: previewData.plan.actions.length,
|
||||
actions: previewData.plan.actions,
|
||||
});
|
||||
} else {
|
||||
console.warn("⚠️ 관계도에 액션이 설정되지 않았습니다:", {
|
||||
hasPlan: !!previewData.plan,
|
||||
planActions: previewData.plan?.actions,
|
||||
fullPreviewData: previewData,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("⚠️ 관계도 API 응답이 올바르지 않습니다:", response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 관계도에서 조건 로딩 실패:", error);
|
||||
|
||||
// API 실패 시 사용자에게 명확한 안내
|
||||
return {
|
||||
success: false,
|
||||
message: "관계도에서 조건을 불러올 수 없습니다. 관계도 설정을 확인해주세요.",
|
||||
canExecuteImmediately: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 여전히 조건이 없으면 경고
|
||||
if (conditions.length === 0) {
|
||||
console.warn("⚠️ 제어 조건이 설정되지 않았습니다.");
|
||||
return {
|
||||
success: false,
|
||||
message: "제어 조건이 설정되지 않았습니다. 관계도에서 관계를 선택하거나 직접 조건을 추가해주세요.",
|
||||
canExecuteImmediately: true,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("🔍 조건 검증 시작:", conditions);
|
||||
|
||||
for (const condition of conditions) {
|
||||
if (condition.type === "condition") {
|
||||
let dataToCheck: Record<string, any> = {};
|
||||
|
||||
// 제어 데이터 소스에 따라 검증할 데이터 결정
|
||||
console.log("🔍 제어 데이터 소스 확인:", {
|
||||
controlDataSource: context.controlDataSource,
|
||||
hasFormData: Object.keys(context.formData).length > 0,
|
||||
hasSelectedRowsData: context.selectedRowsData && context.selectedRowsData.length > 0,
|
||||
selectedRowsData: context.selectedRowsData,
|
||||
});
|
||||
|
||||
switch (context.controlDataSource) {
|
||||
case "form":
|
||||
dataToCheck = context.formData;
|
||||
console.log("📝 폼 데이터 사용:", dataToCheck);
|
||||
break;
|
||||
|
||||
case "table-selection":
|
||||
// 선택된 첫 번째 행의 데이터 사용 (다중 선택 시)
|
||||
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
|
||||
dataToCheck = context.selectedRowsData[0];
|
||||
console.log("📋 테이블 선택 데이터 사용:", dataToCheck);
|
||||
} else {
|
||||
console.warn("⚠️ 테이블에서 항목이 선택되지 않음");
|
||||
return {
|
||||
success: false,
|
||||
message: "테이블에서 항목을 선택해주세요.",
|
||||
canExecuteImmediately: true,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "both":
|
||||
// 폼 데이터와 선택된 데이터 모두 병합
|
||||
dataToCheck = {
|
||||
...context.formData,
|
||||
...(context.selectedRowsData?.[0] || {}),
|
||||
};
|
||||
console.log("🔄 폼 + 테이블 데이터 병합 사용:", dataToCheck);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 기본값이 없는 경우 자동 판단
|
||||
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
|
||||
dataToCheck = context.selectedRowsData[0];
|
||||
console.log("🔄 자동 판단: 테이블 선택 데이터 사용:", dataToCheck);
|
||||
} else {
|
||||
dataToCheck = context.formData;
|
||||
console.log("🔄 자동 판단: 폼 데이터 사용:", dataToCheck);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const fieldValue = dataToCheck[condition.field!];
|
||||
const isValid = this.evaluateSimpleCondition(fieldValue, condition.operator!, condition.value);
|
||||
|
||||
console.log("🔍 조건 검증 결과:", {
|
||||
field: condition.field,
|
||||
operator: condition.operator,
|
||||
expectedValue: condition.value,
|
||||
actualValue: fieldValue,
|
||||
isValid,
|
||||
dataToCheck,
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
const sourceLabel =
|
||||
context.controlDataSource === "form"
|
||||
? "폼"
|
||||
: context.controlDataSource === "table-selection"
|
||||
? "선택된 항목"
|
||||
: "데이터";
|
||||
|
||||
const actualValueMsg = fieldValue !== undefined ? ` (실제값: ${fieldValue})` : " (값 없음)";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `${sourceLabel} 조건 불만족: ${condition.field} ${condition.operator} ${condition.value}${actualValueMsg}`,
|
||||
canExecuteImmediately: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 모든 조건을 만족했으므로 액션 정보도 함께 반환
|
||||
const relationshipActions = (config as any)._relationshipActions || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
canExecuteImmediately: true,
|
||||
actions: relationshipActions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 단순 조건 평가 (메모리에서 즉시)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,292 @@
|
|||
"use client";
|
||||
|
||||
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
|
||||
|
||||
/**
|
||||
* 자동생성 값 생성 유틸리티
|
||||
*/
|
||||
export class AutoGenerationUtils {
|
||||
/**
|
||||
* UUID 생성
|
||||
*/
|
||||
static generateUUID(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 문자열 생성
|
||||
*/
|
||||
static generateRandomString(length: number = 8, prefix?: string, suffix?: string): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return `${prefix || ""}${result}${suffix || ""}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 랜덤 숫자 생성
|
||||
*/
|
||||
static generateRandomNumber(length: number = 6, prefix?: string, suffix?: string): string {
|
||||
const min = Math.pow(10, length - 1);
|
||||
const max = Math.pow(10, length) - 1;
|
||||
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
return `${prefix || ""}${randomNum}${suffix || ""}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간 생성
|
||||
*/
|
||||
static generateCurrentTime(format?: string): string {
|
||||
console.log("🕒 generateCurrentTime 호출됨:", { format });
|
||||
const now = new Date();
|
||||
console.log("🕒 현재 시간 객체:", now);
|
||||
|
||||
let result: string;
|
||||
switch (format) {
|
||||
case "date":
|
||||
result = now.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
break;
|
||||
case "time":
|
||||
result = now.toTimeString().split(" ")[0]; // HH:mm:ss
|
||||
break;
|
||||
case "datetime":
|
||||
result = now.toISOString().replace("T", " ").split(".")[0]; // YYYY-MM-DD HH:mm:ss
|
||||
break;
|
||||
case "timestamp":
|
||||
result = now.getTime().toString();
|
||||
break;
|
||||
default:
|
||||
result = now.toISOString(); // ISO 8601 format
|
||||
break;
|
||||
}
|
||||
|
||||
console.log("🕒 generateCurrentTime 결과:", { format, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시퀀스 번호 생성 (메모리 기반, 실제로는 DB 시퀀스 사용 권장)
|
||||
*/
|
||||
private static sequenceCounters: Map<string, number> = new Map();
|
||||
|
||||
static generateSequence(key: string = "default", startValue: number = 1, prefix?: string, suffix?: string): string {
|
||||
if (!this.sequenceCounters.has(key)) {
|
||||
this.sequenceCounters.set(key, startValue);
|
||||
}
|
||||
|
||||
const current = this.sequenceCounters.get(key)!;
|
||||
this.sequenceCounters.set(key, current + 1);
|
||||
|
||||
return `${prefix || ""}${current}${suffix || ""}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자 ID 가져오기 (실제로는 인증 컨텍스트에서 가져와야 함)
|
||||
*/
|
||||
static getCurrentUserId(): string {
|
||||
// TODO: 실제 인증 시스템과 연동
|
||||
if (typeof window !== "undefined") {
|
||||
const userInfo = localStorage.getItem("userInfo");
|
||||
if (userInfo) {
|
||||
try {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
return parsed.userId || parsed.id || "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사 코드 가져오기
|
||||
*/
|
||||
static getCompanyCode(): string {
|
||||
// TODO: 실제 회사 정보와 연동
|
||||
if (typeof window !== "undefined") {
|
||||
const companyInfo = localStorage.getItem("companyInfo");
|
||||
if (companyInfo) {
|
||||
try {
|
||||
const parsed = JSON.parse(companyInfo);
|
||||
return parsed.companyCode || parsed.code || "COMPANY";
|
||||
} catch {
|
||||
return "COMPANY";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "COMPANY";
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 코드 가져오기
|
||||
*/
|
||||
static getDepartmentCode(): string {
|
||||
// TODO: 실제 부서 정보와 연동
|
||||
if (typeof window !== "undefined") {
|
||||
const userInfo = localStorage.getItem("userInfo");
|
||||
if (userInfo) {
|
||||
try {
|
||||
const parsed = JSON.parse(userInfo);
|
||||
return parsed.departmentCode || parsed.deptCode || "DEPT";
|
||||
} catch {
|
||||
return "DEPT";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "DEPT";
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동생성 값 생성 메인 함수
|
||||
*/
|
||||
static generateValue(config: AutoGenerationConfig, columnName?: string): string | null {
|
||||
console.log("🔧 AutoGenerationUtils.generateValue 호출:", {
|
||||
config,
|
||||
columnName,
|
||||
enabled: config.enabled,
|
||||
type: config.type,
|
||||
});
|
||||
|
||||
if (!config.enabled || config.type === "none") {
|
||||
console.log("⚠️ AutoGenerationUtils.generateValue 스킵:", {
|
||||
enabled: config.enabled,
|
||||
type: config.type,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = config.options || {};
|
||||
|
||||
switch (config.type) {
|
||||
case "uuid":
|
||||
return this.generateUUID();
|
||||
|
||||
case "current_user":
|
||||
return this.getCurrentUserId();
|
||||
|
||||
case "current_time":
|
||||
console.log("🕒 AutoGenerationUtils.generateCurrentTime 호출:", {
|
||||
format: options.format,
|
||||
options,
|
||||
});
|
||||
const timeValue = this.generateCurrentTime(options.format);
|
||||
console.log("🕒 AutoGenerationUtils.generateCurrentTime 결과:", timeValue);
|
||||
return timeValue;
|
||||
|
||||
case "sequence":
|
||||
return this.generateSequence(columnName || "default", options.startValue || 1, options.prefix, options.suffix);
|
||||
|
||||
case "random_string":
|
||||
return this.generateRandomString(options.length || 8, options.prefix, options.suffix);
|
||||
|
||||
case "random_number":
|
||||
return this.generateRandomNumber(options.length || 6, options.prefix, options.suffix);
|
||||
|
||||
case "company_code":
|
||||
return this.getCompanyCode();
|
||||
|
||||
case "department":
|
||||
return this.getDepartmentCode();
|
||||
|
||||
default:
|
||||
console.warn(`Unknown auto generation type: ${config.type}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동생성 타입별 설명 가져오기
|
||||
*/
|
||||
static getTypeDescription(type: AutoGenerationType): string {
|
||||
const descriptions: Record<AutoGenerationType, string> = {
|
||||
uuid: "고유 식별자 (UUID) 생성",
|
||||
current_user: "현재 로그인한 사용자 ID",
|
||||
current_time: "현재 날짜/시간",
|
||||
sequence: "순차적 번호 생성",
|
||||
random_string: "랜덤 문자열 생성",
|
||||
random_number: "랜덤 숫자 생성",
|
||||
company_code: "현재 회사 코드",
|
||||
department: "현재 부서 코드",
|
||||
none: "자동생성 없음",
|
||||
};
|
||||
|
||||
return descriptions[type] || "알 수 없는 타입";
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동생성 미리보기 값 생성
|
||||
*/
|
||||
static generatePreviewValue(config: AutoGenerationConfig): string {
|
||||
if (!config.enabled || config.type === "none") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 미리보기용으로 실제 값 대신 예시 값 반환
|
||||
const options = config.options || {};
|
||||
|
||||
switch (config.type) {
|
||||
case "uuid":
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
|
||||
|
||||
case "current_user":
|
||||
return "user123";
|
||||
|
||||
case "current_time":
|
||||
return this.generateCurrentTime(options.format);
|
||||
|
||||
case "sequence":
|
||||
return `${options.prefix || ""}1${options.suffix || ""}`;
|
||||
|
||||
case "random_string":
|
||||
return `${options.prefix || ""}ABC123${options.suffix || ""}`;
|
||||
|
||||
case "random_number":
|
||||
return `${options.prefix || ""}123456${options.suffix || ""}`;
|
||||
|
||||
case "company_code":
|
||||
return "COMPANY";
|
||||
|
||||
case "department":
|
||||
return "DEPT";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 개발 모드에서 전역 함수로 등록 (테스트용)
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).__AUTO_GENERATION_TEST__ = {
|
||||
generateCurrentTime: AutoGenerationUtils.generateCurrentTime,
|
||||
generateValue: AutoGenerationUtils.generateValue,
|
||||
test: () => {
|
||||
console.log("🧪 자동생성 테스트 시작");
|
||||
|
||||
// 현재 시간 테스트
|
||||
const dateResult = AutoGenerationUtils.generateCurrentTime("date");
|
||||
console.log("📅 날짜 생성 결과:", dateResult);
|
||||
|
||||
// 자동생성 설정 테스트
|
||||
const config = {
|
||||
enabled: true,
|
||||
type: "current_time" as any,
|
||||
options: { format: "date" },
|
||||
};
|
||||
|
||||
const autoResult = AutoGenerationUtils.generateValue(config);
|
||||
console.log("🎯 자동생성 결과:", autoResult);
|
||||
|
||||
return { dateResult, autoResult };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { toast } from "sonner";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
import { OptimizedButtonDataflowService, ExtendedControlContext } from "@/lib/services/optimizedButtonDataflowService";
|
||||
|
||||
/**
|
||||
* 버튼 액션 타입 정의
|
||||
|
|
@ -46,6 +47,10 @@ export interface ButtonActionConfig {
|
|||
confirmMessage?: string;
|
||||
successMessage?: string;
|
||||
errorMessage?: string;
|
||||
|
||||
// 제어관리 관련
|
||||
enableDataflowControl?: boolean;
|
||||
dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,6 +121,9 @@ export class ButtonActionExecutor {
|
|||
case "close":
|
||||
return this.handleClose(config, context);
|
||||
|
||||
case "control":
|
||||
return this.handleControl(config, context);
|
||||
|
||||
default:
|
||||
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
||||
return false;
|
||||
|
|
@ -712,6 +720,497 @@ export class ButtonActionExecutor {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 제어 전용 액션 처리 (조건 체크만 수행)
|
||||
*/
|
||||
private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
console.log("🎯 ButtonActionExecutor.handleControl 실행:", {
|
||||
formData: context.formData,
|
||||
selectedRows: context.selectedRows,
|
||||
selectedRowsData: context.selectedRowsData,
|
||||
config,
|
||||
});
|
||||
|
||||
// 🔥 제어 조건이 설정되어 있는지 확인
|
||||
console.log("🔍 제어관리 활성화 상태 확인:", {
|
||||
enableDataflowControl: config.enableDataflowControl,
|
||||
hasDataflowConfig: !!config.dataflowConfig,
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
fullConfig: config,
|
||||
});
|
||||
|
||||
if (!config.dataflowConfig || !config.enableDataflowControl) {
|
||||
console.warn("⚠️ 제어관리가 비활성화되어 있습니다:", {
|
||||
enableDataflowControl: config.enableDataflowControl,
|
||||
hasDataflowConfig: !!config.dataflowConfig,
|
||||
});
|
||||
toast.warning(
|
||||
"제어관리가 활성화되지 않았습니다. 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 🔥 확장된 제어 컨텍스트 생성
|
||||
// 자동으로 적절한 controlDataSource 결정
|
||||
let controlDataSource = config.dataflowConfig.controlDataSource;
|
||||
|
||||
if (!controlDataSource) {
|
||||
// 설정이 없으면 자동 판단
|
||||
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
|
||||
controlDataSource = "table-selection";
|
||||
console.log("🔄 자동 판단: table-selection 모드 사용");
|
||||
} else if (context.formData && Object.keys(context.formData).length > 0) {
|
||||
controlDataSource = "form";
|
||||
console.log("🔄 자동 판단: form 모드 사용");
|
||||
} else {
|
||||
controlDataSource = "form"; // 기본값
|
||||
console.log("🔄 기본값: form 모드 사용");
|
||||
}
|
||||
}
|
||||
|
||||
const extendedContext: ExtendedControlContext = {
|
||||
formData: context.formData || {},
|
||||
selectedRows: context.selectedRows || [],
|
||||
selectedRowsData: context.selectedRowsData || [],
|
||||
controlDataSource,
|
||||
};
|
||||
|
||||
console.log("🔍 제어 조건 검증 시작:", {
|
||||
dataflowConfig: config.dataflowConfig,
|
||||
extendedContext,
|
||||
});
|
||||
|
||||
// 🔥 실제 제어 조건 검증 수행
|
||||
const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation(
|
||||
config.dataflowConfig,
|
||||
extendedContext,
|
||||
);
|
||||
|
||||
if (validationResult.success) {
|
||||
console.log("✅ 제어 조건 만족 - 액션 실행 시작:", {
|
||||
actions: validationResult.actions,
|
||||
context,
|
||||
});
|
||||
|
||||
// 🔥 조건을 만족했으므로 실제 액션 실행
|
||||
if (validationResult.actions && validationResult.actions.length > 0) {
|
||||
console.log("🚀 액션 실행 시작:", validationResult.actions);
|
||||
await this.executeRelationshipActions(validationResult.actions, context);
|
||||
} else {
|
||||
console.warn("⚠️ 실행할 액션이 없습니다:", {
|
||||
hasActions: !!validationResult.actions,
|
||||
actionsLength: validationResult.actions?.length,
|
||||
validationResult,
|
||||
});
|
||||
toast.success(config.successMessage || "제어 조건을 만족합니다. (실행할 액션 없음)");
|
||||
}
|
||||
|
||||
// 새로고침이 필요한 경우
|
||||
if (context.onRefresh) {
|
||||
context.onRefresh();
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
toast.error(validationResult.message || "제어 조건을 만족하지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("제어 조건 검증 중 오류:", error);
|
||||
toast.error("제어 조건 검증 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계도에서 가져온 액션들을 실행
|
||||
*/
|
||||
private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise<void> {
|
||||
console.log("🚀 관계도 액션 실행 시작:", actions);
|
||||
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
const action = actions[i];
|
||||
|
||||
try {
|
||||
console.log(`🔄 액션 ${i + 1}/${actions.length} 실행:`, action);
|
||||
|
||||
const actionType = action.actionType || action.type; // actionType 우선, type 폴백
|
||||
|
||||
switch (actionType) {
|
||||
case "save":
|
||||
await this.executeActionSave(action, context);
|
||||
break;
|
||||
case "update":
|
||||
await this.executeActionUpdate(action, context);
|
||||
break;
|
||||
case "delete":
|
||||
await this.executeActionDelete(action, context);
|
||||
break;
|
||||
case "insert":
|
||||
await this.executeActionInsert(action, context);
|
||||
break;
|
||||
default:
|
||||
console.warn(`❌ 지원되지 않는 액션 타입 (${i + 1}/${actions.length}):`, {
|
||||
actionType,
|
||||
actionName: action.name,
|
||||
fullAction: action,
|
||||
});
|
||||
// 지원되지 않는 액션은 오류로 처리하여 중단
|
||||
throw new Error(`지원되지 않는 액션 타입: ${actionType}`);
|
||||
}
|
||||
|
||||
console.log(`✅ 액션 ${i + 1}/${actions.length} 완료:`, action.name);
|
||||
|
||||
// 성공 토스트 (개별 액션별)
|
||||
toast.success(`${action.name || `액션 ${i + 1}`} 완료`);
|
||||
} catch (error) {
|
||||
const actionType = action.actionType || action.type;
|
||||
console.error(`❌ 액션 ${i + 1}/${actions.length} 실행 실패:`, action.name, error);
|
||||
|
||||
// 실패 토스트
|
||||
toast.error(
|
||||
`${action.name || `액션 ${i + 1}`} 실행 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`,
|
||||
);
|
||||
|
||||
// 🚨 순차 실행 중단: 하나라도 실패하면 전체 중단
|
||||
throw new Error(
|
||||
`액션 ${i + 1}(${action.name})에서 실패하여 제어 프로세스를 중단합니다: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 모든 액션 실행 완료!");
|
||||
toast.success(`총 ${actions.length}개 액션이 모두 성공적으로 완료되었습니다.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 액션 실행
|
||||
*/
|
||||
private static async executeActionSave(action: any, context: ButtonActionContext): Promise<void> {
|
||||
console.log("💾 저장 액션 실행:", action);
|
||||
console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2));
|
||||
|
||||
// 🎯 필드 매핑 정보 사용하여 저장 데이터 구성
|
||||
let saveData: Record<string, any> = {};
|
||||
|
||||
// 액션에 필드 매핑 정보가 있는지 확인
|
||||
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
|
||||
console.log("📋 필드 매핑 정보 발견:", action.fieldMappings);
|
||||
|
||||
// 필드 매핑에 따라 데이터 구성
|
||||
action.fieldMappings.forEach((mapping: any) => {
|
||||
const { sourceField, targetField, defaultValue, valueType } = mapping;
|
||||
|
||||
let value: any;
|
||||
|
||||
// 값 소스에 따라 데이터 가져오기
|
||||
if (valueType === "form" && context.formData && sourceField) {
|
||||
value = context.formData[sourceField];
|
||||
} else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
|
||||
value = context.selectedRowsData[0][sourceField];
|
||||
} else if (valueType === "default" || !sourceField) {
|
||||
value = defaultValue;
|
||||
}
|
||||
|
||||
// 타겟 필드에 값 설정
|
||||
if (targetField && value !== undefined) {
|
||||
saveData[targetField] = value;
|
||||
console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
|
||||
// 폴백: 기존 방식
|
||||
saveData = {
|
||||
...context.formData,
|
||||
...context.selectedRowsData?.[0], // 선택된 데이터도 포함
|
||||
};
|
||||
}
|
||||
|
||||
console.log("📊 최종 저장할 데이터:", saveData);
|
||||
|
||||
try {
|
||||
// 🔥 실제 저장 API 호출
|
||||
if (!context.tableName) {
|
||||
throw new Error("테이블명이 설정되지 않았습니다.");
|
||||
}
|
||||
|
||||
const result = await DynamicFormApi.saveFormData({
|
||||
screenId: 0, // 임시값
|
||||
tableName: context.tableName,
|
||||
data: saveData,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ 저장 성공:", result);
|
||||
toast.success("데이터가 저장되었습니다.");
|
||||
} else {
|
||||
throw new Error(result.message || "저장 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 저장 실패:", error);
|
||||
toast.error(`저장 실패: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업데이트 액션 실행
|
||||
*/
|
||||
private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise<void> {
|
||||
console.log("🔄 업데이트 액션 실행:", action);
|
||||
console.log("🔍 액션 상세 정보:", JSON.stringify(action, null, 2));
|
||||
|
||||
// 🎯 필드 매핑 정보 사용하여 업데이트 데이터 구성
|
||||
let updateData: Record<string, any> = {};
|
||||
|
||||
// 액션에 필드 매핑 정보가 있는지 확인
|
||||
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
|
||||
console.log("📋 필드 매핑 정보 발견:", action.fieldMappings);
|
||||
|
||||
// 🔑 먼저 선택된 데이터의 모든 필드를 기본으로 포함 (기본키 보존)
|
||||
if (context.selectedRowsData?.[0]) {
|
||||
updateData = { ...context.selectedRowsData[0] };
|
||||
console.log("🔑 선택된 데이터를 기본으로 설정 (기본키 보존):", updateData);
|
||||
}
|
||||
|
||||
// 필드 매핑에 따라 데이터 구성 (덮어쓰기)
|
||||
action.fieldMappings.forEach((mapping: any) => {
|
||||
const { sourceField, targetField, defaultValue, valueType } = mapping;
|
||||
|
||||
let value: any;
|
||||
|
||||
// 값 소스에 따라 데이터 가져오기
|
||||
if (valueType === "form" && context.formData && sourceField) {
|
||||
value = context.formData[sourceField];
|
||||
} else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
|
||||
value = context.selectedRowsData[0][sourceField];
|
||||
} else if (valueType === "default" || !sourceField) {
|
||||
value = defaultValue;
|
||||
}
|
||||
|
||||
// 타겟 필드에 값 설정 (덮어쓰기)
|
||||
if (targetField && value !== undefined) {
|
||||
updateData[targetField] = value;
|
||||
console.log(`📝 필드 매핑: ${sourceField || "default"} -> ${targetField} = ${value}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
|
||||
// 폴백: 기존 방식
|
||||
updateData = {
|
||||
...context.formData,
|
||||
...context.selectedRowsData?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
console.log("📊 최종 업데이트할 데이터:", updateData);
|
||||
|
||||
try {
|
||||
// 🔥 실제 업데이트 API 호출
|
||||
if (!context.tableName) {
|
||||
throw new Error("테이블명이 설정되지 않았습니다.");
|
||||
}
|
||||
|
||||
// 먼저 ID 찾기
|
||||
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
|
||||
let updateId: string | undefined;
|
||||
|
||||
if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
|
||||
updateId = updateData[primaryKeysResult.data[0]];
|
||||
}
|
||||
|
||||
if (!updateId) {
|
||||
// 폴백: 일반적인 ID 필드들 확인
|
||||
const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
|
||||
for (const field of commonIdFields) {
|
||||
if (updateData[field]) {
|
||||
updateId = updateData[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateId) {
|
||||
throw new Error("업데이트할 항목의 ID를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const result = await DynamicFormApi.updateFormData(updateId, {
|
||||
tableName: context.tableName,
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ 업데이트 성공:", result);
|
||||
toast.success("데이터가 업데이트되었습니다.");
|
||||
} else {
|
||||
throw new Error(result.message || "업데이트 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 업데이트 실패:", error);
|
||||
toast.error(`업데이트 실패: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 액션 실행
|
||||
*/
|
||||
private static async executeActionDelete(action: any, context: ButtonActionContext): Promise<void> {
|
||||
console.log("🗑️ 삭제 액션 실행:", action);
|
||||
|
||||
// 실제 삭제 로직 (기존 handleDelete와 유사)
|
||||
if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
|
||||
throw new Error("삭제할 항목을 선택해주세요.");
|
||||
}
|
||||
|
||||
const deleteData = context.selectedRowsData[0];
|
||||
console.log("삭제할 데이터:", deleteData);
|
||||
|
||||
try {
|
||||
// 🔥 실제 삭제 API 호출
|
||||
if (!context.tableName) {
|
||||
throw new Error("테이블명이 설정되지 않았습니다.");
|
||||
}
|
||||
|
||||
// 기존 handleDelete와 동일한 로직으로 ID 찾기
|
||||
const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
|
||||
let deleteId: string | undefined;
|
||||
|
||||
if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
|
||||
deleteId = deleteData[primaryKeysResult.data[0]];
|
||||
}
|
||||
|
||||
if (!deleteId) {
|
||||
// 폴백: 일반적인 ID 필드들 확인
|
||||
const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
|
||||
for (const field of commonIdFields) {
|
||||
if (deleteData[field]) {
|
||||
deleteId = deleteData[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!deleteId) {
|
||||
throw new Error("삭제할 항목의 ID를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName);
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ 삭제 성공:", result);
|
||||
toast.success("데이터가 삭제되었습니다.");
|
||||
} else {
|
||||
throw new Error(result.message || "삭제 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 삭제 실패:", error);
|
||||
toast.error(`삭제 실패: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 삽입 액션 실행 (체크박스 선택된 데이터를 필드매핑에 따라 새 테이블에 삽입)
|
||||
*/
|
||||
private static async executeActionInsert(action: any, context: ButtonActionContext): Promise<void> {
|
||||
console.log("➕ 삽입 액션 실행:", action);
|
||||
|
||||
let insertData: Record<string, any> = {};
|
||||
|
||||
// 액션에 필드 매핑 정보가 있는지 확인
|
||||
if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
|
||||
console.log("📋 삽입 액션 - 필드 매핑 정보:", action.fieldMappings);
|
||||
|
||||
// 🎯 체크박스로 선택된 데이터가 있는지 확인
|
||||
if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
|
||||
throw new Error("삽입할 소스 데이터를 선택해주세요. (테이블에서 체크박스 선택 필요)");
|
||||
}
|
||||
|
||||
const sourceData = context.selectedRowsData[0]; // 첫 번째 선택된 데이터 사용
|
||||
console.log("🎯 삽입 소스 데이터 (체크박스 선택):", sourceData);
|
||||
console.log("🔍 소스 데이터 사용 가능한 키들:", Object.keys(sourceData));
|
||||
|
||||
// 필드 매핑에 따라 데이터 구성
|
||||
action.fieldMappings.forEach((mapping: any) => {
|
||||
const { sourceField, targetField, defaultValue } = mapping;
|
||||
// valueType이 없으면 기본값을 "selection"으로 설정
|
||||
const valueType = mapping.valueType || "selection";
|
||||
|
||||
let value: any;
|
||||
|
||||
console.log(`🔍 매핑 처리 중: ${sourceField} → ${targetField} (valueType: ${valueType})`);
|
||||
|
||||
// 값 소스에 따라 데이터 가져오기
|
||||
if (valueType === "form" && context.formData && sourceField) {
|
||||
// 폼 데이터에서 가져오기
|
||||
value = context.formData[sourceField];
|
||||
console.log(`📝 폼에서 매핑: ${sourceField} → ${targetField} = ${value}`);
|
||||
} else if (valueType === "selection" && sourceField) {
|
||||
// 선택된 테이블 데이터에서 가져오기 (다양한 필드명 시도)
|
||||
value =
|
||||
sourceData[sourceField] ||
|
||||
sourceData[sourceField + "_name"] || // 조인된 필드 (_name 접미사)
|
||||
sourceData[sourceField + "Name"]; // 카멜케이스
|
||||
console.log(`📊 테이블에서 매핑: ${sourceField} → ${targetField} = ${value} (소스필드: ${sourceField})`);
|
||||
} else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) {
|
||||
// 기본값 사용 (valueType이 "default"이거나 defaultValue가 있을 때)
|
||||
value = defaultValue;
|
||||
console.log(`🔧 기본값 매핑: ${targetField} = ${value}`);
|
||||
} else {
|
||||
console.warn(`⚠️ 매핑 실패: ${sourceField} → ${targetField} (값을 찾을 수 없음)`);
|
||||
console.warn(` - valueType: ${valueType}, defaultValue: ${defaultValue}`);
|
||||
console.warn(` - 소스 데이터 키들:`, Object.keys(sourceData));
|
||||
console.warn(` - sourceData[${sourceField}] =`, sourceData[sourceField]);
|
||||
return; // 값이 없으면 해당 필드는 스킵
|
||||
}
|
||||
|
||||
// 대상 필드에 값 설정
|
||||
if (targetField && value !== undefined && value !== null) {
|
||||
insertData[targetField] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🎯 최종 삽입 데이터 (필드매핑 적용):", insertData);
|
||||
} else {
|
||||
// 필드 매핑이 없으면 폼 데이터를 기본으로 사용
|
||||
insertData = { ...context.formData };
|
||||
console.log("📝 기본 삽입 데이터 (폼 기반):", insertData);
|
||||
}
|
||||
|
||||
try {
|
||||
// 🔥 실제 삽입 API 호출 - 필수 매개변수 포함
|
||||
// 필드 매핑에서 첫 번째 targetTable을 찾거나 기본값 사용
|
||||
const targetTable = action.fieldMappings?.[0]?.targetTable || action.targetTable || "test_project_info";
|
||||
|
||||
const formDataPayload = {
|
||||
screenId: 0, // 제어 관리에서는 screenId가 없으므로 0 사용
|
||||
tableName: targetTable, // 필드 매핑에서 대상 테이블명 가져오기
|
||||
data: insertData,
|
||||
};
|
||||
|
||||
console.log("🎯 대상 테이블:", targetTable);
|
||||
console.log("📋 삽입할 데이터:", insertData);
|
||||
|
||||
console.log("💾 폼 데이터 저장 요청:", formDataPayload);
|
||||
|
||||
const result = await DynamicFormApi.saveFormData(formDataPayload);
|
||||
|
||||
if (result.success) {
|
||||
console.log("✅ 삽입 성공:", result);
|
||||
toast.success(`데이터가 타겟 테이블에 성공적으로 삽입되었습니다.`);
|
||||
} else {
|
||||
throw new Error(result.message || "삽입 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 삽입 실패:", error);
|
||||
toast.error(`삽입 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 유효성 검사
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ export interface ComponentSize {
|
|||
height: number;
|
||||
}
|
||||
|
||||
// screen.ts에서 자동생성 관련 타입들을 import
|
||||
export type { AutoGenerationType, AutoGenerationConfig } from "./screen";
|
||||
|
||||
/**
|
||||
* 컴포넌트 렌더러 Props
|
||||
* 모든 컴포넌트 렌더러가 받는 공통 Props
|
||||
|
|
@ -61,6 +64,11 @@ export interface ComponentRendererProps {
|
|||
style?: React.CSSProperties;
|
||||
formData?: Record<string, any>;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
|
||||
// 새로운 기능들
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ export type ButtonActionType =
|
|||
| "popup" // 팝업 열기
|
||||
| "modal" // 모달 열기
|
||||
| "newWindow" // 새 창 열기
|
||||
| "navigate"; // 페이지 이동
|
||||
| "navigate" // 페이지 이동
|
||||
| "control"; // 제어 전용 (조건 체크만)
|
||||
|
||||
// 위치 정보
|
||||
export interface Position {
|
||||
|
|
@ -155,6 +156,31 @@ export interface ComponentStyle {
|
|||
}
|
||||
|
||||
// BaseComponent에 스타일 속성 추가
|
||||
// 자동생성 타입 정의
|
||||
export type AutoGenerationType =
|
||||
| "uuid" // UUID 생성
|
||||
| "current_user" // 현재 사용자 ID
|
||||
| "current_time" // 현재 시간
|
||||
| "sequence" // 시퀀스 번호
|
||||
| "random_string" // 랜덤 문자열
|
||||
| "random_number" // 랜덤 숫자
|
||||
| "company_code" // 회사 코드
|
||||
| "department" // 부서 코드
|
||||
| "none"; // 자동생성 없음
|
||||
|
||||
// 자동생성 설정
|
||||
export interface AutoGenerationConfig {
|
||||
type: AutoGenerationType;
|
||||
enabled: boolean;
|
||||
options?: {
|
||||
length?: number; // 랜덤 문자열/숫자 길이
|
||||
prefix?: string; // 접두사
|
||||
suffix?: string; // 접미사
|
||||
format?: string; // 시간 형식 (current_time용)
|
||||
startValue?: number; // 시퀀스 시작값
|
||||
};
|
||||
}
|
||||
|
||||
export interface BaseComponent {
|
||||
id: string;
|
||||
type: ComponentType;
|
||||
|
|
@ -174,7 +200,11 @@ export interface BaseComponent {
|
|||
| "current_user"
|
||||
| "uuid"
|
||||
| "sequence"
|
||||
| "user_defined"; // 자동 값 타입
|
||||
| "user_defined"; // 자동 값 타입 (레거시)
|
||||
|
||||
// 새로운 기능들
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
}
|
||||
|
||||
// 컨테이너 컴포넌트
|
||||
|
|
|
|||
Loading…
Reference in New Issue