[agent-pipeline] pipe-20260329112709-ncml round-1

This commit is contained in:
DDD1542
2026-03-29 22:56:00 +09:00
parent b3f2383ef0
commit a5f4cd5ba9
397 changed files with 4011 additions and 4161 deletions
@@ -80,8 +80,8 @@ export default function DataFlowEditPage() {
<div className="rounded-lg border border-border bg-white">
<DataFlowDesigner
key={diagramId}
selectedDiagram={diagramName}
diagramId={diagramId}
selected_diagram={diagramName}
diagram_id={diagramId}
onBackToList={handleBackToList}
onDiagramNameUpdate={handleDiagramNameUpdate}
/>
+3 -3
View File
@@ -10,14 +10,14 @@ export async function GET(request: NextRequest) {
const codeLayouts = LayoutRegistry.getAllLayouts().map((layout) => ({
id: layout.id,
name: layout.name,
nameEng: layout.nameEng,
nameEng: layout.name_eng,
description: layout.description,
category: layout.category,
type: "code", // 코드로 생성된 레이아웃
isActive: layout.isActive !== false,
isActive: layout.is_active !== false,
tags: layout.tags || [],
metadata: layout.metadata,
zones: layout.defaultZones?.length || 0,
zones: layout.default_zones?.length || 0,
}));
// 레지스트리 통계
+13 -13
View File
@@ -92,11 +92,11 @@ export default function SimpleTypeSafetyTest() {
const testWidget: WidgetComponent = {
id: "test-widget",
type: "widget",
widgetType: "text",
widget_type: "text",
position: { x: 0, y: 0 },
size: { width: 200, height: 40 },
label: "테스트",
webTypeConfig: {},
web_type_config: {},
};
const testContainer = {
@@ -128,38 +128,38 @@ export default function SimpleTypeSafetyTest() {
{
id: "userName",
type: "widget",
widgetType: "text",
widget_type: "text",
position: { x: 0, y: 0 },
size: { width: 200, height: 40 },
label: "사용자명",
columnName: "user_name",
webTypeConfig: {},
column_name: "user_name",
web_type_config: {},
},
{
id: "isActive",
type: "widget",
widgetType: "checkbox",
widget_type: "checkbox",
position: { x: 0, y: 50 },
size: { width: 200, height: 40 },
label: "활성화",
columnName: "is_active",
webTypeConfig: {},
column_name: "is_active",
web_type_config: {},
},
];
const processedData: Record<string, any> = {};
formComponents.forEach((component) => {
const fieldValue = formData[component.id as keyof typeof formData];
if (fieldValue !== undefined && component.columnName) {
switch (component.widgetType) {
if (fieldValue !== undefined && component.column_name) {
switch (component.widget_type) {
case "text":
processedData[component.columnName] = String(fieldValue);
processedData[component.column_name] = String(fieldValue);
break;
case "checkbox":
processedData[component.columnName] = booleanToYN(Boolean(fieldValue));
processedData[component.column_name] = booleanToYN(Boolean(fieldValue));
break;
default:
processedData[component.columnName] = fieldValue;
processedData[component.column_name] = fieldValue;
}
}
});
+14 -13
View File
@@ -98,12 +98,12 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
// 탭별 필터링
if (tab === "images") {
filtered = files.filter(file => {
const ext = file.realFileName?.split('.').pop()?.toLowerCase() || '';
const ext = file.real_file_name?.split('.').pop()?.toLowerCase() || '';
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
});
} else if (tab === "documents") {
filtered = files.filter(file => {
const ext = file.realFileName?.split('.').pop()?.toLowerCase() || '';
const ext = file.real_file_name?.split('.').pop()?.toLowerCase() || '';
return ['txt', 'md', 'doc', 'docx', 'pdf', 'rtf', 'hwp', 'hwpx'].includes(ext);
});
} else if (tab === "recent") {
@@ -116,8 +116,8 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
if (query.trim()) {
const lowerQuery = query.toLowerCase();
filtered = filtered.filter(file =>
file.realFileName?.toLowerCase().includes(lowerQuery) ||
file.savedFileName?.toLowerCase().includes(lowerQuery) ||
file.real_file_name?.toLowerCase().includes(lowerQuery) ||
file.saved_file_name?.toLowerCase().includes(lowerQuery) ||
file.uploadPage?.toLowerCase().includes(lowerQuery)
);
}
@@ -129,10 +129,11 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
const handleDownload = async (file: GlobalFileInfo) => {
try {
await downloadFile({
fileId: file.objid,
originalName: file.realFileName || file.savedFileName || "download",
fileId: file.id,
server_filename: file.server_filename || "",
original_name: file.real_file_name || file.saved_file_name || "download",
});
toast.success(`파일 다운로드 시작: ${file.realFileName}`);
toast.success(`파일 다운로드 시작: ${file.real_file_name}`);
} catch (error) {
console.error("파일 다운로드 오류:", error);
showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." });
@@ -147,9 +148,9 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
// 파일 접근 불가능하게 설정 (삭제 대신)
const handleRemove = (file: GlobalFileInfo) => {
GlobalFileManager.setFileAccessible(file.objid, false);
GlobalFileManager.setFileAccessible(file.id, false);
refreshFiles();
toast.success(`파일이 목록에서 제거되었습니다: ${file.realFileName}`);
toast.success(`파일이 목록에서 제거되었습니다: ${file.real_file_name}`);
};
// 초기 로드 및 검색/탭 변경 시 필터링
@@ -224,16 +225,16 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
</div>
) : (
filteredFiles.map((file) => (
<Card key={file.objid} className="p-3">
<Card key={file.id} className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
{getFileIcon(file.realFileName || file.savedFileName || "", 20)}
{getFileIcon(file.real_file_name || file.saved_file_name || "", 20)}
<div className="flex-1 min-w-0">
<div className="font-medium truncate">
{file.realFileName || file.savedFileName}
{file.real_file_name || file.saved_file_name}
</div>
<div className="text-sm text-muted-foreground flex items-center gap-2">
<span>{formatFileSize(file.fileSize)}</span>
<span>{formatFileSize(file.size)}</span>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(file.uploadTime).toLocaleDateString()}
@@ -176,7 +176,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
})),
});
},
getItemId: (code: CodeInfo) => code.code_value,
getItemId: (code: CodeInfo) => code.code_value || "",
});
// 새 코드 생성
@@ -213,7 +213,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
try {
await deleteCodeMutation.mutateAsync({
categoryCode,
codeValue: deletingCode.code_value,
codeValue: deletingCode.code_value || "",
});
setShowDeleteModal(false);
@@ -298,7 +298,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
<>
<DndContext {...dragAndDrop.dndContextProps}>
<SortableContext
items={visibleCodes.map((code) => code.code_value)}
items={visibleCodes.map((code) => code.code_value || "")}
strategy={verticalListSortingStrategy}
>
{visibleCodes.map((code, index) => {
+11 -11
View File
@@ -64,7 +64,7 @@ export function CodeFormModal({
categoryCode,
"codeName",
validationStates.codeName.value,
isEditing ? editingCode?.codeValue || editingCode?.code_value : undefined,
isEditing ? editingCode?.code_value : undefined,
validationStates.codeName.enabled,
);
@@ -96,22 +96,22 @@ export function CodeFormModal({
if (isOpen) {
if (isEditing && editingCode) {
// 수정 모드: 기존 데이터 로드 (codeValue는 표시용으로만 설정)
const parentValue = editingCode.parentCodeValue || editingCode.parent_code_value || "";
const parentValue = editingCode.parent_code_value || "";
form.reset({
codeName: editingCode.codeName || editingCode.code_name,
codeNameEng: editingCode.codeNameEng || editingCode.code_name_eng || "",
codeName: editingCode.code_name,
codeNameEng: editingCode.code_name_eng || "",
description: editingCode.description || "",
sortOrder: editingCode.sortOrder || editingCode.sort_order,
isActive: (editingCode.isActive || editingCode.is_active) as "Y" | "N",
sortOrder: editingCode.sort_order,
isActive: editingCode.is_active as "Y" | "N",
parentCodeValue: parentValue,
});
// codeValue는 별도로 설정 (표시용)
form.setValue("codeValue" as any, editingCode.codeValue || editingCode.code_value);
form.setValue("codeValue" as any, editingCode.code_value);
} else {
// 새 코드 모드: 자동 순서 계산
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sortOrder || c.sort_order || 0)) : 0;
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sort_order || 0)) : 0;
// 기본 부모 코드가 있으면 설정 (하위 코드 추가 시)
const parentValue = defaultParentCode || "";
@@ -137,7 +137,7 @@ export function CodeFormModal({
// 수정
await updateCodeMutation.mutateAsync({
categoryCode,
codeValue: editingCode.codeValue || editingCode.code_value || "",
codeValue: editingCode.code_value || "",
data: data as UpdateCodeData,
});
} else {
@@ -252,9 +252,9 @@ export function CodeFormModal({
<Label className="text-xs sm:text-sm"> </Label>
<div className="bg-muted h-8 rounded-md border px-3 py-1.5 text-xs sm:h-10 sm:py-2 sm:text-sm">
{(() => {
const parentCode = codes.find((c) => (c.codeValue || c.code_value) === defaultParentCode);
const parentCode = codes.find((c) => c.code_value === defaultParentCode);
return parentCode
? `${parentCode.codeName || parentCode.code_name} (${defaultParentCode})`
? `${parentCode.code_name} (${defaultParentCode})`
: defaultParentCode;
})()}
</div>
@@ -5,8 +5,8 @@ import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -106,12 +106,12 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
}
// 입력타입 검증
if (!column.inputType) {
if (!column.input_type) {
errors.push("입력타입을 선택해주세요");
}
// 길이 검증 (길이를 지원하는 타입인 경우)
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.input_type) as any;
if (inputTypeOption?.supportsLength && column.length !== undefined) {
if (column.length < VALIDATION_RULES.columnLength.min || column.length > VALIDATION_RULES.columnLength.max) {
errors.push(VALIDATION_RULES.columnLength.errorMessage);
@@ -131,8 +131,8 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
* 입력타입 변경 시 길이 기본값 설정
*/
const handleInputTypeChange = (index: number, inputType: string) => {
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType);
const updates: Partial<CreateColumnDefinition> = { inputType: inputType as any };
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === inputType) as any;
const updates: Partial<CreateColumnDefinition> = { input_type: inputType as any };
// 길이를 지원하는 타입이고 현재 길이가 없으면 기본값 설정
if (inputTypeOption?.supportsLength && !columns[index].length && inputTypeOption.defaultLength) {
@@ -183,7 +183,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
</TableHeader>
<TableBody>
{columns.map((column, index) => {
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.inputType);
const inputTypeOption = INPUT_TYPE_OPTIONS.find((opt) => opt.value === column.input_type) as any;
const rowErrors = validationErrors[index] || [];
const hasRowError = rowErrors.length > 0;
@@ -220,7 +220,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
<TableCell className="h-16 text-sm">
<Select
value={column.inputType}
value={column.input_type}
onValueChange={(value) => handleInputTypeChange(index, value)}
disabled={disabled}
>
@@ -271,8 +271,8 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
<TableCell className="h-16 text-sm">
<Input
value={column.defaultValue || ""}
onChange={(e) => updateColumn(index, { defaultValue: e.target.value })}
value={column.default_value || ""}
onChange={(e) => updateColumn(index, { default_value: e.target.value })}
placeholder="기본값"
disabled={disabled}
className="text-sm"
@@ -37,9 +37,9 @@ export function CompanyDeleteDialog({
const [isDeleting, setIsDeleting] = useState(false);
// 다이얼로그가 열려있지 않으면 렌더링하지 않음
if (!deleteState.isOpen || !deleteState.targetCompany) return null;
if (!deleteState.isOpen || !deleteState.target_company) return null;
const { targetCompany } = deleteState;
const targetCompany = deleteState.target_company;
// 삭제 확인 처리
const handleConfirm = async () => {
+7 -15
View File
@@ -42,7 +42,7 @@ export function CompanyFormModal({
// 모달이 열려있지 않으면 렌더링하지 않음
if (!modalState.isOpen) return null;
const { mode, formData, selectedCompany } = modalState;
const { mode, form_data: formData, selected_company: selectedCompany } = modalState;
const isEditMode = mode === "edit";
// 사업자등록번호 변경 처리
@@ -112,17 +112,9 @@ export function CompanyFormModal({
return (
<Dialog open={modalState.isOpen} onOpenChange={handleCancel}>
<DialogContent
className="sm:max-w-[425px]"
<DialogContent
className="sm:max-w-[425px]"
onKeyDown={handleKeyDown}
defaultWidth={500}
defaultHeight={600}
minWidth={400}
minHeight={500}
maxWidth={700}
maxHeight={800}
modalId="company-form"
userId={modalState.selectedCompany?.company_code}
>
<DialogHeader>
<DialogTitle>{isEditMode ? "회사 정보 수정" : "새 회사 등록"}</DialogTitle>
@@ -237,18 +229,18 @@ export function CompanyFormModal({
)}
{/* 수정 모드일 때 추가 정보 표시 */}
{isEditMode && modalState.selectedCompany && (
{isEditMode && modalState.selected_company && (
<div className="bg-muted/50 rounded-md p-3">
<div className="space-y-1 text-sm">
<p>
<span className="font-medium"> :</span> {modalState.selectedCompany.company_code}
<span className="font-medium"> :</span> {modalState.selected_company.company_code}
</p>
<p>
<span className="font-medium">:</span> {modalState.selectedCompany.writer}
<span className="font-medium">:</span> {modalState.selected_company.writer}
</p>
<p>
<span className="font-medium">:</span>{" "}
{new Date(modalState.selectedCompany.regdate).toLocaleDateString("ko-KR")}
{new Date(modalState.selected_company.regdate).toLocaleDateString("ko-KR")}
</p>
</div>
</div>
+2 -2
View File
@@ -24,7 +24,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
// 디스크 사용량 포맷팅 함수
const formatDiskUsage = (company: Company) => {
if (!company.diskUsage) {
if (!company.disk_usage) {
return (
<div className="text-muted-foreground flex items-center gap-1">
<HardDrive className="h-3 w-3" />
@@ -33,7 +33,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
);
}
const { fileCount, totalSizeMB } = company.diskUsage;
const { file_count: fileCount, total_size_mb: totalSizeMB } = company.disk_usage;
return (
<div className="flex flex-col gap-1">
+17 -17
View File
@@ -33,12 +33,12 @@ import {
RESERVED_WORDS,
} from "../../types/ddl";
export function CreateTableModal({
isOpen,
onClose,
onSuccess,
export function CreateTableModal({
isOpen,
onClose,
onSuccess,
mode = "create",
sourceTableName
source_table_name: sourceTableName
}: CreateTableModalProps) {
const isDuplicateMode = mode === "duplicate" && sourceTableName;
@@ -48,7 +48,7 @@ export function CreateTableModal({
{
name: "",
label: "",
inputType: "text",
input_type: "text",
nullable: true,
order: 1,
},
@@ -69,7 +69,7 @@ export function CreateTableModal({
{
name: "",
label: "",
inputType: "text",
input_type: "text",
nullable: true,
order: 1,
},
@@ -123,10 +123,10 @@ export function CreateTableModal({
// 2. 컬럼 정보 변환
const loadedColumns: CreateColumnDefinition[] = columnsList.map((col, idx) => ({
name: col.columnName,
label: col.displayName || col.columnName,
inputType: col.webType || col.inputType || "text",
nullable: col.isNullable === "YES",
name: col.column_name,
label: col.display_name || col.column_name,
input_type: col.web_type || col.input_type || "text",
nullable: col.is_nullable === "YES",
order: idx + 1,
description: col.description,
}));
@@ -215,7 +215,7 @@ export function CreateTableModal({
{
name: "",
label: "",
inputType: "text",
input_type: "text",
nullable: true,
order: columns.length + 1,
},
@@ -231,7 +231,7 @@ export function CreateTableModal({
return;
}
const validColumns = columns.filter((col) => col.name && col.inputType);
const validColumns = columns.filter((col) => col.name && col.input_type);
if (validColumns.length === 0) {
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
return;
@@ -240,7 +240,7 @@ export function CreateTableModal({
setValidating(true);
try {
const result = await ddlApi.validateTableCreation({
tableName,
table_name: tableName,
columns: validColumns,
description,
});
@@ -269,7 +269,7 @@ export function CreateTableModal({
return;
}
const validColumns = columns.filter((col) => col.name && col.inputType);
const validColumns = columns.filter((col) => col.name && col.input_type);
if (validColumns.length === 0) {
toast.error("최소 1개의 유효한 컬럼이 필요합니다.");
return;
@@ -278,7 +278,7 @@ export function CreateTableModal({
setLoading(true);
try {
const result = await ddlApi.createTable({
tableName,
table_name: tableName,
columns: validColumns,
description,
});
@@ -318,7 +318,7 @@ export function CreateTableModal({
/**
* 폼 유효성 확인
*/
const isFormValid = !tableNameError && tableName && columns.some((col) => col.name && col.inputType);
const isFormValid = !tableNameError && tableName && columns.some((col) => col.name && col.input_type);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
+9 -9
View File
@@ -144,8 +144,8 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
* 성공률 계산
*/
const getSuccessRate = (stats: DDLStatistics) => {
if (stats.totalExecutions === 0) return 0;
return Math.round((stats.successfulExecutions / stats.totalExecutions) * 100);
if (stats.total_executions === 0) return 0;
return Math.round((stats.successful_executions / stats.total_executions) * 100);
};
return (
@@ -314,7 +314,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
<CardTitle className="text-sm font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statistics.totalExecutions}</div>
<div className="text-2xl font-bold">{statistics.total_executions}</div>
</CardContent>
</Card>
@@ -323,7 +323,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-emerald-600">{statistics.successfulExecutions}</div>
<div className="text-2xl font-bold text-emerald-600">{statistics.successful_executions}</div>
</CardContent>
</Card>
@@ -332,7 +332,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-destructive">{statistics.failedExecutions}</div>
<div className="text-2xl font-bold text-destructive">{statistics.failed_executions}</div>
</CardContent>
</Card>
@@ -353,7 +353,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
<CardTitle className="text-base">DDL </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{Object.entries(statistics.byDDLType).map(([type, count]) => (
{Object.entries(statistics.by_ddl_type).map(([type, count]) => (
<div key={type} className="flex items-center justify-between">
<Badge variant={getDDLTypeBadgeVariant(type)}>{type}</Badge>
<span className="font-medium">{count}</span>
@@ -367,7 +367,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{Object.entries(statistics.byUser).map(([user, count]) => (
{Object.entries(statistics.by_user).map(([user, count]) => (
<div key={user} className="flex items-center justify-between">
<Badge variant="outline">{user}</Badge>
<span className="font-medium">{count}</span>
@@ -378,7 +378,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
</div>
{/* 최근 실패 로그 */}
{statistics.recentFailures.length > 0 && (
{statistics.recent_failures.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base text-destructive"> </CardTitle>
@@ -386,7 +386,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
</CardHeader>
<CardContent>
<div className="space-y-2">
{statistics.recentFailures.map((failure, index) => (
{statistics.recent_failures.map((failure, index) => (
<div key={index} className="rounded-lg border border-destructive/20 bg-destructive/10 p-3">
<div className="mb-1 flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -42,8 +42,8 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
);
}
const { summary, lastChecked } = diskUsageInfo;
const lastCheckedDate = new Date(lastChecked);
const { summary, last_checked } = diskUsageInfo;
const lastCheckedDate = new Date(last_checked);
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
@@ -70,7 +70,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
<Building2 className="h-4 w-4 text-primary" />
<div>
<p className="text-xs text-muted-foreground"> </p>
<p className="text-lg font-semibold">{summary.totalCompanies}</p>
<p className="text-lg font-semibold">{summary.total_companies}</p>
</div>
</div>
@@ -79,7 +79,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
<FileText className="h-4 w-4 text-primary" />
<div>
<p className="text-xs text-muted-foreground"> </p>
<p className="text-lg font-semibold">{summary.totalFiles.toLocaleString()}</p>
<p className="text-lg font-semibold">{summary.total_files.toLocaleString()}</p>
</div>
</div>
@@ -88,7 +88,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
<HardDrive className="h-4 w-4 text-primary" />
<div>
<p className="text-xs text-muted-foreground"> </p>
<p className="text-lg font-semibold">{summary.totalSizeMB.toFixed(1)} MB</p>
<p className="text-lg font-semibold">{summary.total_size_mb.toFixed(1)} MB</p>
</div>
</div>
@@ -114,9 +114,9 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground"> </span>
<Badge
variant={summary.totalSizeMB > 1000 ? "destructive" : summary.totalSizeMB > 500 ? "secondary" : "default"}
variant={summary.total_size_mb > 1000 ? "destructive" : summary.total_size_mb > 500 ? "secondary" : "default"}
>
{summary.totalSizeMB > 1000 ? "용량 주의" : summary.totalSizeMB > 500 ? "보통" : "여유"}
{summary.total_size_mb > 1000 ? "용량 주의" : summary.total_size_mb > 500 ? "보통" : "여유"}
</Badge>
</div>
@@ -124,10 +124,10 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
<div className="mt-2 h-2 w-full rounded-full bg-muted">
<div
className={`h-2 rounded-full transition-all duration-300 ${
summary.totalSizeMB > 1000 ? "bg-destructive" : summary.totalSizeMB > 500 ? "bg-primary/60" : "bg-primary"
summary.total_size_mb > 1000 ? "bg-destructive" : summary.total_size_mb > 500 ? "bg-primary/60" : "bg-primary"
}`}
style={{
width: `${Math.min((summary.totalSizeMB / 2000) * 100, 100)}%`,
width: `${Math.min((summary.total_size_mb / 2000) * 100, 100)}%`,
}}
/>
</div>
+3 -3
View File
@@ -42,9 +42,9 @@ export default function LangKeyModal({ isOpen, onClose, onSave, keyData, compani
} else {
// 새 키 추가 모드 - 기본값 설정
setFormData({
companyCode: "",
menuName: "",
langKey: "",
company_code: "",
menu_name: "",
lang_key: "",
description: "",
});
}
@@ -9,10 +9,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
+39 -39
View File
@@ -146,7 +146,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
// });
// ScreenDefinition에서는 screenId 필드를 사용
const actualScreenId = screen['screenId'] || screen.id;
const actualScreenId = screen.screen_id;
if (!actualScreenId) {
console.error("❌ 화면 ID를 찾을 수 없습니다:", screen);
@@ -170,14 +170,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
setFormData((prev) => ({
...prev,
menuUrl: screenUrl,
screenCode: screen.screenCode, // 화면 코드도 함께 저장
screenCode: screen.screen_code, // 화면 코드도 함께 저장
}));
// console.log("🖥️ 화면 선택 완료:", {
// screenId: screen['screenId'],
// legacyId: screen.id,
// actualScreenId,
// screenName: screen.screenName,
// screenName: screen.screen_name,
// menuType: menuType,
// formDataMenuType: formData.menu_type,
// isAdminMenu,
@@ -205,7 +205,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
// POP 화면 선택 시 URL 자동 설정
const handlePopScreenSelect = (screen: ScreenDefinition) => {
const actualScreenId = screen['screenId'] || screen.id;
const actualScreenId = screen.screen_id;
if (!actualScreenId) {
toast.error("화면 ID를 찾을 수 없습니다.");
return;
@@ -244,7 +244,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
} else if (type === "pop") {
setSelectedScreen(null);
if (selectedPopScreen) {
const actualScreenId = selectedPopScreen['screenId'] || selectedPopScreen.id;
const actualScreenId = selectedPopScreen.screen_id;
setFormData((prev) => ({
...prev,
menuUrl: `/pop/screens/${actualScreenId}`,
@@ -258,7 +258,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
} else if (type === "screen") {
setSelectedPopScreen(null);
if (selectedScreen) {
const actualScreenId = selectedScreen['screenId'] || selectedScreen.id;
const actualScreenId = selectedScreen.screen_id;
let screenUrl = `/screens/${actualScreenId}`;
const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menu_type === "0";
if (isAdminMenu) {
@@ -267,7 +267,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
setFormData((prev) => ({
...prev,
menuUrl: screenUrl,
screenCode: selectedScreen.screenCode,
screenCode: selectedScreen.screen_code,
}));
} else {
setFormData((prev) => ({
@@ -372,7 +372,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
// 화면 설정 함수
const setScreenFromId = () => {
const screen = screens.find((s) => s['screenId'].toString() === screenId || s.id?.toString() === screenId);
const screen = screens.find((s) => s.screen_id.toString() === screenId);
if (screen) {
setSelectedScreen(screen);
// console.log("🖥️ 기존 메뉴의 할당된 화면 설정:", {
@@ -414,7 +414,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
const popScreenId = menuUrl.match(/\/pop\/screens\/(\d+)/)?.[1];
if (popScreenId) {
const setPopScreenFromId = () => {
const screen = screens.find((s) => s['screenId'].toString() === popScreenId || s.id?.toString() === popScreenId);
const screen = screens.find((s) => s.screen_id.toString() === popScreenId);
if (screen) {
setSelectedPopScreen(screen);
}
@@ -576,12 +576,12 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1];
if (screenId && !selectedScreen) {
console.log("🔄 화면 목록 로드 완료 - 기존 할당 화면 자동 설정");
const screen = screens.find((s) => s['screenId'].toString() === screenId || s.id?.toString() === screenId);
const screen = screens.find((s) => s.screen_id.toString() === screenId);
if (screen) {
setSelectedScreen(screen);
// console.log("✅ 기존 메뉴의 할당된 화면 자동 설정 완료:", {
// screenId,
// screenName: screen.screenName,
// screenName: screen.screen_name,
// menuUrl,
// });
}
@@ -614,7 +614,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
if (menuUrl.startsWith("/pop/screens/")) {
const popScreenId = menuUrl.match(/\/pop\/screens\/(\d+)/)?.[1];
if (popScreenId && !selectedPopScreen) {
const screen = screens.find((s) => s['screenId'].toString() === popScreenId || s.id?.toString() === popScreenId);
const screen = screens.find((s) => s.screen_id.toString() === popScreenId);
if (screen) {
setSelectedPopScreen(screen);
}
@@ -738,7 +738,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
if (menuObjid > 0) {
console.log("📋 화면-메뉴 관계 테이블 업데이트 시작:", {
screenId: selectedScreen['screenId'],
screenId: selectedScreen.screen_id,
menuObjid,
});
@@ -750,10 +750,10 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
// 2. 기존 화면들 모두 제거
for (const existingScreen of existingScreens) {
try {
await menuScreenApi.unassignScreenFromMenu(existingScreen['screenId'], menuObjid);
console.log(`✅ 기존 화면 제거 완료: ${existingScreen.screenName}`);
await menuScreenApi.unassignScreenFromMenu(existingScreen.screen_id, menuObjid);
console.log(`✅ 기존 화면 제거 완료: ${existingScreen.screen_name}`);
} catch (unassignError) {
console.warn(`⚠️ 기존 화면 제거 실패: ${existingScreen.screenName}`, unassignError);
console.warn(`⚠️ 기존 화면 제거 실패: ${existingScreen.screen_name}`, unassignError);
}
}
} catch (getError) {
@@ -761,7 +761,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
}
// 3. 새 화면 할당
await menuScreenApi.assignScreenToMenu(selectedScreen['screenId'], menuObjid);
await menuScreenApi.assignScreenToMenu(selectedScreen.screen_id, menuObjid);
console.log("✅ 새 화면 할당 완료");
}
} catch (assignError) {
@@ -1009,7 +1009,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
className="w-full justify-between"
>
<span className="text-left">
{selectedScreen ? selectedScreen.screenName : "화면을 선택하세요"}
{selectedScreen ? selectedScreen.screen_name : "화면을 선택하세요"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
@@ -1034,28 +1034,28 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
{screens
.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
screen.screen_name.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screen_code.toLowerCase().includes(screenSearchText.toLowerCase()),
)
.map((screen, index) => (
<div
key={`screen-${screen['screenId'] || screen.id || index}-${screen.screenCode || index}`}
key={`screen-${screen.screen_id || index}-${screen.screen_code || index}`}
onClick={() => handleScreenSelect(screen)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-muted"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{screen.screenName}</div>
<div className="text-xs text-muted-foreground">{screen.screenCode}</div>
<div className="text-sm font-medium">{screen.screen_name}</div>
<div className="text-xs text-muted-foreground">{screen.screen_code}</div>
</div>
<div className="text-xs text-muted-foreground/70">ID: {screen['screenId'] || screen.id || "N/A"}</div>
<div className="text-xs text-muted-foreground/70">ID: {screen.screen_id || "N/A"}</div>
</div>
</div>
))}
{screens.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
screen.screen_name.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screen_code.toLowerCase().includes(screenSearchText.toLowerCase()),
).length === 0 && <div className="px-3 py-2 text-sm text-muted-foreground"> .</div>}
</div>
</div>
@@ -1065,8 +1065,8 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
{/* 선택된 화면 정보 표시 */}
{selectedScreen && (
<div className="bg-accent rounded-md border p-3">
<div className="text-sm font-medium text-primary">{selectedScreen.screenName}</div>
<div className="text-primary text-xs">: {selectedScreen.screenCode}</div>
<div className="text-sm font-medium text-primary">{selectedScreen.screen_name}</div>
<div className="text-primary text-xs">: {selectedScreen.screen_code}</div>
<div className="text-primary text-xs"> URL: {formData.menuUrl}</div>
</div>
)}
@@ -1165,7 +1165,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
className="w-full justify-between"
>
<span className="text-left">
{selectedPopScreen ? selectedPopScreen.screenName : "POP 화면을 선택하세요"}
{selectedPopScreen ? selectedPopScreen.screen_name : "POP 화면을 선택하세요"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
@@ -1188,28 +1188,28 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
{screens
.filter(
(screen) =>
screen.screenName.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(popScreenSearchText.toLowerCase()),
screen.screen_name.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screen_code.toLowerCase().includes(popScreenSearchText.toLowerCase()),
)
.map((screen, index) => (
<div
key={`pop-screen-${screen['screenId'] || screen.id || index}-${screen.screenCode || index}`}
key={`pop-screen-${screen.screen_id || index}-${screen.screen_code || index}`}
onClick={() => handlePopScreenSelect(screen)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{screen.screenName}</div>
<div className="text-xs text-gray-500">{screen.screenCode}</div>
<div className="text-sm font-medium">{screen.screen_name}</div>
<div className="text-xs text-gray-500">{screen.screen_code}</div>
</div>
<div className="text-xs text-gray-400">ID: {screen['screenId'] || screen.id || "N/A"}</div>
<div className="text-xs text-gray-400">ID: {screen.screen_id || "N/A"}</div>
</div>
</div>
))}
{screens.filter(
(screen) =>
screen.screenName.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(popScreenSearchText.toLowerCase()),
screen.screen_name.toLowerCase().includes(popScreenSearchText.toLowerCase()) ||
screen.screen_code.toLowerCase().includes(popScreenSearchText.toLowerCase()),
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500"> .</div>}
</div>
</div>
@@ -1218,8 +1218,8 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
{selectedPopScreen && (
<div className="bg-accent rounded-md border p-3">
<div className="text-sm font-medium text-blue-900">{selectedPopScreen.screenName}</div>
<div className="text-primary text-xs">: {selectedPopScreen.screenCode}</div>
<div className="text-sm font-medium text-blue-900">{selectedPopScreen.screen_name}</div>
<div className="text-primary text-xs">: {selectedPopScreen.screen_code}</div>
<div className="text-primary text-xs"> URL: {formData.menuUrl}</div>
</div>
)}
@@ -54,7 +54,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
const [menuTypeFilter, setMenuTypeFilter] = useState<string>("all");
// 최고 관리자 여부 확인
const isSuperAdmin = currentUser?.['companyCode'] === "*" && currentUser?.['userType'] === "SUPER_ADMIN";
const isSuperAdmin = currentUser?.company_code === "*" && currentUser?.user_type === "SUPER_ADMIN";
// 회사 정보 가져오기
useEffect(() => {
@@ -97,9 +97,9 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
console.log("🔍 [MenuPermissionsTable] 전체 메뉴 로드 시작", {
currentUser: {
userId: currentUser['userId'],
companyCode: currentUser['companyCode'],
userType: currentUser['userType'],
userId: currentUser.user_id,
companyCode: currentUser.company_code,
userType: currentUser.user_type,
},
isSuperAdmin,
roleGroupCompanyCode: roleGroup.company_code,
+12 -12
View File
@@ -38,7 +38,7 @@ export function SortableCodeItem({
onToggleExpand,
}: SortableCodeItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: code.codeValue || code.code_value || "",
id: code.code_value || "",
disabled: isDragOverlay,
});
const updateCodeMutation = useUpdateCode();
@@ -51,7 +51,7 @@ export function SortableCodeItem({
// 활성/비활성 토글 핸들러
const handleToggleActive = async (checked: boolean) => {
try {
const codeValue = code.codeValue || code.code_value;
const codeValue = code.code_value;
if (!codeValue) {
return;
}
@@ -60,10 +60,10 @@ export function SortableCodeItem({
categoryCode,
codeValue: codeValue,
data: {
codeName: code.codeName || code.code_name,
codeNameEng: code.codeNameEng || code.code_name_eng || "",
codeName: code.code_name,
codeNameEng: code.code_name_eng || "",
description: code.description || "",
sortOrder: code.sortOrder || code.sort_order,
sortOrder: code.sort_order,
isActive: checked ? "Y" : "N",
},
});
@@ -75,7 +75,7 @@ export function SortableCodeItem({
// 계층구조 깊이에 따른 들여쓰기
const depth = code.depth || 1;
const indentLevel = (depth - 1) * 28; // 28px per level
const hasParent = !!(code.parentCodeValue || code.parent_code_value);
const hasParent = !!code.parent_code_value;
return (
<div className="flex items-stretch">
@@ -122,7 +122,7 @@ export function SortableCodeItem({
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
)}
<h4 className="text-sm font-semibold">{code.codeName || code.code_name}</h4>
<h4 className="text-sm font-semibold">{code.code_name}</h4>
{/* 접힌 상태에서 자식 개수 표시 */}
{hasChildren && !isExpanded && <span className="text-muted-foreground text-[10px]">({childCount})</span>}
{/* 깊이 표시 배지 */}
@@ -150,7 +150,7 @@ export function SortableCodeItem({
</Badge>
)}
<Badge
variant={code.isActive === "Y" || code.is_active === "Y" ? "default" : "secondary"}
variant={code.is_active === "Y" ? "default" : "secondary"}
className={cn(
"cursor-pointer text-xs transition-colors",
updateCodeMutation.isPending && "cursor-not-allowed opacity-50",
@@ -159,21 +159,21 @@ export function SortableCodeItem({
e.preventDefault();
e.stopPropagation();
if (!updateCodeMutation.isPending) {
const isActive = code.isActive === "Y" || code.is_active === "Y";
const isActive = code.is_active === "Y";
handleToggleActive(!isActive);
}
}}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
{code.isActive === "Y" || code.is_active === "Y" ? "활성" : "비활성"}
{code.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</div>
<p className="text-muted-foreground mt-1 text-xs">{code.codeValue || code.code_value}</p>
<p className="text-muted-foreground mt-1 text-xs">{code.code_value}</p>
{/* 부모 코드 표시 */}
{hasParent && (
<p className="text-muted-foreground mt-0.5 text-[10px]">
: {code.parentCodeValue || code.parent_code_value}
: {code.parent_code_value}
</p>
)}
{code.description && <p className="text-muted-foreground mt-1 text-xs">{code.description}</p>}
@@ -78,7 +78,7 @@ export function UserHistoryModal({ isOpen, onClose, userId, userName }: UserHist
setHistoryList(mappedHistoryList);
setTotalItems(responseTotal);
setMaxPageSize(response.maxPageSize || 1);
setMaxPageSize(response.max_page_size || 1);
} else if (response && response.success && (!response.data || response.data.length === 0)) {
// 데이터가 비어있는 경우
console.log("📋 변경이력이 없습니다.");
+2 -2
View File
@@ -28,7 +28,7 @@ export function UserToolbar({
// 통합 검색어 변경
const handleV2SearchChange = (value: string) => {
onSearchChange({
searchValue: value,
search_value: value,
// 통합 검색 시 고급 검색 필드들 클리어
searchType: undefined,
search_sabun: undefined,
@@ -78,7 +78,7 @@ export function UserToolbar({
/>
<Input
placeholder="통합 검색..."
value={searchFilter.searchValue || ""}
value={searchFilter.search_value || ""}
onChange={(e) => handleV2SearchChange(e.target.value)}
disabled={isAdvancedSearchMode}
className={`h-10 pl-10 text-sm ${
@@ -83,9 +83,9 @@ export function DashboardSaveModal({
try {
const [adminData, userData] = await Promise.all([menuApi.getAdminMenus(), menuApi.getUserMenus()]);
// API 응답이 배열인지 확인하고 처리
const adminMenuList = Array.isArray(adminData) ? adminData : adminData?.data || [];
const userMenuList = Array.isArray(userData) ? userData : userData?.data || [];
// API 응답에서 data 배열 추출
const adminMenuList = (adminData?.data || []) as any[];
const userMenuList = (userData?.data || []) as any[];
setAdminMenus(adminMenuList);
setUserMenus(userMenuList);
@@ -29,6 +29,7 @@ export function MultiChartConfigPanel({
xAxis: string;
yAxis: string[];
label?: string;
chartType?: "bar" | "line" | "area";
}>
>(config.dataSourceConfigs || []);
@@ -147,7 +147,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
}
// 외부 커넥션 ID 저장 (백엔드에서 인증 정보 조회용)
updates.external_connection_id = connection.id;
updates.externalConnectionId = connection.id;
console.log("최종 업데이트:", updates);
@@ -190,10 +190,10 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
// 쿼리 파라미터를 배열로 정규화 (객체 형식 호환)
const normalizeQueryParams = (): KeyValuePair[] => {
if (!dataSource.query_params) return [];
if (Array.isArray(dataSource.query_params)) return dataSource.query_params;
if (!dataSource.queryParams) return [];
if (Array.isArray(dataSource.queryParams)) return dataSource.queryParams;
// 객체 형식이면 배열로 변환
return Object.entries(dataSource.query_params as Record<string, string>).map(([key, value]) => ({
return Object.entries(dataSource.queryParams as Record<string, string>).map(([key, value]) => ({
id: `param_${Date.now()}_${Math.random()}`,
key,
value,
@@ -204,21 +204,21 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
const addQueryParam = () => {
const queryParams = normalizeQueryParams();
onChange({
query_params: [...queryParams, { id: `param_${Date.now()}`, key: "", value: "" }],
queryParams: [...queryParams, { id: `param_${Date.now()}`, key: "", value: "" }],
});
};
// 쿼리 파라미터 제거
const removeQueryParam = (id: string) => {
const queryParams = normalizeQueryParams();
onChange({ query_params: queryParams.filter((p) => p.id !== id) });
onChange({ queryParams: queryParams.filter((p) => p.id !== id) });
};
// 쿼리 파라미터 업데이트
const updateQueryParam = (id: string, updates: Partial<KeyValuePair>) => {
const queryParams = normalizeQueryParams();
onChange({
query_params: queryParams.map((p) => (p.id === id ? { ...p, ...updates } : p)),
queryParams: queryParams.map((p) => (p.id === id ? { ...p, ...updates } : p)),
});
};
@@ -288,7 +288,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
headers: headers,
query_params: params,
body: requestBody,
external_connection_id: dataSource.external_connection_id, // DB 토큰 등 인증 정보 조회용
external_connection_id: dataSource.externalConnectionId, // DB 토큰 등 인증 정보 조회용
}),
});
@@ -335,26 +335,26 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
// JSON Path 처리
let data = apiData;
if (dataSource.json_path) {
const paths = dataSource.json_path.split(".");
if (dataSource.jsonPath) {
const paths = dataSource.jsonPath.split(".");
for (const path of paths) {
// 배열인 경우 인덱스 접근, 객체인 경우 키 접근
if (data === null || data === undefined) {
throw new Error(`JSON Path "${dataSource.json_path}"에서 데이터를 찾을 수 없습니다 (null/undefined)`);
throw new Error(`JSON Path "${dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다 (null/undefined)`);
}
if (Array.isArray(data)) {
// 배열인 경우 숫자 인덱스로 접근 시도
const index = parseInt(path);
if (!isNaN(index) && index >= 0 && index < data.length) {
data = data[index];
} else {
throw new Error(`JSON Path "${dataSource.json_path}"에서 배열 인덱스 "${path}"를 찾을 수 없습니다`);
throw new Error(`JSON Path "${dataSource.jsonPath}"에서 배열 인덱스 "${path}"를 찾을 수 없습니다`);
}
} else if (typeof data === "object" && path in data) {
data = (data as Record<string, any>)[path];
} else {
throw new Error(`JSON Path "${dataSource.json_path}"에서 "${path}" 키를 찾을 수 없습니다`);
throw new Error(`JSON Path "${dataSource.jsonPath}"에서 "${path}" 키를 찾을 수 없습니다`);
}
}
}
@@ -614,8 +614,8 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
<Label className="text-xs font-medium text-foreground">JSON Path ()</Label>
<Input
placeholder="data.results"
value={dataSource.json_path || ""}
onChange={(e) => onChange({ json_path: e.target.value })}
value={dataSource.jsonPath || ""}
onChange={(e) => onChange({ jsonPath: e.target.value })}
/>
<p className="text-[11px] text-muted-foreground">
JSON (: data.results, items, response.data)
@@ -628,8 +628,8 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
<div className="space-y-1.5">
<Label className="text-xs font-medium text-foreground"> </Label>
<Select
value={(dataSource.refresh_interval || 0).toString()}
onValueChange={(value) => onChange({ refresh_interval: parseInt(value) })}
value={(dataSource.refreshInterval || 0).toString()}
onValueChange={(value) => onChange({ refreshInterval: parseInt(value) })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="간격 선택" />
@@ -158,6 +158,7 @@ export interface ChartDataSource {
jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results")
// 공통
refreshInterval?: number; // 자동 새로고침 간격 (ms, 0이면 비활성)
lastExecuted?: string; // 마지막 실행 시간
lastError?: string; // 마지막 오류 메시지
mapDisplayType?: "auto" | "marker" | "polygon"; // 지도 표시 방식 (auto: 자동, marker: 마커, polygon: 영역)
@@ -186,6 +187,9 @@ export interface ChartDataSource {
// REST API 위치 데이터 저장 설정 (MapTestWidgetV2용)
saveToHistory?: boolean; // REST API에서 가져온 위치 데이터를 vehicle_location_history에 저장
// 자동 새로고침 간격 (API polling, 초 단위, 0이면 수동)
refreshInterval?: number;
}
export interface ChartConfig {
@@ -267,6 +271,9 @@ export interface ChartConfig {
showWeather?: boolean; // 날씨 정보 표시 여부
showWeatherAlerts?: boolean; // 기상특보 영역 표시 여부
// 마커 설정
markerType?: string; // 마커 종류 (circle, arrow, truck)
// 마커 색상 설정
markerColorMode?: "single" | "conditional"; // 마커 색상 모드 (단일/조건부)
markerColorColumn?: string; // 색상 조건 컬럼
@@ -23,7 +23,7 @@ interface CalendarWidgetProps {
*/
export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps) {
// Context에서 선택된 날짜 관리
const { selectedDate, setSelectedDate } = useDashboard();
const { selected_date, setSelectedDate } = useDashboard();
// 현재 표시 중인 년/월
const today = new Date();
@@ -112,7 +112,7 @@ export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps)
days={calendarDays}
config={config}
isCompact={isCompact}
selectedDate={selectedDate}
selectedDate={selected_date}
onDateClick={handleDateClick}
/>
)}
@@ -94,7 +94,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
const result = await dashboardApi.executeQuery(query);
// console.log("🔍 [ListWidget] 쿼리 결과:", result);
if (result.success && result.rows.length > 0) {
if (result.rows.length > 0) {
setAdditionalDetailData(result.rows[0]);
} else {
setAdditionalDetailData({});
@@ -124,7 +124,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.executeQuery(query);
if (result.success && result.rows.length > 0) {
if (result.rows.length > 0) {
setAdditionalDetailData(result.rows[0]);
} else {
setAdditionalDetailData({});
@@ -343,10 +343,10 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
if (element.dataSource.type === "api" && element.dataSource.endpoint) {
// REST API - 백엔드 프록시를 통한 호출
const params = new URLSearchParams();
if (element.dataSource.query_params) {
Object.entries(element.dataSource.query_params).forEach(([key, value]) => {
if (key && value) {
params.append(key, value);
if (element.dataSource.queryParams) {
element.dataSource.queryParams.forEach((item) => {
if (item.key && item.value) {
params.append(item.key, item.value);
}
});
}
@@ -390,7 +390,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
headers: headersObj,
query_params: Object.fromEntries(params),
body: requestBody,
external_connection_id: element.dataSource.external_connection_id,
external_connection_id: element.dataSource.externalConnectionId,
}),
});
@@ -408,13 +408,13 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
// JSON Path 처리
let processedData = apiData;
if (element.dataSource.json_path) {
const paths = element.dataSource.json_path.split(".");
if (element.dataSource.jsonPath) {
const paths = element.dataSource.jsonPath.split(".");
for (const path of paths) {
if (processedData && typeof processedData === "object" && path in processedData) {
processedData = processedData[path];
} else {
throw new Error(`JSON Path "${element.dataSource.json_path}"에서 데이터를 찾을 수 없습니다`);
throw new Error(`JSON Path "${element.dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`);
}
}
}
@@ -430,20 +430,21 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
};
} else if (element.dataSource.query) {
// Database (현재 DB 또는 외부 DB)
if (element.dataSource.connection_type === "external" && element.dataSource.external_connection_id) {
if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) {
// 외부 DB
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
const externalResult = await ExternalDbConnectionAPI.executeQuery(
parseInt(element.dataSource.external_connection_id),
parseInt(element.dataSource.externalConnectionId),
element.dataSource.query,
);
if (!externalResult.success) {
if (!externalResult.success || !externalResult.data) {
throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
}
const extData = externalResult.data as unknown as { columns: string[]; rows: Record<string, any>[]; rowCount: number };
queryResult = {
columns: externalResult.data.columns,
rows: externalResult.data.rows,
totalRows: externalResult.data.rowCount,
columns: extData.columns,
rows: extData.rows,
totalRows: extData.rowCount,
executionTime: 0,
};
} else {
@@ -472,17 +473,17 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
loadData();
// 자동 새로고침 설정
const refreshInterval = element.dataSource?.refresh_interval;
const refreshInterval = element.dataSource?.refreshInterval;
if (refreshInterval && refreshInterval > 0) {
const interval = setInterval(loadData, refreshInterval);
return () => clearInterval(interval);
}
}, [
element.dataSource?.query,
element.dataSource?.connection_type,
element.dataSource?.external_connection_id,
element.dataSource?.connectionType,
element.dataSource?.externalConnectionId,
element.dataSource?.endpoint,
element.dataSource?.refresh_interval,
element.dataSource?.refreshInterval,
]);
// 로딩 중
@@ -527,10 +528,10 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
const displayColumns =
config.columns.length > 0
? config.columns
: data.columns.map((col) => ({
: data.columns.map((col): import("../types").ListColumn => ({
id: col,
name: col,
dataKey: col,
label: col,
field: col,
visible: true,
}));
@@ -562,7 +563,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
className={col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}
style={{ width: col.width ? `${col.width}px` : undefined }}
>
{col.label || col.name}
{col.label}
</TableHead>
))}
</TableRow>
@@ -592,7 +593,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
key={col.id}
className={col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}
>
{String(row[col.dataKey || col.field] ?? "")}
{String(row[col.field] ?? "")}
</TableCell>
))}
</TableRow>
@@ -626,11 +627,11 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
.filter((col) => col.visible)
.map((col) => (
<div key={col.id}>
<div className="text-muted-foreground text-xs font-medium">{col.label || col.name}</div>
<div className="text-muted-foreground text-xs font-medium">{col.label}</div>
<div
className={`text-foreground font-medium ${col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}`}
>
{String(row[col.dataKey || col.field] ?? "")}
{String(row[col.field] ?? "")}
</div>
</div>
))}
@@ -697,15 +698,18 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
{detailPopupData && (
<>
{/* 설정된 필드 그룹이 있으면 사용, 없으면 기본 그룹 생성 */}
{config.rowDetailPopup?.fieldGroups && config.rowDetailPopup.fieldGroups.length > 0
? // 설정된 필드 그룹 렌더링
config.rowDetailPopup.fieldGroups.map((group) =>
renderFieldGroup(group, { ...detailPopupData, ...additionalDetailData }),
)
: // 기본 필드 그룹 렌더링
getDefaultFieldGroups(detailPopupData, additionalDetailData).map((group) =>
renderFieldGroup(group, { ...detailPopupData, ...additionalDetailData }),
)}
{(() => {
const configFieldGroups = config.rowDetailPopup?.additionalQuery?.displayColumns?.flatMap((col) => col.fieldGroups ?? []) ?? [];
return configFieldGroups.length > 0
? // 설정된 필드 그룹 렌더링
configFieldGroups.map((group) =>
renderFieldGroup(group, { ...detailPopupData, ...additionalDetailData }),
)
: // 기본 필드 그룹 렌더링
getDefaultFieldGroups(detailPopupData, additionalDetailData).map((group) =>
renderFieldGroup(group, { ...detailPopupData, ...additionalDetailData }),
);
})()}
</>
)}
</div>
@@ -145,6 +145,9 @@ interface DbObject {
display_order?: number;
locked?: boolean;
visible?: boolean;
hierarchy_level?: number;
parent_key?: string;
external_key?: string;
}
const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), {
@@ -264,8 +267,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
data_binding: null,
created_at: now, // 고정된 값 사용
updated_at: now, // 고정된 값 사용
material_count: obj.materialCount,
material_preview_height: obj.materialPreview?.height,
material_count: obj.material_count,
material_preview_height: obj.material_preview?.height,
}));
}, [placedObjects, layoutId]);
@@ -674,10 +677,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
obj.type === "location-stp" ||
obj.type === "location-temp" ||
obj.type === "location-dest") &&
obj.locaKey,
obj.loca_key,
);
if (locationObjects.length > 0 && dbConnectionId && materialTableName) {
const locaKeys = locationObjects.map((obj) => obj.locaKey!);
const locaKeys = locationObjects.map((obj) => obj.loca_key!);
setTimeout(() => {
loadMaterialCountsForLocations(locaKeys, dbConnectionId, materialTableName);
}, 100);
@@ -887,7 +890,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
]);
if (countsResponse.success && countsResponse.data && countsResponse.data.length > 0) {
const materialCount = countsResponse.data[0].count;
const materialCount = countsResponse.data[0].material_count;
// 자재 개수에 비례해서 높이(Y축) 설정 (최소 5, 최대 30)
// 자재 1개 = 높이 5, 자재 10개 = 높이 15, 자재 50개 = 높이 30
@@ -913,12 +916,12 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
position: { x, y: yPosition, z },
size: objectSize,
color: OBJECT_COLORS[draggedTool] || DEFAULT_COLOR, // 타입별 기본 색상
areaKey,
locaKey,
locType,
hierarchyLevel,
parentKey,
externalKey,
area_key: areaKey,
loca_key: locaKey,
loc_type: locType,
hierarchy_level: hierarchyLevel,
parent_key: parentKey,
external_key: externalKey,
};
// 공간적 종속성 검증
@@ -928,15 +931,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
id: newObject.id,
position: newObject.position,
size: newObject.size,
hierarchyLevel: newObject.hierarchyLevel || 1,
parentId: newObject.parentId,
hierarchyLevel: newObject.hierarchy_level || 1,
parentId: newObject.parent_id,
},
placedObjects.map((obj) => ({
id: obj.id,
position: obj.position,
size: obj.size,
hierarchyLevel: obj.hierarchyLevel || 1,
parentId: obj.parentId,
hierarchyLevel: obj.hierarchy_level || 1,
parentId: obj.parent_id,
})),
);
@@ -955,24 +958,24 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const parentObj = placedObjects.find((obj) => obj.id === validation.parent!.id);
// 2. 논리적 키 검사 (DB에서 가져온 데이터인 경우)
if (parentObj && parentObj.externalKey && newObject.parentKey) {
if (parentObj.externalKey !== newObject.parentKey) {
if (parentObj && parentObj.external_key && newObject.parent_key) {
if (parentObj.external_key !== newObject.parent_key) {
toast({
variant: "destructive",
title: "배치 오류",
description: `이 Location은 '${newObject.parentKey}' Area에만 배치할 수 있습니다. (현재 선택된 Area: ${parentObj.externalKey})`,
description: `이 Location은 '${newObject.parent_key}' Area에만 배치할 수 있습니다. (현재 선택된 Area: ${parentObj.external_key})`,
});
return;
}
}
newObject.parentId = validation.parent.id;
} else if (newObject.parentKey) {
newObject.parent_id = validation.parent.id;
} else if (newObject.parent_key) {
// DB 데이터인데 부모 영역 위에 놓이지 않은 경우
toast({
variant: "destructive",
title: "배치 오류",
description: `이 Location은 '${newObject.parentKey}' Area 내부에 배치해야 합니다.`,
description: `이 Location은 '${newObject.parent_key}' Area 내부에 배치해야 합니다.`,
});
return;
}
@@ -1067,18 +1070,18 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
setSelectedObject(obj || null);
// Area를 클릭한 경우, 해당 Area의 Location 목록 로드
if (obj && obj.type === "area" && obj.areaKey && selectedDbConnection) {
loadLocationsForArea(obj.areaKey);
if (obj && obj.type === "area" && obj.area_key && selectedDbConnection) {
loadLocationsForArea(obj.area_key);
setShowMaterialPanel(false);
}
// Location을 클릭한 경우, 해당 Location의 자재 목록 로드 (STP는 자재 미적재이므로 제외)
else if (
obj &&
(obj.type === "location-bed" || obj.type === "location-temp" || obj.type === "location-dest") &&
obj.locaKey &&
obj.loca_key &&
selectedDbConnection
) {
loadMaterialsForLocation(obj.locaKey);
loadMaterialsForLocation(obj.loca_key);
} else {
setShowMaterialPanel(false);
}
@@ -1103,24 +1106,23 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
setPlacedObjects((prev) =>
prev.map((obj) => {
if (
!obj.locaKey ||
!obj.loca_key ||
obj.type === "location-stp" // STP는 자재 없음
) {
return obj;
}
// 백엔드 응답 필드명: location_key, count (대소문자 모두 체크)
const materialCount = response.data?.find(
(mc: any) => mc.LOCAKEY === obj.locaKey || mc.location_key === obj.locaKey || mc.locakey === obj.locaKey,
(mc: any) => mc.LOCAKEY === obj.loca_key || mc.location_key === obj.loca_key || mc.locakey === obj.loca_key,
);
if (materialCount) {
// count 또는 material_count 필드 사용
const count = materialCount.count || materialCount.material_count || 0;
const count = materialCount.material_count || 0;
const maxLayer = materialCount.max_layer || count;
console.log(`📊 ${obj.locaKey}: 자재 ${count}`);
console.log(`📊 ${obj.loca_key}: 자재 ${count}`);
return {
...obj,
materialCount: Number(count),
materialPreview: {
material_count: Number(count),
material_preview: {
height: maxLayer * 1.5, // 층당 1.5 높이 (시각적)
},
};
@@ -1286,13 +1288,13 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
});
// 2. 하위 계층 객체 이동 시 논리적 키 검증
if (hierarchyConfig && targetObj.hierarchyLevel && targetObj.hierarchyLevel > 1) {
if (hierarchyConfig && targetObj.hierarchy_level && targetObj.hierarchy_level > 1) {
const spatialObjects = updatedObjects.map((obj) => ({
id: obj.id,
position: obj.position,
size: obj.size,
hierarchyLevel: obj.hierarchyLevel || 1,
parentId: obj.parentId,
hierarchyLevel: obj.hierarchy_level || 1,
parentId: obj.parent_id,
}));
const targetSpatialObj = spatialObjects.find((obj) => obj.id === objectId);
@@ -1307,12 +1309,12 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const newParentObj = prev.find((obj) => obj.id === validation.parent!.id);
// DB에서 가져온 데이터인 경우 논리적 키 검증
if (newParentObj && newParentObj.externalKey && targetObj.parentKey) {
if (newParentObj.externalKey !== targetObj.parentKey) {
if (newParentObj && newParentObj.external_key && targetObj.parent_key) {
if (newParentObj.external_key !== targetObj.parent_key) {
toast({
variant: "destructive",
title: "이동 불가",
description: `이 Location은 '${targetObj.parentKey}' Area에만 배치할 수 있습니다. (현재 선택된 Area: ${newParentObj.externalKey})`,
description: `이 Location은 '${targetObj.parent_key}' Area에만 배치할 수 있습니다. (현재 선택된 Area: ${newParentObj.external_key})`,
});
return prev; // 이동 취소
}
@@ -1321,16 +1323,16 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
// 부모 ID 업데이트
updatedObjects = updatedObjects.map((obj) => {
if (obj.id === objectId) {
return { ...obj, parentId: validation.parent!.id };
return { ...obj, parent_id: validation.parent!.id };
}
return obj;
});
} else if (targetObj.parentKey) {
} else if (targetObj.parent_key) {
// DB 데이터인데 부모 영역 밖으로 이동하려는 경우
toast({
variant: "destructive",
title: "이동 불가",
description: `이 Location은 '${targetObj.parentKey}' Area 내부에 있어야 합니다.`,
description: `이 Location은 '${targetObj.parent_key}' Area 내부에 있어야 합니다.`,
});
return prev; // 이동 취소
}
@@ -1342,8 +1344,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
id: obj.id,
position: obj.position,
size: obj.size,
hierarchyLevel: obj.hierarchyLevel || 1,
parentId: obj.parentId,
hierarchyLevel: obj.hierarchy_level || 1,
parentId: obj.parent_id,
}));
const descendants = getAllDescendants(objectId, spatialObjects);
@@ -1471,14 +1473,14 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
setIsSaving(true);
try {
const response = await updateLayout(layoutId, {
layoutName: layoutName,
layout_name: layoutName,
description: undefined,
hierarchyConfig: hierarchyConfig, // 계층 구조 설정
externalDbConnectionId: selectedDbConnection, // 외부 DB 연결 ID
warehouseKey: selectedWarehouse, // 선택된 창고
hierarchy_config: hierarchyConfig, // 계층 구조 설정
external_db_connection_id: selectedDbConnection, // 외부 DB 연결 ID
warehouse_key: selectedWarehouse, // 선택된 창고
objects: placedObjects.map((obj, index) => ({
...obj,
displayOrder: index, // 현재 순서대로 저장
display_order: index, // 현재 순서대로 저장
})),
});
@@ -1524,6 +1526,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
displayOrder: obj.display_order,
locked: obj.locked,
visible: obj.visible !== false,
hierarchyLevel: obj.hierarchy_level || 1,
parentKey: obj.parent_key,
externalKey: obj.external_key,
}));
setPlacedObjects(reloadedObjects);
@@ -1821,7 +1826,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<div className="space-y-2">
{availableAreas.map((area) => {
// 이미 배치된 Area인지 확인
const isPlaced = placedObjects.some((obj) => obj.areaKey === area.AREAKEY);
const isPlaced = placedObjects.some((obj) => obj.area_key === area.AREAKEY);
return (
<div
@@ -1888,7 +1893,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
obj.type === "location-stp" ||
obj.type === "location-temp" ||
obj.type === "location-dest") &&
obj.locaKey === location.LOCAKEY,
obj.loca_key === location.LOCAKEY,
);
return (
@@ -1978,8 +1983,8 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const childLocations = placedObjects.filter(
(obj) =>
obj.type !== "area" &&
obj.areaKey === areaObj.areaKey &&
(obj.parentId === areaObj.id || obj.externalKey === areaObj.externalKey),
obj.area_key === areaObj.area_key &&
(obj.parent_id === areaObj.id || obj.external_key === areaObj.external_key),
);
return (
@@ -2032,9 +2037,9 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<p className="text-muted-foreground mt-1 text-[10px]">
: ({locationObj.position.x.toFixed(1)}, {locationObj.position.z.toFixed(1)})
</p>
{locationObj.locaKey && (
{locationObj.loca_key && (
<p className="text-muted-foreground mt-0.5 text-[10px]">
Key: {locationObj.locaKey}
Key: {locationObj.loca_key}
</p>
)}
</div>
@@ -2125,7 +2130,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<div>
<h3 className="text-lg font-semibold"> </h3>
<p className="text-muted-foreground text-xs">
{selectedObject.name} ({selectedObject.locaKey})
{selectedObject.name} ({selectedObject.loca_key})
</p>
</div>
<Button variant="ghost" size="sm" onClick={() => setShowMaterialPanel(false)}>
@@ -2159,14 +2164,14 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
const layerColumn = hierarchyConfig?.material?.layerColumn || "LOLAYER";
const keyColumn = hierarchyConfig?.material?.keyColumn || "STKKEY";
const displayColumns = hierarchyConfig?.material?.displayColumns || [];
const layerNumber = material[layerColumn] || index + 1;
const layerNumber = ((material as unknown as Record<string, unknown>)[layerColumn] as number | undefined) ?? (index + 1);
return (
<TableRow key={material[keyColumn] || `material-${index}`}>
<TableRow key={String((material as unknown as Record<string, unknown>)[keyColumn] ?? `material-${index}`)}>
<TableCell className="whitespace-nowrap px-3 py-3 text-sm font-medium">{layerNumber}</TableCell>
{displayColumns.map((col) => (
<TableCell key={col.column} className="px-3 py-3 text-sm">
{material[col.column] || "-"}
{String((material as unknown as Record<string, unknown>)[col.column] ?? "-")}
</TableCell>
))}
</TableRow>
@@ -123,9 +123,9 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
if (response.success && response.data) {
const { layout, objects } = response.data;
// 레이아웃 정보 저장 (snake_case와 camelCase 모두 지원)
setLayoutName(layout.layout_name || layout.layoutName);
const dbConnectionId = layout.external_db_connection_id || layout.externalDbConnectionId;
// 레이아웃 정보 저장
setLayoutName(layout.layout_name);
const dbConnectionId = layout.external_db_connection_id;
setExternalDbConnectionId(dbConnectionId);
// hierarchy_config 저장
@@ -267,7 +267,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
if (response.success && response.data) {
const { layout, objects } = response.data;
const dbConnectionId = layout.external_db_connection_id || layout.externalDbConnectionId;
const dbConnectionId = layout.external_db_connection_id;
// hierarchy_config 파싱
let hierarchyConfigData: any = null;
@@ -216,7 +216,7 @@ export function DepartmentStructure({
{/* 인원수 */}
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Users className="h-3 w-3" />
<span>{dept.memberCount || 0}</span>
<span>{dept.member_count || 0}</span>
</div>
</div>
@@ -19,28 +19,28 @@ const MM_TO_PX = 4;
function defaultComponent(type: BarcodeLabelComponent["type"], barcodeType?: string): BarcodeLabelComponent {
const id = `comp_${uuidv4()}`;
const base = { id, type, x: 10 * MM_TO_PX, y: 10 * MM_TO_PX, width: 80, height: 24, zIndex: 0 };
const base = { id, type, x: 10 * MM_TO_PX, y: 10 * MM_TO_PX, width: 80, height: 24, z_index: 0 };
switch (type) {
case "text":
return { ...base, content: "텍스트", fontSize: 10, fontColor: "#000000" };
return { ...base, content: "텍스트", font_size: 10, font_color: "#000000" };
case "barcode": {
const isQR = barcodeType === "QR";
return {
...base,
width: isQR ? 100 : 120,
height: isQR ? 100 : 40,
barcodeType: barcodeType || "CODE128",
barcodeValue: isQR ? "" : "123456789",
showBarcodeText: !isQR,
barcode_type: barcodeType || "CODE128",
barcode_value: isQR ? "" : "123456789",
show_barcode_text: !isQR,
};
}
case "image":
return { ...base, width: 60, height: 60, imageUrl: "", objectFit: "contain" };
return { ...base, width: 60, height: 60, image_url: "", object_fit: "contain" };
case "line":
return { ...base, width: 100, height: 2, lineColor: "#000", lineWidth: 1 };
return { ...base, width: 100, height: 2, line_color: "#000", line_width: 1 };
case "rectangle":
return { ...base, width: 80, height: 40, backgroundColor: "transparent", lineColor: "#000", lineWidth: 1 };
return { ...base, width: 80, height: 40, background_color: "transparent", line_color: "#000", line_width: 1 };
default:
return base;
}
@@ -71,7 +71,7 @@ export function BarcodeDesignerCanvas() {
id: `comp_${uuidv4()}`,
x: snapValueToGrid(x),
y: snapValueToGrid(y),
zIndex: components.length,
z_index: components.length,
};
addComponent(newComp);
},
@@ -38,7 +38,7 @@ function QRJsonFields({
update: (u: Partial<BarcodeLabelComponent>) => void;
}) {
const [pairs, setPairs] = useState<{ key: string; value: string }[]>(() => {
const parsed = parseQRJsonValue(selected.barcodeValue || "");
const parsed = parseQRJsonValue(selected.barcode_value || "");
if (Object.keys(parsed).length > 0) {
return Object.entries(parsed).map(([key, value]) => ({ key, value }));
}
@@ -47,11 +47,11 @@ function QRJsonFields({
// 바코드 값이 바깥에서 바뀌면 파싱해서 동기화
useEffect(() => {
const parsed = parseQRJsonValue(selected.barcodeValue || "");
const parsed = parseQRJsonValue(selected.barcode_value || "");
if (Object.keys(parsed).length > 0) {
setPairs(Object.entries(parsed).map(([key, value]) => ({ key, value: String(value ?? "") })));
}
}, [selected.barcodeValue]);
}, [selected.barcode_value]);
const applyJson = () => {
const obj: Record<string, string> = {};
@@ -59,7 +59,7 @@ function QRJsonFields({
const k = key.trim();
if (k) obj[k] = value.trim();
});
update({ barcodeValue: JSON.stringify(obj) });
update({ barcode_value: JSON.stringify(obj) });
};
const setPair = (index: number, field: "key" | "value", val: string) => {
@@ -237,16 +237,16 @@ export function BarcodeDesignerRightPanel() {
type="number"
min={6}
max={72}
value={selected.fontSize || 10}
onChange={(e) => update({ fontSize: Number(e.target.value) || 10 })}
value={selected.font_size || 10}
onChange={(e) => update({ font_size: Number(e.target.value) || 10 })}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
type="color"
value={selected.fontColor || "#000000"}
onChange={(e) => update({ fontColor: e.target.value })}
value={selected.font_color || "#000000"}
onChange={(e) => update({ font_color: e.target.value })}
className="h-9 w-20 p-1"
/>
</div>
@@ -258,8 +258,8 @@ export function BarcodeDesignerRightPanel() {
<div>
<Label className="text-xs"> </Label>
<Select
value={selected.barcodeType || "CODE128"}
onValueChange={(v) => update({ barcodeType: v })}
value={selected.barcode_type || "CODE128"}
onValueChange={(v) => update({ barcode_type: v })}
>
<SelectTrigger className="h-9">
<SelectValue />
@@ -273,21 +273,21 @@ export function BarcodeDesignerRightPanel() {
</SelectContent>
</Select>
</div>
{selected.barcodeType === "QR" && (
{selected.barcode_type === "QR" && (
<QRJsonFields selected={selected} update={update} />
)}
<div>
<Label className="text-xs">{selected.barcodeType === "QR" ? "값 (직접 JSON 입력)" : "값"}</Label>
<Label className="text-xs">{selected.barcode_type === "QR" ? "값 (직접 JSON 입력)" : "값"}</Label>
<Input
value={selected.barcodeValue || ""}
onChange={(e) => update({ barcodeValue: e.target.value })}
placeholder={selected.barcodeType === "QR" ? '{"part_no":"","part_name":"","spec":""}' : "123456789"}
value={selected.barcode_value || ""}
onChange={(e) => update({ barcode_value: e.target.value })}
placeholder={selected.barcode_type === "QR" ? '{"part_no":"","part_name":"","spec":""}' : "123456789"}
/>
</div>
<div className="flex items-center gap-2">
<Switch
checked={selected.showBarcodeText !== false}
onCheckedChange={(v) => update({ showBarcodeText: v })}
checked={selected.show_barcode_text !== false}
onCheckedChange={(v) => update({ show_barcode_text: v })}
/>
<Label className="text-xs"> (1D)</Label>
</div>
@@ -301,16 +301,16 @@ export function BarcodeDesignerRightPanel() {
<Input
type="number"
min={1}
value={selected.lineWidth || 1}
onChange={(e) => update({ lineWidth: Number(e.target.value) || 1 })}
value={selected.line_width || 1}
onChange={(e) => update({ line_width: Number(e.target.value) || 1 })}
/>
</div>
<div>
<Label className="text-xs"></Label>
<Input
type="color"
value={selected.lineColor || "#000000"}
onChange={(e) => update({ lineColor: e.target.value })}
value={selected.line_color || "#000000"}
onChange={(e) => update({ line_color: e.target.value })}
className="h-9 w-20 p-1"
/>
</div>
@@ -324,16 +324,16 @@ export function BarcodeDesignerRightPanel() {
<Input
type="number"
min={0}
value={selected.lineWidth ?? 1}
onChange={(e) => update({ lineWidth: Number(e.target.value) || 0 })}
value={selected.line_width ?? 1}
onChange={(e) => update({ line_width: Number(e.target.value) || 0 })}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
type="color"
value={selected.lineColor || "#000000"}
onChange={(e) => update({ lineColor: e.target.value })}
value={selected.line_color || "#000000"}
onChange={(e) => update({ line_color: e.target.value })}
className="h-9 w-20 p-1"
/>
</div>
@@ -341,8 +341,8 @@ export function BarcodeDesignerRightPanel() {
<Label className="text-xs"> </Label>
<Input
type="color"
value={selected.backgroundColor || "#ffffff"}
onChange={(e) => update({ backgroundColor: e.target.value })}
value={selected.background_color || "#ffffff"}
onChange={(e) => update({ background_color: e.target.value })}
className="h-9 w-20 p-1"
/>
</div>
@@ -353,8 +353,8 @@ export function BarcodeDesignerRightPanel() {
<div>
<Label className="text-xs"> URL</Label>
<Input
value={selected.imageUrl || ""}
onChange={(e) => update({ imageUrl: e.target.value })}
value={selected.image_url || ""}
onChange={(e) => update({ image_url: e.target.value })}
placeholder="https://..."
/>
<p className="text-muted-foreground mt-1 text-xs"> </p>
@@ -71,7 +71,7 @@ export function BarcodeDesignerToolbar() {
setCreating(true);
try {
const createRes = await barcodeApi.createLabel({
labelNameKor: name,
label_name_kor: name,
});
if (!createRes.success || !createRes.data?.labelId) throw new Error(createRes.message || "생성 실패");
const newId = createRes.data.labelId;
@@ -79,7 +79,7 @@ export function BarcodeDesignerToolbar() {
await barcodeApi.saveLayout(newId, {
width_mm: widthMm,
height_mm: heightMm,
components: components.map((c, i) => ({ ...c, zIndex: i })),
components: components.map((c, i) => ({ ...c, z_index: i })),
});
toast({ title: "저장됨", description: "라벨이 생성되었습니다." });
@@ -145,7 +145,7 @@ export function BarcodeDesignerToolbar() {
layout={{
width_mm: widthMm,
height_mm: heightMm,
components: components.map((c, i) => ({ ...c, zIndex: i })),
components: components.map((c, i) => ({ ...c, z_index: i })),
}}
labelName={labelMaster?.label_name_kor || "라벨"}
/>
@@ -147,12 +147,12 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
top: component.y,
width: component.width,
height: component.height,
zIndex: component.zIndex,
zIndex: component.z_index,
};
const border = selected ? "2px solid #2563eb" : "1px solid transparent";
const isBarcode = component.type === "barcode";
const isQR = component.barcodeType === "QR";
const isQR = component.barcode_type === "QR";
const content = () => {
switch (component.type) {
@@ -160,9 +160,9 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
return (
<div
style={{
fontSize: component.fontSize || 10,
color: component.fontColor || "#000",
fontWeight: component.fontWeight || "normal",
fontSize: component.font_size || 10,
color: component.font_color || "#000",
fontWeight: component.font_weight || "normal",
overflow: "hidden",
wordBreak: "break-all",
width: "100%",
@@ -176,29 +176,29 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
if (isQR) {
return (
<QRRender
value={component.barcodeValue || ""}
value={component.barcode_value || ""}
size={Math.min(component.width, component.height)}
/>
);
}
return (
<Barcode1DRender
value={component.barcodeValue || "123456789"}
format={component.barcodeType || "CODE128"}
value={component.barcode_value || "123456789"}
format={component.barcode_type || "CODE128"}
width={component.width}
height={component.height}
showText={component.showBarcodeText !== false}
showText={component.show_barcode_text !== false}
/>
);
case "image":
return component.imageUrl ? (
return component.image_url ? (
<img
src={getFullImageUrl(component.imageUrl)}
src={getFullImageUrl(component.image_url)}
alt=""
style={{
width: "100%",
height: "100%",
objectFit: (component.objectFit as "contain") || "contain",
objectFit: (component.object_fit as "contain") || "contain",
}}
/>
) : (
@@ -211,9 +211,9 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
<div
style={{
width: "100%",
height: component.lineWidth || 1,
backgroundColor: component.lineColor || "#000",
marginTop: (component.height - (component.lineWidth || 1)) / 2,
height: component.line_width || 1,
backgroundColor: component.line_color || "#000",
marginTop: (component.height - (component.line_width || 1)) / 2,
}}
/>
);
@@ -223,8 +223,8 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
style={{
width: "100%",
height: "100%",
backgroundColor: component.backgroundColor || "transparent",
border: `${component.lineWidth || 1}px solid ${component.lineColor || "#000"}`,
backgroundColor: component.background_color || "transparent",
border: `${component.line_width || 1}px solid ${component.line_color || "#000"}`,
}}
/>
);
@@ -98,27 +98,27 @@ export function CascadingDropdown({
// 부모 값 변경 시 자동 초기화
useEffect(() => {
if (config.clearOnParentChange !== false) {
if (prevParentValueRef.current !== undefined &&
prevParentValueRef.current !== parentValue &&
if (config.clear_on_parent_change !== false) {
if (prevParentValueRef.current !== undefined &&
prevParentValueRef.current !== parentValue &&
value) {
// 부모 값이 변경되면 현재 값 초기화
onChange?.("");
}
}
prevParentValueRef.current = parentValue;
}, [parentValue, config.clearOnParentChange, value, onChange]);
}, [parentValue, config.clear_on_parent_change, value, onChange]);
// 부모 값이 없을 때 메시지
const getPlaceholder = () => {
if (!parentValue) {
return config.emptyParentMessage || "상위 항목을 먼저 선택하세요";
return config.empty_parent_message || "상위 항목을 먼저 선택하세요";
}
if (loading) {
return config.loadingMessage || "로딩 중...";
return config.loading_message || "로딩 중...";
}
if (options.length === 0) {
return config.noOptionsMessage || "선택 가능한 항목이 없습니다";
return config.no_options_message || "선택 가능한 항목이 없습니다";
}
return placeholder || "선택하세요";
};
@@ -152,7 +152,7 @@ export function CascadingDropdown({
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-sm">
{config.loadingMessage || "로딩 중..."}
{config.loading_message || "로딩 중..."}
</span>
</div>
) : (
@@ -163,8 +163,8 @@ export function CascadingDropdown({
{options.length === 0 ? (
<div className="text-muted-foreground px-2 py-4 text-center text-sm">
{!parentValue
? config.emptyParentMessage || "상위 항목을 먼저 선택하세요"
: config.noOptionsMessage || "선택 가능한 항목이 없습니다"}
? config.empty_parent_message || "상위 항목을 먼저 선택하세요"
: config.no_options_message || "선택 가능한 항목이 없습니다"}
</div>
) : (
options.map((option) => (
+15 -15
View File
@@ -257,7 +257,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
console.log("🔍 엔티티 데이터 조회:", refTable);
const response = await DynamicFormApi.getTableData(refTable, {
page: 1,
pageSize: 1000,
page_size: 1000,
});
console.log("🔍 엔티티 데이터 응답:", response);
// getTableData는 { success, data: [...] } 형식으로 반환
@@ -521,12 +521,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const typeResponse = await getTableColumns(tbl);
if (typeResponse.success && typeResponse.data?.columns) {
for (const tc of typeResponse.data.columns) {
if (tc.inputType === "numbering") {
if (tc.input_type === "numbering") {
try {
const settings = typeof tc.detailSettings === "string"
? JSON.parse(tc.detailSettings) : tc.detailSettings;
const settings = typeof tc.detail_settings === "string"
? JSON.parse(tc.detail_settings) : tc.detail_settings;
if (settings?.numberingRuleId) {
numberingColSet.add(tc.columnName);
numberingColSet.add(tc.column_name);
}
} catch { /* 파싱 실패 무시 */ }
}
@@ -672,7 +672,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
}
const categoryColumns = colResponse.data.columns.filter(
(col: any) => col.inputType === "category"
(col: any) => col.input_type === "category"
);
if (categoryColumns.length === 0) {
@@ -693,13 +693,13 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
: mapping.systemColumn;
const catCol = categoryColumns.find(
(cc: any) => (cc.columnName || cc.column_name) === rawName
(cc: any) => cc.column_name === rawName
);
if (catCol) {
mappedCategoryColumns.push({
systemCol: rawName,
excelCol: mapping.excelColumn,
displayName: catCol.displayName || catCol.display_name || rawName,
displayName: catCol.display_name || rawName,
});
}
}
@@ -968,18 +968,18 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
if (response.success && response.data?.columns) {
for (const col of response.data.columns) {
if (col.inputType === "numbering") {
if (col.input_type === "numbering") {
try {
const settings =
typeof col.detailSettings === "string"
? JSON.parse(col.detailSettings)
: col.detailSettings;
typeof col.detail_settings === "string"
? JSON.parse(col.detail_settings)
: col.detail_settings;
if (settings?.numberingRuleId) {
console.log(
`✅ 채번 컬럼 자동 감지: ${col.columnName} → 규칙 ID: ${settings.numberingRuleId}`
`✅ 채번 컬럼 자동 감지: ${col.column_name} → 규칙 ID: ${settings.numberingRuleId}`
);
return {
columnName: col.columnName,
columnName: col.column_name,
numberingRuleId: settings.numberingRuleId,
};
}
@@ -1173,7 +1173,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
// DynamicFormApi.getTableData 사용
const existingResponse = await DynamicFormApi.getTableData(tableName, {
page: 1,
pageSize: 10000,
page_size: 10000,
});
console.log("📊 중복 체크용 기존 데이터 조회 결과:", existingResponse);
@@ -275,8 +275,8 @@ export function MultiColumnHierarchySelect({
</SelectTrigger>
<SelectContent>
{largeOptions.map((code) => {
const codeValue = code.codeValue || code.code_value || "";
const codeName = code.codeName || code.code_name || "";
const codeValue = code.code_value || "";
const codeName = code.code_name || "";
return (
<SelectItem key={codeValue} value={codeValue}>
{codeName}
@@ -321,8 +321,8 @@ export function MultiColumnHierarchySelect({
</div>
) : (
mediumOptions.map((code) => {
const codeValue = code.codeValue || code.code_value || "";
const codeName = code.codeName || code.code_name || "";
const codeValue = code.code_value || "";
const codeName = code.code_name || "";
return (
<SelectItem key={codeValue} value={codeValue}>
{codeName}
@@ -368,8 +368,8 @@ export function MultiColumnHierarchySelect({
</div>
) : (
smallOptions.map((code) => {
const codeValue = code.codeValue || code.code_value || "";
const codeName = code.codeName || code.code_name || "";
const codeValue = code.code_value || "";
const codeName = code.code_name || "";
return (
<SelectItem key={codeValue} value={codeValue}>
{codeName}
@@ -367,14 +367,14 @@ export const MultiTableExcelUploadModal: React.FC<MultiTableExcelUploadModalProp
if (!colResponse.success || !colResponse.data?.columns) continue;
const categoryColumns = colResponse.data.columns.filter(
(col: any) => col.inputType === "category"
(col: any) => col.input_type === "category"
);
if (categoryColumns.length === 0) continue;
// 매핑된 컬럼 중 카테고리 타입인 것 찾기
for (const catCol of categoryColumns) {
const catColName = catCol.columnName || catCol.column_name;
const catDisplayName = catCol.displayName || catCol.display_name || catColName;
const catColName = catCol.column_name;
const catDisplayName = catCol.display_name || catColName;
// level.columns에서 해당 dbColumn 찾기
const levelCol = level.columns.find((lc) => lc.dbColumn === catColName);
+14 -14
View File
@@ -46,7 +46,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const { userId, userName, user } = useAuth();
const splitPanelContext = useSplitPanelContext();
const tabId = useTabId();
const activeTabId = useTabStore((s) => s[s.mode].activeTabId);
const activeTabId = useTabStore((s) => s[s.mode].active_tab_id);
const [modalState, setModalState] = useState<ScreenModalState>({
isOpen: false,
@@ -112,12 +112,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const bindingUpdates: Record<string, any> = {};
for (const comp of screenData.components) {
const db =
comp.componentConfig?.dataBinding ||
comp.component_config?.dataBinding ||
(comp as any).dataBinding;
if (!db?.sourceComponentId || !db?.sourceColumn) continue;
if (db.sourceComponentId !== detail.source) continue;
const colName = (comp as any).columnName || comp.componentConfig?.columnName;
const colName = (comp as any).columnName || comp.component_config?.columnName;
if (!colName) continue;
const selectedRow = detail.data[0];
@@ -209,7 +209,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const handleOpenModal = (event: CustomEvent) => {
// 활성 탭에서만 이벤트 처리 (다른 탭의 ScreenModal 인스턴스는 무시)
const storeState = useTabStore.getState();
const currentActiveTabId = storeState[storeState.mode].activeTabId;
const currentActiveTabId = storeState[storeState.mode].active_tab_id;
if (tabId && tabId !== currentActiveTabId) return;
const {
@@ -290,7 +290,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
// parentDataMapping에서 명시된 필드만 추출
const parentDataMapping = splitPanelContext?.parentDataMapping || [];
const parentDataMapping = splitPanelContext?.parent_data_mapping || [];
// 부모 데이터 소스
// 🔧 수정: 여러 소스를 병합 (우선순위: splitPanelParentData > selectedLeftData > 기존 formData의 링크 필드)
@@ -298,7 +298,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// - splitPanelParentData: item_info 데이터 (screen 226에서 전달)
// - selectedLeftData: customer_mng 데이터 (SplitPanel 좌측 선택)
// - 기존 formData: 이전 모달에서 설정된 link 필드 (customer_code 등)
const contextData = splitPanelContext?.selectedLeftData || {};
const contextData = splitPanelContext?.selected_left_data || {};
const eventData = splitPanelParentData && Object.keys(splitPanelParentData).length > 0
? splitPanelParentData
: {};
@@ -342,9 +342,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// parentDataMapping에 정의된 필드만 전달
for (const mapping of parentDataMapping) {
const sourceValue = rawParentData[mapping.sourceColumn];
const sourceValue = rawParentData[mapping.source_column];
if (sourceValue !== undefined && sourceValue !== null) {
parentData[mapping.targetColumn] = sourceValue;
parentData[mapping.target_column] = sourceValue;
}
}
@@ -524,7 +524,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const urlParams = new URLSearchParams(window.location.search);
const mode = urlParams.get("mode");
const editId = urlParams.get("editId");
const tableName = urlParams.get("tableName") || screenInfo.tableName;
const tableName = urlParams.get("tableName") || screenInfo.table_name;
const groupByColumnsParam = urlParams.get("groupByColumns");
const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // 🆕 Primary Key 컬럼명
@@ -723,7 +723,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
console.log("[ScreenModal] 조건부 레이어 로드 완료:", layerDefs.length, "개",
layerDefs.map((l) => ({
id: l.id, name: l.name, conditionValue: l.conditionValue,
id: l.id, name: l.name, conditionValue: l.condition_value,
componentCount: l.components.length,
condition: l.condition,
}))
@@ -744,7 +744,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
conditionalLayers.forEach((layer) => {
if (!layer.condition) return;
const { targetComponentId, operator, value } = layer.condition;
const { target_component_id: targetComponentId, operator, value } = layer.condition;
if (!targetComponentId) return;
// V2 레이아웃: overrides.columnName 우선
@@ -813,7 +813,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const currentActiveLayerIds = conditionalLayers
.filter((layer) => {
if (!layer.condition) return false;
const { targetComponentId, operator, value } = layer.condition;
const { target_component_id: targetComponentId, operator, value } = layer.condition;
if (!targetComponentId) return false;
const allComponents = screenData?.components || [];
@@ -1075,8 +1075,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
) : screenData ? (
<ScreenContextProvider
screenId={modalState.screen_id || undefined}
tableName={screenData.screenInfo?.tableName}
screen_id={modalState.screen_id || undefined}
table_name={screenData.screenInfo?.table_name}
>
<ActiveTabProvider>
<TableOptionsProvider>
@@ -54,8 +54,8 @@ export default function CargoListWidget({ element }: CargoListWidgetProps) {
},
body: JSON.stringify({
query: element.dataSource.query,
connection_type: element.dataSource.connection_type || "current",
connectionId: element.dataSource.connectionId,
connection_type: element.dataSource.connectionType || "current",
connectionId: element.dataSource.externalConnectionId,
}),
});
@@ -111,8 +111,8 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
}
const queryParams: Record<string, string> = {};
if (source.query_params) {
source.query_params.forEach((param) => {
if (source.queryParams) {
source.queryParams.forEach((param) => {
if (param.key && param.value) {
queryParams[param.key] = param.value;
}
@@ -150,15 +150,15 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
}
let apiData = result.data;
if (source.json_path) {
const pathParts = source.json_path.split(".");
if (source.jsonPath) {
const pathParts = source.jsonPath.split(".");
for (const part of pathParts) {
apiData = apiData?.[part];
}
}
const rows = Array.isArray(apiData) ? apiData : [apiData];
return applyColumnMapping(rows, source.column_mapping);
return applyColumnMapping(rows, source.columnMapping);
};
// Database 데이터 로딩
@@ -168,9 +168,9 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
}
let result;
if (source.connection_type === "external" && source.external_connection_id) {
if (source.connectionType === "external" && source.externalConnectionId) {
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
result = await ExternalDbConnectionAPI.executeQuery(parseInt(source.external_connection_id), source.query);
result = await ExternalDbConnectionAPI.executeQuery(parseInt(source.externalConnectionId), source.query);
} else {
const { dashboardApi } = await import("@/lib/api/dashboard");
@@ -190,8 +190,8 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
throw new Error(result.message || "쿼리 실패");
}
const rows = result.rows || result.data || [];
return applyColumnMapping(rows, source.column_mapping);
const rows = (result as any).rows || result.data || [];
return applyColumnMapping(rows, source.columnMapping);
};
// 초기 로드
@@ -206,7 +206,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
if (!dataSources || dataSources.length === 0) return;
const intervals = dataSources
.map((ds) => ds.refresh_interval)
.map((ds) => ds.refreshInterval)
.filter((interval): interval is number => typeof interval === "number" && interval > 0);
if (intervals.length === 0) return;
@@ -55,8 +55,8 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
},
body: JSON.stringify({
query: element.dataSource.query,
connection_type: element.dataSource.connection_type || "current",
connectionId: element.dataSource.connectionId,
connection_type: element.dataSource.connectionType || "current",
connectionId: element.dataSource.externalConnectionId,
}),
});
@@ -49,7 +49,7 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
body: JSON.stringify({
query: element.dataSource.query,
connectionType: element.dataSource.connectionType || "current",
connectionId: element.dataSource.connectionId,
connectionId: element.dataSource.externalConnectionId,
}),
});
@@ -50,7 +50,7 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
body: JSON.stringify({
query: element.dataSource.query,
connectionType: element.dataSource.connectionType || "current",
connectionId: element.dataSource.connectionId,
connectionId: element.dataSource.externalConnectionId,
}),
});
@@ -270,7 +270,7 @@ export default function ExchangeWidget({
{/* 데이터 출처 */}
<div className="mt-3 pt-2 border-t text-center">
<p className="text-xs text-muted-foreground">: {exchangeRate.source}</p>
<p className="text-xs text-muted-foreground">출처: 환율 API</p>
</div>
</div>
);
@@ -43,7 +43,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
body: JSON.stringify({
query: element.dataSource.query,
connectionType: element.dataSource.connectionType || "current",
connectionId: element.dataSource.connectionId,
connectionId: element.dataSource.externalConnectionId,
}),
});
@@ -12,6 +12,7 @@ import {
import {
AlertDialog,
AlertDialogAction,
AlertDialogFooter,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
@@ -43,15 +44,15 @@ import { toast } from "sonner";
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
isOpen,
connection,
companyCode,
company_code: companyCode,
onConfirm,
onCancel,
}) => {
const [config, setConfig] = useState<ConnectionConfig>({
relationshipName: "",
connectionType: "simple-key",
fromColumnName: "",
toColumnName: "",
relationship_name: "",
connection_type: "simple-key",
from_column_name: "",
to_column_name: "",
settings: {},
});
@@ -125,28 +126,28 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
actions: actionsData.map((action: Record<string, unknown>) => ({
id: (action.id as string) || `action-${Date.now()}`,
name: (action.name as string) || "새 액션",
actionType: (action.actionType as "insert" | "update" | "delete" | "upsert") || "insert",
action_type: (action.action_type as "insert" | "update" | "delete" | "upsert") || "insert",
conditions: Array.isArray(action.conditions)
? (action.conditions as ConditionNode[]).map((condition) => ({
...condition,
operator: condition.operator || "=", // 기본값 보장
}))
: [],
fieldMappings: Array.isArray(action.fieldMappings)
? action.fieldMappings.map((mapping: Record<string, unknown>) => ({
sourceTable: (mapping.sourceTable as string) || "",
sourceField: (mapping.sourceField as string) || "",
targetTable: (mapping.targetTable as string) || "",
targetField: (mapping.targetField as string) || "",
defaultValue: (mapping.defaultValue as string) || "",
transformFunction: (mapping.transformFunction as string) || "",
field_mappings: Array.isArray(action.field_mappings)
? action.field_mappings.map((mapping: Record<string, unknown>) => ({
source_table: (mapping.source_table as string) || "",
source_field: (mapping.source_field as string) || "",
target_table: (mapping.target_table as string) || "",
target_field: (mapping.target_field as string) || "",
default_value: (mapping.default_value as string) || "",
transform_function: (mapping.transformFunction as string) || "",
}))
: [],
splitConfig: action.splitConfig
split_config: action.split_config
? {
sourceField: ((action.splitConfig as Record<string, unknown>).sourceField as string) || "",
delimiter: ((action.splitConfig as Record<string, unknown>).delimiter as string) || ",",
targetField: ((action.splitConfig as Record<string, unknown>).targetField as string) || "",
source_field: ((action.split_config as Record<string, unknown>).source_field as string) || "",
delimiter: ((action.split_config as Record<string, unknown>).delimiter as string) || ",",
target_field: ((action.split_config as Record<string, unknown>).target_field as string) || "",
}
: undefined,
})),
@@ -178,8 +179,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}
setExternalCallSettings({
configId: (externalCallData.configId as number) || undefined,
configName: (externalCallData.configName as string) || undefined,
config_id: (externalCallData.config_id as number) || undefined,
config_name: (externalCallData.config_name as string) || undefined,
message: (externalCallData.message as string) || "",
});
}
@@ -210,25 +211,25 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 모달이 열릴 때마다 캐시 초기화 (라벨 업데이트 반영)
setTableColumnsCache({});
const fromTableName = connection.fromNode.tableName;
const toTableName = connection.toNode.tableName;
const fromDisplayName = connection.fromNode.displayName;
const toDisplayName = connection.toNode.displayName;
const fromTableName = connection.from_node.table_name;
const toTableName = connection.to_node.table_name;
const fromDisplayName = connection.from_node.display_name;
const toDisplayName = connection.to_node.display_name;
// 테이블 선택 설정
setSelectedFromTable(fromTableName);
setSelectedToTable(toTableName);
// 기존 관계 정보가 있으면 사용, 없으면 기본값 설정
const existingRel = connection.existingRelationship;
const existingRel = connection.existing_relationship;
const connectionType =
(existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key";
(existingRel?.connection_type as "simple-key" | "data-save" | "external-call") || "simple-key";
setConfig({
relationshipName: existingRel?.relationshipName || `${fromDisplayName}${toDisplayName}`,
connectionType,
fromColumnName: "",
toColumnName: "",
relationship_name: existingRel?.relationship_name || `${fromDisplayName}${toDisplayName}`,
connection_type: connectionType,
from_column_name: "",
to_column_name: "",
settings: existingRel?.settings || {},
});
@@ -248,17 +249,17 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
setSelectedToColumns([]);
// 선택된 컬럼 정보가 있다면 설정
if (connection.selectedColumnsData) {
const fromColumns = connection.selectedColumnsData[fromTableName]?.columns || [];
const toColumns = connection.selectedColumnsData[toTableName]?.columns || [];
if (connection.selected_columns_data) {
const fromColumns = connection.selected_columns_data[fromTableName]?.columns || [];
const toColumns = connection.selected_columns_data[toTableName]?.columns || [];
setSelectedFromColumns(fromColumns);
setSelectedToColumns(toColumns);
setConfig((prev) => ({
...prev,
fromColumnName: fromColumns.join(", "),
toColumnName: toColumns.join(", "),
from_column_name: fromColumns.join(", "),
to_column_name: toColumns.join(", "),
}));
}
}
@@ -302,8 +303,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
useEffect(() => {
setConfig((prev) => ({
...prev,
fromColumnName: selectedFromColumns.join(", "),
toColumnName: selectedToColumns.join(", "),
from_column_name: selectedFromColumns.join(", "),
to_column_name: selectedToColumns.join(", "),
}));
}, [selectedFromColumns, selectedToColumns]);
@@ -333,12 +334,12 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 필드 매핑에서 사용되는 모든 테이블 수집
dataSaveSettings.actions?.forEach((action) => {
action.fieldMappings?.forEach((mapping) => {
if (mapping.sourceTable && !tableColumnsCache[mapping.sourceTable]) {
tablesToLoad.add(mapping.sourceTable);
action.field_mappings?.forEach((mapping) => {
if (mapping.source_table && !tableColumnsCache[mapping.source_table]) {
tablesToLoad.add(mapping.source_table);
}
if (mapping.targetTable && !tableColumnsCache[mapping.targetTable]) {
tablesToLoad.add(mapping.targetTable);
if (mapping.target_table && !tableColumnsCache[mapping.target_table]) {
tablesToLoad.add(mapping.target_table);
}
});
});
@@ -353,7 +354,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}, [dataSaveSettings.actions, tableColumnsCache]); // eslint-disable-line react-hooks/exhaustive-deps
const handleConfirm = () => {
if (!config.relationshipName || !connection) {
if (!config.relationship_name || !connection) {
toast.error("필수 정보를 모두 입력해주세요.");
return;
}
@@ -362,7 +363,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
let settings = {};
let plan = {}; // plan 변수 선언
switch (config.connectionType) {
switch (config.connection_type) {
case "simple-key":
settings = simpleKeySettings;
break;
@@ -371,10 +372,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증
for (const action of dataSaveSettings.actions) {
if (action.actionType !== "insert") {
if (action.action_type !== "insert") {
if (!action.conditions || action.conditions.length === 0) {
toast.error(
`${action.actionType.toUpperCase()} 액션은 실행조건이 필수입니다. '${action.name}' 액션에 실행조건을 추가해주세요.`,
`${action.action_type.toUpperCase()} 액션은 실행조건이 필수입니다. '${action.name}' 액션에 실행조건을 추가해주세요.`,
);
return;
}
@@ -393,7 +394,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
if (!hasValidConditions) {
toast.error(
`${action.actionType.toUpperCase()} 액션은 완전한 실행조건이 필요합니다. '${action.name}' 액션에 필드, 연산자, 값을 모두 설정해주세요.`,
`${action.action_type.toUpperCase()} 액션은 완전한 실행조건이 필요합니다. '${action.name}' 액션에 필드, 연산자, 값을 모두 설정해주세요.`,
);
return;
}
@@ -410,7 +411,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 기존 설정 호환성 유지
plan = {
externalCall: {
configId: externalCallSettings.configId,
configId: externalCallSettings.config_id,
configName: externalCallSettings.configName,
message: externalCallSettings.message,
},
@@ -421,7 +422,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}
// 단순 키값 연결일 때만 컬럼 선택 검증
if (config.connectionType === "simple-key") {
if (config.connection_type === "simple-key") {
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
return;
@@ -429,26 +430,26 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}
// 선택된 테이블과 컬럼 정보 사용
const fromTableName = selectedFromTable || connection.fromNode.tableName;
const toTableName = selectedToTable || connection.toNode.tableName;
const fromTableName = selectedFromTable || connection.from_node.table_name;
const toTableName = selectedToTable || connection.to_node.table_name;
// 조건부 연결 설정 데이터 준비
const conditionalSettings = isConditionalConnection(config.connectionType)
const conditionalSettings = isConditionalConnection(config.connection_type)
? {
control: {
triggerType: "insert",
conditionTree: conditions.length > 0 ? conditions : null,
},
category: {
type: config.connectionType,
type: config.connection_type,
},
plan: {
sourceTable: fromTableName,
targetActions:
config.connectionType === "data-save"
config.connection_type === "data-save"
? dataSaveSettings.actions.map((action) => ({
id: action.id,
actionType: action.actionType,
actionType: action.action_type,
enabled: true,
conditions:
action.conditions?.map((condition) => {
@@ -459,15 +460,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}
return baseCondition;
}) || [],
fieldMappings: action.fieldMappings.map((mapping) => ({
sourceTable: mapping.sourceTable,
sourceField: mapping.sourceField,
targetTable: mapping.targetTable,
targetField: mapping.targetField,
defaultValue: mapping.defaultValue,
transformFunction: mapping.transformFunction,
fieldMappings: action.field_mappings.map((mapping) => ({
sourceTable: mapping.source_table,
sourceField: mapping.source_field,
targetTable: mapping.target_table,
targetField: mapping.target_field,
defaultValue: mapping.default_value,
transformFunction: mapping.transform_function,
})),
splitConfig: action.splitConfig,
splitConfig: action.split_config,
}))
: [],
},
@@ -475,17 +476,17 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
: {};
// 컬럼 정보는 단순 키값 연결일 때만 사용
const finalFromColumns = config.connectionType === "simple-key" ? selectedFromColumns : [];
const finalToColumns = config.connectionType === "simple-key" ? selectedToColumns : [];
const finalFromColumns = config.connection_type === "simple-key" ? selectedFromColumns : [];
const finalToColumns = config.connection_type === "simple-key" ? selectedToColumns : [];
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
const relationshipData: TableRelationship = {
relationship_name: config.relationshipName,
relationship_name: config.relationship_name,
from_table_name: fromTableName,
to_table_name: toTableName,
from_column_name: finalFromColumns.join(","), // 여러 컬럼을 콤마로 구분
to_column_name: finalToColumns.join(","), // 여러 컬럼을 콤마로 구분
connection_type: config.connectionType,
connection_type: config.connection_type,
company_code: companyCode,
settings: {
...settings,
@@ -495,17 +496,17 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
};
// 성공 모달 표시를 위한 상태 설정
setCreatedConnectionName(config.relationshipName);
setCreatedConnectionName(config.relationship_name);
setPendingRelationshipData(relationshipData);
setShowSuccessModal(true);
};
const handleCancel = () => {
setConfig({
relationshipName: "",
connectionType: "simple-key",
fromColumnName: "",
toColumnName: "",
relationship_name: "",
connection_type: "simple-key",
from_column_name: "",
to_column_name: "",
});
onCancel();
};
@@ -525,10 +526,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 연결 종류별 설정 패널 렌더링
const renderConnectionTypeSettings = () => {
console.log("🔍 [ConnectionSetupModal] renderConnectionTypeSettings - connectionType:", config.connectionType);
console.log("🔍 [ConnectionSetupModal] renderConnectionTypeSettings - connectionType:", config.connection_type);
console.log("🔍 [ConnectionSetupModal] externalCallConfig:", externalCallConfig);
switch (config.connectionType) {
switch (config.connection_type) {
case "simple-key":
return (
<SimpleKeySettingsComponent
@@ -564,8 +565,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
console.log("🚀 [ConnectionSetupModal] Rendering ExternalCallPanel");
return (
<ExternalCallPanel
relationshipId={connection?.id || `temp-${Date.now()}`}
initialSettings={externalCallConfig}
relationship_id={connection?.from_node?.table_name ? `${connection.from_node.table_name}-${connection.to_node?.table_name}` : `temp-${Date.now()}`}
initial_settings={externalCallConfig}
onSettingsChange={setExternalCallConfig}
/>
);
@@ -577,11 +578,11 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const isButtonDisabled = () => {
// 공통 검증: 관계 이름은 필수
const hasRelationshipName = !!config.relationshipName?.trim();
const hasRelationshipName = !!config.relationship_name?.trim();
if (!hasRelationshipName) return true;
// 연결 타입별 검증
switch (config.connectionType) {
switch (config.connection_type) {
case "simple-key":
// 단순 키값 연결: From과 To 컬럼이 모두 선택되어야 함
const hasFromColumns = selectedFromColumns.length > 0;
@@ -594,32 +595,32 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// DELETE 액션은 필드 매핑이 필요 없음
const allActionsHaveMappings = dataSaveSettings.actions.every((action) => {
if (action.actionType === "delete") {
if (action.action_type === "delete") {
return true; // DELETE는 필드 매핑 불필요
}
return action.fieldMappings.length > 0;
return action.field_mappings.length > 0;
});
const allMappingsComplete = dataSaveSettings.actions.every((action) => {
if (action.actionType === "delete") {
if (action.action_type === "delete") {
return true; // DELETE는 필드 매핑 검증 생략
}
// INSERT 액션의 경우 최소 하나의 매핑이 있으면 됨 (모든 컬럼 매핑 필수 조건 제거)
if (action.actionType === "insert") {
if (action.action_type === "insert") {
return true; // 필드 매핑이 있으면 충분함
}
return action.fieldMappings.every((mapping) => {
return action.field_mappings.every((mapping) => {
// 타겟은 항상 필요
if (!mapping.targetTable || !mapping.targetField) return false;
if (!mapping.target_table || !mapping.target_field) return false;
// 소스와 기본값 중 하나는 있어야 함
const hasSource = mapping.sourceTable && mapping.sourceField;
const hasDefault = mapping.defaultValue && mapping.defaultValue.trim();
const hasSource = mapping.source_table && mapping.source_field;
const hasDefault = mapping.default_value && mapping.default_value.trim();
// FROM 테이블이 비어있으면 기본값이 필요
if (!mapping.sourceTable) {
if (!mapping.source_table) {
return !!hasDefault;
}
@@ -630,7 +631,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증
const allRequiredConditionsMet = dataSaveSettings.actions.every((action) => {
if (action.actionType === "insert") {
if (action.action_type === "insert") {
return true; // INSERT는 조건 불필요
}
@@ -659,9 +660,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
case "external-call":
// 외부 호출: 새로운 설정이 있으면 API URL 검증, 없으면 기존 설정 검증
if (externalCallConfig) {
return !externalCallConfig.restApiSettings?.apiUrl?.trim();
return !externalCallConfig.rest_api_settings?.apiUrl?.trim();
} else {
return !externalCallSettings.configId || !externalCallSettings.message?.trim();
return !externalCallSettings.config_id || !externalCallSettings.message?.trim();
}
default:
@@ -689,7 +690,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<Label htmlFor="relationshipName"> </Label>
<Input
id="relationshipName"
value={config.relationshipName}
value={config.relationship_name}
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
placeholder="employee_id_department_id_연결"
className="text-sm"
@@ -701,11 +702,11 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<ConnectionTypeSelector config={config} onConfigChange={setConfig} />
{/* 조건부 연결을 위한 조건 설정 */}
{isConditionalConnection(config.connectionType) && (
{isConditionalConnection(config.connection_type) && (
<ConditionalSettings
conditions={conditions}
fromTableColumns={fromTableColumns}
fromTableName={selectedFromTable || connection.fromNode.tableName}
fromTableName={selectedFromTable || connection.from_node.table_name}
onAddCondition={addCondition}
onAddGroupStart={addGroupStart}
onAddGroupEnd={addGroupEnd}
+2 -2
View File
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { EdgeProps, getBezierPath, EdgeLabelRenderer, BaseEdge } from "@xyflow/react";
import { EdgeProps, Edge, getBezierPath, EdgeLabelRenderer, BaseEdge } from "@xyflow/react";
interface CustomEdgeData {
relationshipType: string;
@@ -9,7 +9,7 @@ interface CustomEdgeData {
label?: string;
}
export const CustomEdge: React.FC<EdgeProps<CustomEdgeData>> = ({
export const CustomEdge: React.FC<EdgeProps<Edge<CustomEdgeData>>> = ({
id,
sourceX,
sourceY,
@@ -33,8 +33,8 @@ const nodeTypes = {
const edgeTypes = {};
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
companyCode: propCompanyCode = "*",
diagramId,
company_code: propCompanyCode = "*",
diagram_id: diagramId,
}) => {
const { user: authUser } = useAuth();
@@ -113,6 +113,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
fromColumns: Array.isArray(rel.fromColumns) ? rel.fromColumns : [],
toColumns: Array.isArray(rel.toColumns) ? rel.toColumns : [],
connectionType: rel.connectionType || "simple-key",
connection_type: (rel.connectionType || "simple-key") as "simple-key" | "data-save" | "external-call",
relationshipName: rel.relationshipName || "",
note: rel.note || "", // 🔥 연결 설명 로드
}));
@@ -140,8 +141,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
},
data: {
table: {
tableName,
displayName: tableName,
table_name: tableName,
display_name: tableName,
description: "",
columns: Array.isArray(columns)
? columns.map((col) => ({
@@ -152,8 +153,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
: [],
},
onColumnClick: handleColumnClick,
selectedColumns: [],
connectedColumns: {},
selected_columns: [],
connected_columns: {},
},
selected: false,
};
@@ -168,14 +169,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
},
data: {
table: {
tableName,
displayName: tableName,
table_name: tableName,
display_name: tableName,
description: "",
columns: [],
},
onColumnClick: handleColumnClick,
selectedColumns: [],
connectedColumns: {},
selected_columns: [],
connected_columns: {},
},
selected: false,
};
@@ -279,24 +280,20 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
position: { x: Math.random() * 300, y: Math.random() * 200 },
data: {
table: {
tableName: table.tableName,
displayName: table.displayName || table.tableName,
table_name: table.tableName,
display_name: table.displayName || table.tableName,
description: "", // 새로 추가된 노드는 description 없이 통일
columns: Array.isArray(table.columns)
? table.columns.map((col) => ({
columnName: col.columnName || "unknown",
name: col.columnName || "unknown", // 호환성을 위해 유지
displayName: col.displayName, // 한국어 라벨
columnLabel: col.columnLabel, // 한국어 라벨
name: col.columnName || "unknown",
type: col.dataType || "varchar",
dataType: col.dataType || "varchar",
description: col.description || "",
}))
: [],
},
onColumnClick: handleColumnClick,
selectedColumns: selectedColumns[table.tableName] || [],
connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음
selected_columns: selectedColumns[table.tableName] || [],
connected_columns: {}, // 새로 추가된 노드는 연결 정보 없음
},
};
@@ -470,15 +467,15 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
if (!firstNode || !secondNode) return;
setPendingConnection({
fromNode: {
from_node: {
id: firstNode.id,
tableName: firstNode.data.table.tableName,
displayName: firstNode.data.table.displayName,
table_name: firstNode.data.table.table_name,
display_name: firstNode.data.table.display_name,
},
toNode: {
to_node: {
id: secondNode.id,
tableName: secondNode.data.table.tableName,
displayName: secondNode.data.table.displayName,
table_name: secondNode.data.table.table_name,
display_name: secondNode.data.table.display_name,
},
});
};
@@ -497,6 +494,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
fromColumns: relationshipData.from_column_name ? relationshipData.from_column_name.split(",") : [],
toColumns: relationshipData.to_column_name ? relationshipData.to_column_name.split(",") : [],
connectionType: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
connection_type: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
relationshipName: relationshipData.relationship_name,
note: (relationshipData.settings as any)?.notes || "", // 🔥 notes를 note로 변환
settings: relationshipData.settings || {},
@@ -537,6 +535,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
fromColumns: relationshipData.from_column_name ? relationshipData.from_column_name.split(",") : [],
toColumns: relationshipData.to_column_name ? relationshipData.to_column_name.split(",") : [],
connectionType: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
connection_type: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
relationshipName: relationshipData.relationship_name,
note: (relationshipData.settings as any)?.notes || "", // 🔥 notes를 note로 변환
settings: relationshipData.settings || {},
@@ -650,8 +649,8 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
.filter((rel) => rel.settings?.control)
.map((rel) => ({
id: rel.id,
triggerType: rel.settings?.control?.triggerType || "insert",
conditions: (rel.settings?.control?.conditionTree || []).map((condition: Record<string, unknown>) => ({
triggerType: rel.settings?.control?.trigger_type || "insert",
conditions: (rel.settings?.control?.condition_tree || []).map((condition: Record<string, unknown>) => ({
...condition,
logicalOperator:
condition.logicalOperator === "AND" || condition.logicalOperator === "OR"
@@ -909,12 +908,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<ConnectionSetupModal
key={
pendingConnection
? `${pendingConnection.fromNode?.tableName || "unknown"}-${pendingConnection.toNode?.tableName || "unknown"}`
? `${pendingConnection.from_node?.table_name || "unknown"}-${pendingConnection.to_node?.table_name || "unknown"}`
: "connection-modal"
}
isOpen={!!pendingConnection}
connection={pendingConnection}
companyCode={companyCode}
company_code={companyCode}
onConfirm={handleConfirmConnection}
onCancel={handleCancelConnection}
/>
@@ -933,41 +932,41 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
onEdit={() => {
if (selectedEdgeInfo) {
// 기존 관계 찾기
const existingRelationship = tempRelationships.find((rel) => rel.id === selectedEdgeInfo.relationshipId);
const existingRelationship = tempRelationships.find((rel) => rel.id === selectedEdgeInfo.relationship_id);
if (existingRelationship) {
// 편집 모드로 설정
setEditingRelationshipId(selectedEdgeInfo.relationshipId);
setEditingRelationshipId(selectedEdgeInfo.relationship_id);
// 연결 설정 모달 열기
const fromTable = nodes.find((node) => node.data?.table?.tableName === selectedEdgeInfo.fromTable);
const toTable = nodes.find((node) => node.data?.table?.tableName === selectedEdgeInfo.toTable);
const fromTable = nodes.find((node) => node.data?.table?.table_name === selectedEdgeInfo.from_table);
const toTable = nodes.find((node) => node.data?.table?.table_name === selectedEdgeInfo.to_table);
if (fromTable && toTable) {
setPendingConnection({
fromNode: {
from_node: {
id: fromTable.id,
tableName: selectedEdgeInfo.fromTable,
displayName: fromTable.data?.table?.displayName || selectedEdgeInfo.fromTable,
table_name: selectedEdgeInfo.from_table,
display_name: fromTable.data?.table?.display_name || selectedEdgeInfo.from_table,
},
toNode: {
to_node: {
id: toTable.id,
tableName: selectedEdgeInfo.toTable,
displayName: toTable.data?.table?.displayName || selectedEdgeInfo.toTable,
table_name: selectedEdgeInfo.to_table,
display_name: toTable.data?.table?.display_name || selectedEdgeInfo.to_table,
},
selectedColumnsData: {
[selectedEdgeInfo.fromTable]: {
displayName: fromTable.data?.table?.displayName || selectedEdgeInfo.fromTable,
columns: selectedEdgeInfo.fromColumns || [],
selected_columns_data: {
[selectedEdgeInfo.from_table]: {
display_name: fromTable.data?.table?.display_name || selectedEdgeInfo.from_table,
columns: selectedEdgeInfo.from_columns || [],
},
[selectedEdgeInfo.toTable]: {
displayName: toTable.data?.table?.displayName || selectedEdgeInfo.toTable,
columns: selectedEdgeInfo.toColumns || [],
[selectedEdgeInfo.to_table]: {
display_name: toTable.data?.table?.display_name || selectedEdgeInfo.to_table,
columns: selectedEdgeInfo.to_columns || [],
},
},
existingRelationship: {
relationshipName: existingRelationship.relationshipName,
connectionType: existingRelationship.connectionType,
existing_relationship: {
relationship_name: existingRelationship.relationshipName,
connection_type: existingRelationship.connectionType,
settings: existingRelationship.settings,
},
});
@@ -983,10 +982,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
onDelete={() => {
if (selectedEdgeInfo) {
// 관계 삭제
setTempRelationships((prev) => prev.filter((rel) => rel.id !== selectedEdgeInfo.relationshipId));
setTempRelationships((prev) => prev.filter((rel) => rel.id !== selectedEdgeInfo.relationship_id));
// 엣지 삭제
setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== selectedEdgeInfo.relationshipId));
setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== selectedEdgeInfo.relationship_id));
// 변경사항 표시
setHasUnsavedChanges(true);
@@ -7,7 +7,7 @@ import { ExtendedJsonRelationship } from "@/types/dataflowTypes";
interface DataFlowSidebarProps {
companyCode: string;
nodes: Array<{ id: string; data: { table: { tableName: string } } }>;
nodes: Array<{ id: string; data: { table: { table_name: string } } }>;
edges: Array<{ id: string }>;
tempRelationships: ExtendedJsonRelationship[];
hasUnsavedChanges: boolean;
@@ -39,7 +39,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
<span className="text-sm text-white">🔗</span>
</div>
<div>
<div className="text-sm font-bold text-white">{edgeInfo.relationshipName}</div>
<div className="text-sm font-bold text-white">{edgeInfo.relationship_name}</div>
<div className="text-xs text-blue-100"> </div>
</div>
</div>
@@ -57,7 +57,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
<div className="text-center">
<div className="text-xs font-medium tracking-wide text-muted-foreground uppercase"> </div>
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
{edgeInfo.connectionType}
{edgeInfo.connection_type}
</div>
</div>
</div>
@@ -68,10 +68,10 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
{/* From 테이블 */}
<div className="rounded-lg border-l-4 border-emerald-400 bg-emerald-50 p-3">
<div className="mb-2 text-xs font-bold tracking-wide text-emerald-700 uppercase">FROM</div>
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.fromTable}</div>
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.from_table}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{edgeInfo.fromColumns.map((column, index) => (
{edgeInfo.from_columns.map((column, index) => (
<span
key={index}
className="inline-flex items-center rounded-md bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 ring-1 ring-emerald-200"
@@ -91,10 +91,10 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
{/* To 테이블 */}
<div className="rounded-lg border-l-4 border-primary/60 bg-accent p-3">
<div className="mb-2 text-xs font-bold tracking-wide text-primary uppercase">TO</div>
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.toTable}</div>
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.to_table}</div>
<div className="space-y-1">
<div className="flex flex-wrap gap-2">
{edgeInfo.toColumns.map((column, index) => (
{edgeInfo.to_columns.map((column, index) => (
<span
key={index}
className="inline-flex items-center rounded-md bg-primary/20 px-2.5 py-0.5 text-xs font-medium text-primary ring-1 ring-primary/20"
@@ -80,28 +80,28 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
}
// 연결 설정 모달 열기
const fromTable = nodes.find((node) => node.data?.table?.tableName === relationship.fromTable);
const toTable = nodes.find((node) => node.data?.table?.tableName === relationship.toTable);
const fromTable = nodes.find((node) => node.data?.table?.table_name === relationship.fromTable);
const toTable = nodes.find((node) => node.data?.table?.table_name === relationship.toTable);
if (fromTable && toTable) {
onSetPendingConnection({
fromNode: {
id: fromTable.id,
tableName: relationship.fromTable,
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
displayName: fromTable.data?.table?.display_name || relationship.fromTable,
},
toNode: {
id: toTable.id,
tableName: relationship.toTable,
displayName: toTable.data?.table?.displayName || relationship.toTable,
displayName: toTable.data?.table?.display_name || relationship.toTable,
},
selectedColumnsData: {
[relationship.fromTable]: {
displayName: fromTable.data?.table?.displayName || relationship.fromTable,
displayName: fromTable.data?.table?.display_name || relationship.fromTable,
columns: relationship.fromColumns || [],
},
[relationship.toTable]: {
displayName: toTable.data?.table?.displayName || relationship.toTable,
displayName: toTable.data?.table?.display_name || relationship.toTable,
columns: relationship.toColumns || [],
},
},
@@ -12,6 +12,7 @@ import {
import {
AlertDialog,
AlertDialogAction,
AlertDialogFooter,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
@@ -57,7 +57,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
const node = nodes.find((n) => n.id === nodeId);
if (!node) return null;
const { tableName, displayName } = node.data.table;
const { table_name, display_name } = node.data.table;
return (
<div key={`selected-${nodeId}-${index}`}>
{/* 테이블 정보 */}
@@ -76,7 +76,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
index === 0 ? "text-success" : index === 1 ? "text-primary" : "text-foreground"
}`}
>
{displayName}
{display_name}
</div>
{selectedNodes.length === 2 && (
<div
@@ -88,7 +88,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
</div>
)}
</div>
<div className="text-xs text-muted-foreground">{tableName}</div>
<div className="text-xs text-muted-foreground">{table_name}</div>
</div>
{/* 연결 화살표 (마지막이 아닌 경우) */}
@@ -33,7 +33,7 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
const { addActionGroupStart, addActionGroupEnd, getActionCurrentGroupLevel } = useActionConditionHelpers();
// INSERT가 아닌 액션 타입인지 확인
const isConditionRequired = action.actionType !== "insert";
const isConditionRequired = action.action_type !== "insert";
// 유효한 조건이 있는지 확인 (group-start, group-end만 있는 경우 제외)
const hasValidConditions =
@@ -161,7 +161,7 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
<div>
<div className="font-medium"> </div>
<div className="mt-1">
{action.actionType.toUpperCase()} .
{action.action_type.toUpperCase()} .
<br />
<strong>, , </strong> .
<br />
@@ -44,10 +44,10 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
enableMultiConnection = false,
}) => {
// 🆕 다중 커넥션 상태 관리
const [fromConnectionId, setFromConnectionId] = useState<number | undefined>(action.fromConnection?.connectionId);
const [toConnectionId, setToConnectionId] = useState<number | undefined>(action.toConnection?.connectionId);
const [selectedFromTable, setSelectedFromTable] = useState<string | undefined>(action.fromTable || fromTableName);
const [selectedToTable, setSelectedToTable] = useState<string | undefined>(action.targetTable || toTableName);
const [fromConnectionId, setFromConnectionId] = useState<number | undefined>((action as any).fromConnection?.connectionId);
const [toConnectionId, setToConnectionId] = useState<number | undefined>((action as any).toConnection?.connectionId);
const [selectedFromTable, setSelectedFromTable] = useState<string | undefined>((action as any).fromTable || fromTableName);
const [selectedToTable, setSelectedToTable] = useState<string | undefined>((action as any).targetTable || toTableName);
// 다중 커넥션이 활성화된 경우 새로운 UI 렌더링
if (enableMultiConnection) {
@@ -55,7 +55,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
}
// 기존 INSERT 액션 처리 (단일 커넥션)
if (action.actionType === "insert" && fromTableColumns.length > 0 && toTableColumns.length > 0) {
if (action.action_type === "insert" && fromTableColumns.length > 0 && toTableColumns.length > 0) {
return (
<InsertFieldMappingPanel
action={action}
@@ -96,23 +96,23 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
const handleToTableChange = (tableName: string) => {
setSelectedToTable(tableName);
updateActionTable("targetTable", tableName);
updateActionTable("target_table", tableName);
};
// 액션 커넥션 정보 업데이트
const updateActionConnection = (type: "fromConnection" | "toConnection", connectionId: number) => {
const newActions = [...settings.actions];
if (!newActions[actionIndex][type]) {
newActions[actionIndex][type] = {};
if (!(newActions[actionIndex] as any)[type]) {
(newActions[actionIndex] as any)[type] = {};
}
newActions[actionIndex][type]!.connectionId = connectionId;
(newActions[actionIndex] as any)[type]!.connectionId = connectionId;
onSettingsChange({ ...settings, actions: newActions });
};
// 액션 테이블 정보 업데이트
const updateActionTable = (type: "fromTable" | "targetTable", tableName: string) => {
const updateActionTable = (type: "fromTable" | "target_table", tableName: string) => {
const newActions = [...settings.actions];
newActions[actionIndex][type] = tableName;
(newActions[actionIndex] as any)[type] = tableName;
onSettingsChange({ ...settings, actions: newActions });
};
@@ -124,7 +124,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
toConnectionId={toConnectionId}
onFromConnectionChange={handleFromConnectionChange}
onToConnectionChange={handleToConnectionChange}
actionType={action.actionType}
actionType={action.action_type}
allowSameConnection={true}
/>
@@ -137,7 +137,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
selectedToTable={selectedToTable}
onFromTableChange={handleFromTableChange}
onToTableChange={handleToTableChange}
actionType={action.actionType}
actionType={action.action_type}
allowSameTable={true}
showSameTableWarning={true}
/>
@@ -151,7 +151,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
// 액션 타입별 패널 렌더링
function renderActionSpecificPanel() {
switch (action.actionType) {
switch (action.action_type) {
case "insert":
return (
<InsertFieldMappingPanel
@@ -200,26 +200,25 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
}
const addFieldMapping = () => {
const newActions = [...settings.actions];
newActions[actionIndex].fieldMappings.push({
sourceTable: "",
sourceField: "",
targetTable: "",
targetField: "",
defaultValue: "",
transformFunction: "",
newActions[actionIndex].field_mappings.push({
source_table: "",
source_field: "",
target_table: "",
target_field: "",
default_value: "",
});
onSettingsChange({ ...settings, actions: newActions });
};
const updateFieldMapping = (mappingIndex: number, field: string, value: string) => {
const newActions = [...settings.actions];
(newActions[actionIndex].fieldMappings[mappingIndex] as any)[field] = value;
(newActions[actionIndex].field_mappings[mappingIndex] as any)[field] = value;
onSettingsChange({ ...settings, actions: newActions });
};
const removeFieldMapping = (mappingIndex: number) => {
const newActions = [...settings.actions];
newActions[actionIndex].fieldMappings = newActions[actionIndex].fieldMappings.filter((_, i) => i !== mappingIndex);
newActions[actionIndex].field_mappings = newActions[actionIndex].field_mappings.filter((_: unknown, i: number) => i !== mappingIndex);
onSettingsChange({ ...settings, actions: newActions });
};
@@ -236,9 +235,9 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
</Button>
</div>
<div className="space-y-3">
{action.fieldMappings.map((mapping, mappingIndex) => (
{action.field_mappings.map((mapping, mappingIndex) => (
<div
key={`${action.id}-mapping-${mappingIndex}-${mapping.sourceField || "empty"}-${mapping.targetField || "empty"}`}
key={`${action.id}-mapping-${mappingIndex}-${mapping.source_field || "empty"}-${mapping.target_field || "empty"}`}
className="rounded border bg-white p-2"
>
{/* 컴팩트한 매핑 표시 */}
@@ -246,16 +245,16 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
{/* 소스 */}
<div className="flex items-center gap-1 rounded bg-accent px-2 py-1">
<Select
value={mapping.sourceTable || "__EMPTY__"}
value={mapping.source_table || "__EMPTY__"}
onValueChange={(value) => {
const actualValue = value === "__EMPTY__" ? "" : value;
updateFieldMapping(mappingIndex, "sourceTable", actualValue);
updateFieldMapping(mappingIndex, "sourceField", "");
updateFieldMapping(mappingIndex, "source_table", actualValue);
updateFieldMapping(mappingIndex, "source_field", "");
if (actualValue) {
updateFieldMapping(mappingIndex, "defaultValue", "");
updateFieldMapping(mappingIndex, "default_value", "");
}
}}
disabled={!!(mapping.defaultValue && mapping.defaultValue.trim())}
disabled={!!(mapping.default_value && mapping.default_value.trim())}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
<SelectValue placeholder="테이블" />
@@ -271,11 +270,11 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
))}
</SelectContent>
</Select>
{mapping.sourceTable && (
{mapping.source_table && (
<button
onClick={() => {
updateFieldMapping(mappingIndex, "sourceTable", "");
updateFieldMapping(mappingIndex, "sourceField", "");
updateFieldMapping(mappingIndex, "source_table", "");
updateFieldMapping(mappingIndex, "source_field", "");
}}
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground/70 hover:bg-muted/80 hover:text-muted-foreground"
title="소스 테이블 지우기"
@@ -285,21 +284,21 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
)}
<span className="text-muted-foreground/70">.</span>
<Select
value={mapping.sourceField}
value={mapping.source_field}
onValueChange={(value) => {
updateFieldMapping(mappingIndex, "sourceField", value);
updateFieldMapping(mappingIndex, "source_field", value);
if (value) {
updateFieldMapping(mappingIndex, "defaultValue", "");
updateFieldMapping(mappingIndex, "default_value", "");
}
}}
disabled={!mapping.sourceTable || !!(mapping.defaultValue && mapping.defaultValue.trim())}
disabled={!mapping.source_table || !!(mapping.default_value && mapping.default_value.trim())}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
<SelectValue placeholder="컬럼" />
</SelectTrigger>
<SelectContent>
{mapping.sourceTable &&
tableColumnsCache[mapping.sourceTable]?.map((column) => (
{mapping.source_table &&
tableColumnsCache[mapping.source_table]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
<div className="truncate" title={column.columnName}>
{column.displayName && column.displayName !== column.columnName
@@ -317,10 +316,10 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
{/* 타겟 */}
<div className="flex items-center gap-1 rounded bg-emerald-50 px-2 py-1">
<Select
value={mapping.targetTable || ""}
value={mapping.target_table || ""}
onValueChange={(value) => {
updateFieldMapping(mappingIndex, "targetTable", value);
updateFieldMapping(mappingIndex, "targetField", "");
updateFieldMapping(mappingIndex, "target_table", value);
updateFieldMapping(mappingIndex, "target_field", "");
}}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
@@ -338,16 +337,16 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
</Select>
<span className="text-muted-foreground/70">.</span>
<Select
value={mapping.targetField}
onValueChange={(value) => updateFieldMapping(mappingIndex, "targetField", value)}
disabled={!mapping.targetTable}
value={mapping.target_field}
onValueChange={(value) => updateFieldMapping(mappingIndex, "target_field", value)}
disabled={!mapping.target_table}
>
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
<SelectValue placeholder="컬럼" />
</SelectTrigger>
<SelectContent>
{mapping.targetTable &&
tableColumnsCache[mapping.targetTable]?.map((column) => (
{mapping.target_table &&
tableColumnsCache[mapping.target_table]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
<div className="truncate" title={column.columnName}>
{column.displayName && column.displayName !== column.columnName
@@ -362,15 +361,15 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
{/* 기본값 (인라인) */}
<Input
value={mapping.defaultValue || ""}
value={mapping.default_value || ""}
onChange={(e) => {
updateFieldMapping(mappingIndex, "defaultValue", e.target.value);
updateFieldMapping(mappingIndex, "default_value", e.target.value);
if (e.target.value.trim()) {
updateFieldMapping(mappingIndex, "sourceTable", "");
updateFieldMapping(mappingIndex, "sourceField", "");
updateFieldMapping(mappingIndex, "source_table", "");
updateFieldMapping(mappingIndex, "source_field", "");
}
}}
disabled={!!mapping.sourceTable}
disabled={!!mapping.source_table}
className="h-6 w-20 text-xs"
placeholder="기본값"
/>
@@ -389,14 +388,14 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
))}
{/* 필드 매핑이 없을 때 안내 메시지 */}
{action.fieldMappings.length === 0 && (
{action.field_mappings.length === 0 && (
<div className="rounded border border-destructive/20 bg-destructive/10 p-3 text-xs text-destructive">
<div className="flex items-start gap-2">
<span className="text-destructive"></span>
<div>
<div className="font-medium"> </div>
<div className="mt-1">
{action.actionType.toUpperCase()}
{action.action_type.toUpperCase()}
.
</div>
</div>
@@ -28,23 +28,23 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
}) => {
const updateSplitConfig = (field: string, value: string) => {
const newActions = [...settings.actions];
if (!newActions[actionIndex].splitConfig) {
newActions[actionIndex].splitConfig = {
sourceField: "",
if (!newActions[actionIndex].split_config) {
newActions[actionIndex].split_config = {
source_field: "",
delimiter: ",",
targetField: "",
target_field: "",
};
}
(newActions[actionIndex].splitConfig as any)[field] = value;
(newActions[actionIndex].split_config as any)[field] = value;
onSettingsChange({ ...settings, actions: newActions });
};
const clearSplitConfig = () => {
const newActions = [...settings.actions];
newActions[actionIndex].splitConfig = {
sourceField: "",
newActions[actionIndex].split_config = {
source_field: "",
delimiter: ",",
targetField: "",
target_field: "",
};
onSettingsChange({ ...settings, actions: newActions });
};
@@ -55,11 +55,11 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<summary className="flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium text-foreground hover:bg-muted hover:text-foreground">
<div className="flex items-center gap-2">
()
{action.splitConfig && action.splitConfig.sourceField && (
{action.split_config && action.split_config.source_field && (
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-xs text-emerald-700"></span>
)}
</div>
{action.splitConfig && action.splitConfig.sourceField && (
{action.split_config && action.split_config.source_field && (
<Button
size="sm"
variant="ghost"
@@ -81,8 +81,8 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<div>
<Label className="text-xs text-muted-foreground"> </Label>
<Select
value={action.splitConfig?.sourceField || ""}
onValueChange={(value) => updateSplitConfig("sourceField", value)}
value={action.split_config?.source_field || ""}
onValueChange={(value) => updateSplitConfig("source_field", value)}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="필드 선택" />
@@ -101,7 +101,7 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<div>
<Label className="text-xs text-muted-foreground"></Label>
<Input
value={action.splitConfig?.delimiter || ""}
value={action.split_config?.delimiter || ""}
onChange={(e) => updateSplitConfig("delimiter", e.target.value)}
className="h-6 text-xs"
placeholder=","
@@ -110,8 +110,8 @@ export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
<div>
<Label className="text-xs text-muted-foreground"> </Label>
<Select
value={action.splitConfig?.targetField || ""}
onValueChange={(value) => updateSplitConfig("targetField", value)}
value={action.split_config?.target_field || ""}
onValueChange={(value) => updateSplitConfig("target_field", value)}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="필드 선택" />
@@ -17,11 +17,11 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
<div className="mt-2 grid grid-cols-3 gap-2">
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "simple-key"
config.connection_type === "simple-key"
? "border-primary bg-accent"
: "border-border hover:border-input"
}`}
onClick={() => onConfigChange({ ...config, connectionType: "simple-key" })}
onClick={() => onConfigChange({ ...config, connection_type: "simple-key" })}
>
<Key className="mx-auto h-6 w-6 text-primary" />
<div className="mt-1 text-xs font-medium"> </div>
@@ -30,11 +30,11 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "data-save"
config.connection_type === "data-save"
? "border-emerald-500 bg-emerald-50"
: "border-border hover:border-input"
}`}
onClick={() => onConfigChange({ ...config, connectionType: "data-save" })}
onClick={() => onConfigChange({ ...config, connection_type: "data-save" })}
>
<Save className="mx-auto h-6 w-6 text-emerald-500" />
<div className="mt-1 text-xs font-medium"> </div>
@@ -43,13 +43,13 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "external-call"
config.connection_type === "external-call"
? "border-orange-500 bg-amber-50"
: "border-border hover:border-input"
}`}
onClick={() => {
console.log("🔄 [ConnectionTypeSelector] External call selected");
onConfigChange({ ...config, connectionType: "external-call" });
onConfigChange({ ...config, connection_type: "external-call" });
}}
>
<Globe className="mx-auto h-6 w-6 text-amber-500" />
@@ -35,11 +35,6 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
}) => {
// 🎨 항상 새로운 UI 사용
return (
<DataConnectionDesigner
onClose={undefined} // 닫기 버튼 제거 (항상 새 UI 사용)
initialData={{
connectionType: "data_save",
}}
/>
<DataConnectionDesigner />
);
};
@@ -22,42 +22,42 @@ const handleTestExternalCall = async (settings: ExternalCallSettingsType) => {
try {
// 설정을 백엔드 형식으로 변환
const backendSettings: Record<string, unknown> = {
callType: settings.callType,
callType: settings.call_type,
timeout: 10000, // 10초 타임아웃 설정
};
if (settings.callType === "rest-api") {
backendSettings.apiType = settings.apiType;
if (settings.call_type === "rest-api") {
backendSettings.apiType = settings.api_type;
switch (settings.apiType) {
switch (settings.api_type) {
case "slack":
backendSettings.webhookUrl = settings.slackWebhookUrl;
backendSettings.webhookUrl = settings.slack_webhook_url;
backendSettings.message =
settings.slackMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.channel = settings.slackChannel;
settings.slack_message || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.channel = settings.slack_channel;
break;
case "kakao-talk":
backendSettings.accessToken = settings.kakaoAccessToken;
backendSettings.accessToken = settings.kakao_access_token;
backendSettings.message =
settings.kakaoMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
settings.kakao_message || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
break;
case "discord":
backendSettings.webhookUrl = settings.discordWebhookUrl;
backendSettings.webhookUrl = settings.discord_webhook_url;
backendSettings.message =
settings.discordMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.username = settings.discordUsername;
settings.discord_message || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
backendSettings.username = settings.discord_username;
break;
case "generic":
default:
backendSettings.url = settings.apiUrl;
backendSettings.method = settings.httpMethod || "POST";
backendSettings.url = settings.api_url;
backendSettings.method = settings.http_method || "POST";
try {
backendSettings.headers = settings.headers ? JSON.parse(settings.headers) : {};
} catch (error) {
console.warn("Headers JSON 파싱 실패, 기본값 사용:", error);
backendSettings.headers = {};
}
backendSettings.body = settings.bodyTemplate || "{}";
backendSettings.body = settings.body_template || "{}";
break;
}
}
@@ -145,9 +145,9 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Select
value={settings.callType}
value={settings.call_type}
onValueChange={(value: "rest-api" | "email" | "ftp" | "queue") =>
onSettingsChange({ ...settings, callType: value })
onSettingsChange({ ...settings, call_type: value })
}
>
<SelectTrigger className="text-sm">
@@ -162,16 +162,16 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Select>
</div>
{settings.callType === "rest-api" && (
{settings.call_type === "rest-api" && (
<>
<div>
<Label htmlFor="apiType" className="text-sm">
API
</Label>
<Select
value={settings.apiType || "generic"}
value={settings.api_type || "generic"}
onValueChange={(value: "slack" | "kakao-talk" | "discord" | "generic") =>
onSettingsChange({ ...settings, apiType: value })
onSettingsChange({ ...settings, api_type: value })
}
>
<SelectTrigger className="text-sm">
@@ -187,7 +187,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</div>
{/* 슬랙 설정 */}
{settings.apiType === "slack" && (
{settings.api_type === "slack" && (
<>
<div>
<Label htmlFor="slackWebhookUrl" className="text-sm">
@@ -195,8 +195,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Input
id="slackWebhookUrl"
value={settings.slackWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, slackWebhookUrl: e.target.value })}
value={settings.slack_webhook_url || ""}
onChange={(e) => onSettingsChange({ ...settings, slack_webhook_url: e.target.value })}
placeholder="https://hooks.slack.com/services/..."
className="text-sm"
/>
@@ -207,8 +207,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Input
id="slackChannel"
value={settings.slackChannel || ""}
onChange={(e) => onSettingsChange({ ...settings, slackChannel: e.target.value })}
value={settings.slack_channel || ""}
onChange={(e) => onSettingsChange({ ...settings, slack_channel: e.target.value })}
placeholder="#general"
className="text-sm"
/>
@@ -219,8 +219,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Textarea
id="slackMessage"
value={settings.slackMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, slackMessage: e.target.value })}
value={settings.slack_message || ""}
onChange={(e) => onSettingsChange({ ...settings, slack_message: e.target.value })}
placeholder="데이터 처리가 완료되었습니다. 총 {{recordCount}}건이 처리되었습니다."
rows={2}
className="text-sm"
@@ -230,7 +230,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
)}
{/* 카카오톡 설정 */}
{settings.apiType === "kakao-talk" && (
{settings.api_type === "kakao-talk" && (
<>
<div>
<Label htmlFor="kakaoAccessToken" className="text-sm">
@@ -239,8 +239,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
<Input
id="kakaoAccessToken"
type="password"
value={settings.kakaoAccessToken || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoAccessToken: e.target.value })}
value={settings.kakao_access_token || ""}
onChange={(e) => onSettingsChange({ ...settings, kakao_access_token: e.target.value })}
placeholder="카카오 API 액세스 토큰을 입력하세요"
className="text-sm"
/>
@@ -251,8 +251,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Textarea
id="kakaoMessage"
value={settings.kakaoMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, kakaoMessage: e.target.value })}
value={settings.kakao_message || ""}
onChange={(e) => onSettingsChange({ ...settings, kakao_message: e.target.value })}
placeholder="데이터 처리 완료! 총 {{recordCount}}건 처리되었습니다."
rows={2}
className="text-sm"
@@ -262,7 +262,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
)}
{/* 디스코드 설정 */}
{settings.apiType === "discord" && (
{settings.api_type === "discord" && (
<>
<div>
<Label htmlFor="discordWebhookUrl" className="text-sm">
@@ -270,8 +270,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Input
id="discordWebhookUrl"
value={settings.discordWebhookUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, discordWebhookUrl: e.target.value })}
value={settings.discord_webhook_url || ""}
onChange={(e) => onSettingsChange({ ...settings, discord_webhook_url: e.target.value })}
placeholder="https://discord.com/api/webhooks/..."
className="text-sm"
/>
@@ -282,8 +282,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Input
id="discordUsername"
value={settings.discordUsername || ""}
onChange={(e) => onSettingsChange({ ...settings, discordUsername: e.target.value })}
value={settings.discord_username || ""}
onChange={(e) => onSettingsChange({ ...settings, discord_username: e.target.value })}
placeholder="ERP 시스템"
className="text-sm"
/>
@@ -294,8 +294,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Textarea
id="discordMessage"
value={settings.discordMessage || ""}
onChange={(e) => onSettingsChange({ ...settings, discordMessage: e.target.value })}
value={settings.discord_message || ""}
onChange={(e) => onSettingsChange({ ...settings, discord_message: e.target.value })}
placeholder="데이터 처리가 완료되었습니다! 🎉"
rows={2}
className="text-sm"
@@ -305,7 +305,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
)}
{/* 일반 API 설정 */}
{settings.apiType === "generic" && (
{settings.api_type === "generic" && (
<>
<div>
<Label htmlFor="apiUrl" className="text-sm">
@@ -313,8 +313,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Input
id="apiUrl"
value={settings.apiUrl || ""}
onChange={(e) => onSettingsChange({ ...settings, apiUrl: e.target.value })}
value={settings.api_url || ""}
onChange={(e) => onSettingsChange({ ...settings, api_url: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
@@ -325,9 +325,9 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
HTTP Method
</Label>
<Select
value={settings.httpMethod}
value={settings.http_method}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
onSettingsChange({ ...settings, httpMethod: value })
onSettingsChange({ ...settings, http_method: value })
}
>
<SelectTrigger className="text-sm">
@@ -361,8 +361,8 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
</Label>
<Textarea
id="bodyTemplate"
value={settings.bodyTemplate}
onChange={(e) => onSettingsChange({ ...settings, bodyTemplate: e.target.value })}
value={settings.body_template}
onChange={(e) => onSettingsChange({ ...settings, body_template: e.target.value })}
placeholder="{}"
rows={2}
className="text-sm"
@@ -90,17 +90,17 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
setMultiToColumns(toCols);
// 테이블 라벨명 설정
const fromTable = fromTables.find((t) => t.tableName === fromTableName);
const toTable = toTables.find((t) => t.tableName === toTableName);
const fromTable = fromTables.find((t) => t.table_name === fromTableName);
const toTable = toTables.find((t) => t.table_name === toTableName);
setFromTableDisplayName(
fromTable?.displayName && fromTable.displayName !== fromTable.tableName
? fromTable.displayName
fromTable?.display_name && fromTable.display_name !== fromTable.table_name
? fromTable.display_name
: fromTableName,
);
setToTableDisplayName(
toTable?.displayName && toTable.displayName !== toTable.tableName ? toTable.displayName : toTableName,
toTable?.display_name && toTable.display_name !== toTable.table_name ? toTable.display_name : toTableName,
);
} catch (error) {
console.error("컬럼 정보 및 테이블 정보 로드 실패:", error);
@@ -151,17 +151,17 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
const columnsToUse = multiToColumns.length > 0 ? multiToColumns : toTableColumns || [];
const mappings: ColumnMapping[] = columnsToUse.map((toCol) => {
const existingMapping = action.fieldMappings.find((mapping) => mapping.targetField === toCol.columnName);
const existingMapping = action.field_mappings.find((mapping) => mapping.target_field === toCol.columnName);
return {
toColumnName: toCol.columnName,
fromColumnName: existingMapping?.sourceField || undefined,
defaultValue: existingMapping?.defaultValue || "",
fromColumnName: existingMapping?.source_field || undefined,
defaultValue: existingMapping?.default_value || "",
};
});
setColumnMappings(mappings);
}, [action.fieldMappings, multiToColumns.length, toTableColumns?.length]);
}, [action.field_mappings, multiToColumns.length, toTableColumns?.length]);
// columnMappings 변경 시 settings 업데이트
const updateSettings = (newMappings: ColumnMapping[]) => {
@@ -171,15 +171,15 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
const fieldMappings = newMappings
.filter((mapping) => mapping.fromColumnName || (mapping.defaultValue && mapping.defaultValue.trim()))
.map((mapping) => ({
sourceTable: mapping.fromColumnName ? fromTableName || "" : "",
sourceField: mapping.fromColumnName || "",
targetTable: toTableName || "",
targetField: mapping.toColumnName,
defaultValue: mapping.defaultValue || "",
transformFunction: "",
source_table: mapping.fromColumnName ? fromTableName || "" : "",
source_field: mapping.fromColumnName || "",
target_table: toTableName || "",
target_field: mapping.toColumnName,
default_value: mapping.defaultValue || "",
transform_function: "",
}));
newActions[actionIndex].fieldMappings = fieldMappings;
newActions[actionIndex].field_mappings = fieldMappings;
onSettingsChange({ ...settings, actions: newActions });
};
@@ -99,7 +99,7 @@ export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
const term = searchTerm.toLowerCase();
return tables.filter(
(table) => table.tableName.toLowerCase().includes(term) || table.displayName?.toLowerCase().includes(term),
(table) => table.table_name.toLowerCase().includes(term) || table.display_name?.toLowerCase().includes(term),
);
};
@@ -167,16 +167,16 @@ export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
// 테이블 아이템 렌더링
const renderTableItem = (table: MultiConnectionTableInfo) => (
<SelectItem key={table.tableName} value={table.tableName}>
<SelectItem key={table.table_name} value={table.table_name}>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<Table className="h-3 w-3" />
<span className="font-medium">
{table.displayName && table.displayName !== table.tableName ? table.displayName : table.tableName}
{table.display_name && table.display_name !== table.table_name ? table.display_name : table.table_name}
</span>
</div>
<Badge variant="outline" className="text-xs">
{table.columnCount}
{table.column_count}
</Badge>
</div>
</SelectItem>
@@ -249,13 +249,13 @@ export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
</Select>
{/* 테이블 정보 */}
{selectedFromTable && fromTables.find((t) => t.tableName === selectedFromTable) && (
{selectedFromTable && fromTables.find((t) => t.table_name === selectedFromTable) && (
<div className="text-muted-foreground text-xs">
<div className="flex items-center gap-1">
<Info className="h-3 w-3" />
<span>
{fromTables.find((t) => t.tableName === selectedFromTable)?.columnCount} , :{" "}
{fromTables.find((t) => t.tableName === selectedFromTable)?.connectionName}
{fromTables.find((t) => t.table_name === selectedFromTable)?.column_count} , :{" "}
{fromTables.find((t) => t.table_name === selectedFromTable)?.connection_name}
</span>
</div>
</div>
@@ -310,13 +310,13 @@ export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
</Select>
{/* 테이블 정보 */}
{selectedToTable && toTables.find((t) => t.tableName === selectedToTable) && (
{selectedToTable && toTables.find((t) => t.table_name === selectedToTable) && (
<div className="text-muted-foreground text-xs">
<div className="flex items-center gap-1">
<Info className="h-3 w-3" />
<span>
{toTables.find((t) => t.tableName === selectedToTable)?.columnCount} , :{" "}
{toTables.find((t) => t.tableName === selectedToTable)?.connectionName}
{toTables.find((t) => t.table_name === selectedToTable)?.column_count} , :{" "}
{toTables.find((t) => t.table_name === selectedToTable)?.connection_name}
</span>
</div>
</div>
@@ -341,18 +341,18 @@ export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
<span> :</span>
<Badge variant="secondary" className="font-mono">
{(() => {
const fromTable = fromTables.find((t) => t.tableName === selectedFromTable);
return fromTable?.displayName && fromTable.displayName !== fromTable.tableName
? fromTable.displayName
const fromTable = fromTables.find((t) => t.table_name === selectedFromTable);
return fromTable?.display_name && fromTable.display_name !== fromTable.table_name
? fromTable.display_name
: selectedFromTable;
})()}
</Badge>
<span></span>
<Badge variant="secondary" className="font-mono">
{(() => {
const toTable = toTables.find((t) => t.tableName === selectedToTable);
return toTable?.displayName && toTable.displayName !== toTable.tableName
? toTable.displayName
const toTable = toTables.find((t) => t.table_name === selectedToTable);
return toTable?.display_name && toTable.display_name !== toTable.table_name
? toTable.display_name
: selectedToTable;
})()}
</Badge>
@@ -109,12 +109,12 @@ const RightPanel: React.FC<RightPanelProps> = ({ state, actions }) => {
<div className="flex-1 overflow-hidden px-4">
<div className="h-full max-h-[calc(100vh-400px)] overflow-y-auto">
<ExternalCallPanel
relationshipId={`external-call-${Date.now()}`}
relationship_id={`external-call-${Date.now()}`}
readonly={false}
initialSettings={
initial_settings={
state.externalCallConfig || {
callType: "rest-api",
restApiSettings: {
call_type: "rest-api",
rest_api_settings: {
apiUrl: "",
httpMethod: "POST",
headers: { "Content-Type": "application/json" },
@@ -21,6 +21,9 @@ export interface ColumnInfo {
primaryKey: boolean;
foreignKey?: boolean;
defaultValue?: any;
columnName?: string;
displayName?: string;
webType?: string;
}
export interface TableInfo {
@@ -71,8 +74,34 @@ export interface DataConnectionState {
fieldMappings: FieldMapping[];
mappingStats: MappingStats;
// 액션 설정
actionType?: "insert" | "update" | "delete" | "upsert";
actionConditions: any[];
actionGroups: Array<{ actions: Array<{ fieldMappings?: FieldMapping[] }> }>;
// UI 상태
selectedMapping?: string;
isLoading: boolean;
validationErrors: ValidationError[];
}
export interface DataConnectionActions {
saveMappings: () => Promise<void>;
testExecution: () => Promise<void>;
setConnectionType: (type: "data_save" | "external_call") => void;
updateMapping: (id: string, mapping: Partial<FieldMapping>) => void;
deleteMapping: (id: string) => void;
}
export interface LeftPanelProps {
state: DataConnectionState;
actions: DataConnectionActions;
}
export interface MappingDetailListProps {
mappings: FieldMapping[];
selectedMapping?: string;
onSelectMapping: (id: string) => void;
onUpdateMapping: (id: string, mapping: Partial<FieldMapping>) => void;
onDeleteMapping: (id: string) => void;
}
@@ -63,19 +63,19 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
...localConfig,
direction,
// 방향에 따라 불필요한 매핑 제거
inboundMapping:
inbound_mapping:
direction === "inbound" || direction === "bidirectional"
? localConfig.inboundMapping || {
targetTable: "",
fieldMappings: [],
insertMode: "insert" as const,
? localConfig.inbound_mapping || {
target_table: "",
field_mappings: [],
insert_mode: "insert" as const,
}
: undefined,
outboundMapping:
outbound_mapping:
direction === "outbound" || direction === "bidirectional"
? localConfig.outboundMapping || {
sourceTable: "",
fieldMappings: [],
? localConfig.outbound_mapping || {
source_table: "",
field_mappings: [],
}
: undefined,
};
@@ -90,8 +90,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
(mapping: Partial<InboundMapping>) => {
const newConfig = {
...localConfig,
inboundMapping: {
...localConfig.inboundMapping!,
inbound_mapping: {
...localConfig.inbound_mapping!,
...mapping,
},
};
@@ -106,8 +106,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
(mapping: Partial<OutboundMapping>) => {
const newConfig = {
...localConfig,
outboundMapping: {
...localConfig.outboundMapping!,
outbound_mapping: {
...localConfig.outbound_mapping!,
...mapping,
},
};
@@ -120,7 +120,7 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
// 필드 매핑 업데이트 (Inbound)
const handleInboundFieldMappingsChange = useCallback(
(fieldMappings: FieldMapping[]) => {
handleInboundMappingChange({ fieldMappings });
handleInboundMappingChange({ field_mappings: fieldMappings });
},
[handleInboundMappingChange],
);
@@ -128,7 +128,7 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
// 필드 매핑 업데이트 (Outbound)
const handleOutboundFieldMappingsChange = useCallback(
(fieldMappings: FieldMapping[]) => {
handleOutboundMappingChange({ fieldMappings });
handleOutboundMappingChange({ field_mappings: fieldMappings });
},
[handleOutboundMappingChange],
);
@@ -139,18 +139,18 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
if (
(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") &&
localConfig.inboundMapping
localConfig.inbound_mapping
) {
if (!localConfig.inboundMapping.targetTable) return false;
if (localConfig.inboundMapping.fieldMappings.length === 0) return false;
if (!localConfig.inbound_mapping.target_table) return false;
if (localConfig.inbound_mapping.field_mappings.length === 0) return false;
}
if (
(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") &&
localConfig.outboundMapping
localConfig.outbound_mapping
) {
if (!localConfig.outboundMapping.sourceTable) return false;
if (localConfig.outboundMapping.fieldMappings.length === 0) return false;
if (!localConfig.outbound_mapping.source_table) return false;
if (localConfig.outbound_mapping.field_mappings.length === 0) return false;
}
return true;
@@ -228,8 +228,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.inboundMapping?.targetTable || ""}
onValueChange={(value) => handleInboundMappingChange({ targetTable: value })}
value={localConfig.inbound_mapping?.target_table || ""}
onValueChange={(value) => handleInboundMappingChange({ target_table: value })}
disabled={readonly || tablesLoading}
>
<SelectTrigger>
@@ -248,7 +248,7 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
) : (
availableTables.map((table) => (
<SelectItem key={table.name} value={table.name}>
{table.displayName || table.name}
{table.display_name || table.name}
</SelectItem>
))
)}
@@ -259,8 +259,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.inboundMapping?.insertMode || "insert"}
onValueChange={(value) => handleInboundMappingChange({ insertMode: value as any })}
value={localConfig.inbound_mapping?.insert_mode || "insert"}
onValueChange={(value) => handleInboundMappingChange({ insert_mode: value as any })}
disabled={readonly}
>
<SelectTrigger>
@@ -278,14 +278,14 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
</div>
{/* 키 필드 설정 (upsert/update 모드일 때) */}
{localConfig.inboundMapping?.insertMode !== "insert" && (
{localConfig.inbound_mapping?.insert_mode !== "insert" && (
<div className="space-y-2">
<Label> </Label>
<Input
value={localConfig.inboundMapping?.keyFields?.join(", ") || ""}
value={localConfig.inbound_mapping?.key_fields?.join(", ") || ""}
onChange={(e) =>
handleInboundMappingChange({
keyFields: e.target.value
key_fields: e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
@@ -301,12 +301,12 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
)}
{/* 필드 매핑 에디터 */}
{localConfig.inboundMapping?.targetTable && (
{localConfig.inbound_mapping?.target_table && (
<FieldMappingEditor
mappings={localConfig.inboundMapping.fieldMappings}
mappings={localConfig.inbound_mapping.field_mappings}
onMappingsChange={handleInboundFieldMappingsChange}
direction="inbound"
targetTable={availableTables.find((t) => t.name === localConfig.inboundMapping?.targetTable)}
targetTable={availableTables.find((t) => t.name === localConfig.inbound_mapping?.target_table)}
readonly={readonly}
/>
)}
@@ -320,8 +320,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<div className="space-y-2">
<Label> </Label>
<Select
value={localConfig.outboundMapping?.sourceTable || ""}
onValueChange={(value) => handleOutboundMappingChange({ sourceTable: value })}
value={localConfig.outbound_mapping?.source_table || ""}
onValueChange={(value) => handleOutboundMappingChange({ source_table: value })}
disabled={readonly}
>
<SelectTrigger>
@@ -330,7 +330,7 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.name} value={table.name}>
{table.displayName || table.name}
{table.display_name || table.name}
</SelectItem>
))}
</SelectContent>
@@ -342,8 +342,8 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<div className="space-y-2">
<Label> ()</Label>
<Textarea
value={localConfig.outboundMapping?.sourceFilter || ""}
onChange={(e) => handleOutboundMappingChange({ sourceFilter: e.target.value })}
value={localConfig.outbound_mapping?.source_filter || ""}
onChange={(e) => handleOutboundMappingChange({ source_filter: e.target.value })}
placeholder="status = 'active' AND created_at >= '2024-01-01'"
disabled={readonly}
rows={2}
@@ -354,12 +354,12 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
</div>
{/* 필드 매핑 에디터 */}
{localConfig.outboundMapping?.sourceTable && (
{localConfig.outbound_mapping?.source_table && (
<FieldMappingEditor
mappings={localConfig.outboundMapping.fieldMappings}
mappings={localConfig.outbound_mapping.field_mappings}
onMappingsChange={handleOutboundFieldMappingsChange}
direction="outbound"
sourceTable={availableTables.find((t) => t.name === localConfig.outboundMapping?.sourceTable)}
sourceTable={availableTables.find((t) => t.name === localConfig.outbound_mapping?.source_table)}
readonly={readonly}
/>
)}
@@ -374,15 +374,15 @@ export const DataMappingSettings: React.FC<DataMappingSettingsProps> = ({
<h4 className="mb-2 text-sm font-medium"> </h4>
<div className="text-muted-foreground space-y-1 text-xs">
<div>: {DATA_DIRECTION_OPTIONS.find((o) => o.value === localConfig.direction)?.label}</div>
{localConfig.inboundMapping && (
{localConfig.inbound_mapping && (
<div>
{localConfig.inboundMapping.targetTable}({localConfig.inboundMapping.fieldMappings.length}
{localConfig.inbound_mapping.target_table}({localConfig.inbound_mapping.field_mappings.length}
)
</div>
)}
{localConfig.outboundMapping && (
{localConfig.outbound_mapping && (
<div>
{localConfig.outboundMapping.sourceTable} ({localConfig.outboundMapping.fieldMappings.length}
{localConfig.outbound_mapping.source_table} ({localConfig.outbound_mapping.field_mappings.length}
)
</div>
)}
@@ -15,7 +15,7 @@ import {
RestApiSettings as RestApiSettingsType,
ApiTestResult,
} from "@/types/external-call/ExternalCallTypes";
import { DataMappingConfig, TableInfo } from "@/types/external-call/DataMappingTypes";
import { DataMappingConfig, TableInfo, DataType } from "@/types/external-call/DataMappingTypes";
// API import
import { DataFlowAPI } from "@/lib/api/dataflow";
@@ -33,35 +33,35 @@ import { DataMappingSettings } from "./DataMappingSettings";
* REST API , ,
*/
const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
relationshipId,
relationship_id,
onSettingsChange,
initialSettings,
initial_settings,
readonly = false,
}) => {
console.log("🌐 [ExternalCallPanel] Component mounted with props:", {
relationshipId,
initialSettings,
relationship_id,
initial_settings,
readonly,
});
// 상태 관리
const [config, setConfig] = useState<ExternalCallConfig>(
() => {
if (initialSettings) {
console.log("🔄 [ExternalCallPanel] 기존 설정 로드:", initialSettings);
return initialSettings;
if (initial_settings) {
console.log("🔄 [ExternalCallPanel] 기존 설정 로드:", initial_settings);
return initial_settings;
}
console.log("🔄 [ExternalCallPanel] 기본 설정 사용");
return {
callType: "rest-api",
restApiSettings: {
apiUrl: "",
httpMethod: "POST",
call_type: "rest-api",
rest_api_settings: {
api_url: "",
http_method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
bodyTemplate: `{
body_template: `{
"message": "데이터가 업데이트되었습니다",
"data": {{sourceData}},
"timestamp": "{{timestamp}}",
@@ -71,7 +71,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
type: "none",
},
timeout: 30000, // 30초
retryCount: 3,
retry_count: 3,
},
};
},
@@ -83,12 +83,12 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
// 데이터 매핑 상태
const [dataMappingConfig, setDataMappingConfig] = useState<DataMappingConfig>(() => {
// initialSettings에서 데이터 매핑 정보 불러오기
if (initialSettings?.dataMappingConfig) {
console.log("🔄 [ExternalCallPanel] 기존 데이터 매핑 설정 로드:", initialSettings.dataMappingConfig);
return initialSettings.dataMappingConfig;
// initial_settings에서 데이터 매핑 정보 불러오기
if (initial_settings?.data_mapping_config) {
console.log("🔄 [ExternalCallPanel] 기존 데이터 매핑 설정 로드:", initial_settings.data_mapping_config);
return initial_settings.data_mapping_config;
}
console.log("🔄 [ExternalCallPanel] 기본 데이터 매핑 설정 사용");
return {
direction: "none",
@@ -113,19 +113,19 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
const columns = await DataFlowAPI.getTableColumns(table.tableName);
return {
name: table.tableName,
displayName: table.displayName || table.tableName,
display_name: table.displayName || table.tableName,
fields: columns.map((col) => ({
name: col.columnName,
dataType: col.dataType,
nullable: col.nullable,
isPrimaryKey: col.isPrimaryKey || false,
data_type: (col.dataType || "string") as DataType,
nullable: Boolean(col.isNullable),
is_primary_key: false,
})),
};
} catch (error) {
console.warn(`테이블 ${table.tableName} 컬럼 정보 로드 실패:`, error);
return {
name: table.tableName,
displayName: table.displayName || table.tableName,
display_name: table.displayName || table.tableName,
fields: [],
};
}
@@ -151,10 +151,10 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
(newSettings: RestApiSettingsType) => {
const updatedConfig: ExternalCallConfig = {
...config,
restApiSettings: newSettings,
rest_api_settings: newSettings,
metadata: {
...config.metadata,
updatedAt: new Date().toISOString(),
created_at: config.metadata?.created_at || new Date().toISOString(),
updated_at: new Date().toISOString(),
version: "1.0",
},
};
@@ -162,7 +162,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
setConfig(updatedConfig);
onSettingsChange({
...updatedConfig,
dataMappingConfig,
data_mapping_config: dataMappingConfig,
});
},
[config, onSettingsChange, dataMappingConfig],
@@ -178,7 +178,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
// 전체 설정에 데이터 매핑 정보 포함하여 상위로 전달
onSettingsChange({
...config,
dataMappingConfig: newMappingConfig,
data_mapping_config: newMappingConfig,
});
},
[config, onSettingsChange],
@@ -194,16 +194,16 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
// 설정 유효성 검사
const validateConfig = useCallback(() => {
const { restApiSettings } = config;
const rest_api_settings = config.rest_api_settings;
// HTTP 메서드에 따라 바디 필요 여부 결정
const methodNeedsBody = !["GET", "HEAD", "DELETE"].includes(restApiSettings.httpMethod?.toUpperCase());
const methodNeedsBody = !["GET", "HEAD", "DELETE"].includes(rest_api_settings.http_method?.toUpperCase());
const isValid = !!(
restApiSettings.apiUrl &&
restApiSettings.apiUrl.startsWith("http") &&
restApiSettings.httpMethod &&
(methodNeedsBody ? restApiSettings.bodyTemplate : true) // GET/HEAD/DELETE는 바디 불필요
rest_api_settings.api_url &&
rest_api_settings.api_url.startsWith("http") &&
rest_api_settings.http_method &&
(methodNeedsBody ? rest_api_settings.body_template : true) // GET/HEAD/DELETE는 바디 불필요
);
setIsConfigValid(isValid);
@@ -263,7 +263,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
{/* API 설정 탭 */}
<TabsContent value="settings" className="flex-1 space-y-2 overflow-y-auto">
<RestApiSettings
settings={config.restApiSettings}
settings={config.rest_api_settings}
onSettingsChange={handleRestApiSettingsChange}
readonly={readonly}
/>
@@ -274,7 +274,7 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
<DataMappingSettings
config={dataMappingConfig}
onConfigChange={handleDataMappingConfigChange}
httpMethod={config.restApiSettings?.httpMethod || "GET"}
httpMethod={config.rest_api_settings?.http_method || "GET"}
availableTables={availableTables}
readonly={readonly}
tablesLoading={tablesLoading}
@@ -285,13 +285,13 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
<TabsContent value="test" className="flex-1 space-y-4 overflow-y-auto">
{isConfigValid ? (
<ExternalCallTestPanel
settings={config.restApiSettings}
settings={config.rest_api_settings}
context={{
relationshipId,
diagramId: "test-diagram",
relationship_id,
diagram_id: "test-diagram",
user_id: "current-user",
executionId: "test-execution",
sourceData: { test: "data" },
execution_id: "test-execution",
source_data: { test: "data" },
timestamp: new Date().toISOString(),
}}
onTestResult={handleTestResult}
@@ -324,11 +324,11 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground"> :</span>
<span className="ml-2 font-mono">{lastTestResult.statusCode || "N/A"}</span>
<span className="ml-2 font-mono">{lastTestResult.status_code || "N/A"}</span>
</div>
<div>
<span className="text-muted-foreground"> :</span>
<span className="ml-2 font-mono">{lastTestResult.responseTime}ms</span>
<span className="ml-2 font-mono">{lastTestResult.response_time}ms</span>
</div>
</div>
@@ -338,11 +338,11 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
</Alert>
)}
{lastTestResult.responseData && (
{lastTestResult.response && (
<div>
<span className="text-muted-foreground text-sm"> :</span>
<pre className="bg-muted mt-1 max-h-32 overflow-auto rounded p-2 text-xs">
{JSON.stringify(lastTestResult.responseData, null, 2)}
{JSON.stringify(lastTestResult.response, null, 2)}
</pre>
</div>
)}
@@ -407,20 +407,20 @@ const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
<CardContent className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"> ID:</span>
<code className="bg-muted rounded px-2 py-1 text-xs">{relationshipId}</code>
<code className="bg-muted rounded px-2 py-1 text-xs">{relationship_id}</code>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant="outline">{config.callType.toUpperCase()}</Badge>
<Badge variant="outline">{config.call_type.toUpperCase()}</Badge>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<Badge variant={isConfigValid ? "default" : "secondary"}>{isConfigValid ? "완료" : "미완료"}</Badge>
</div>
{config.metadata?.updatedAt && (
{config.metadata?.updated_at && (
<div className="flex justify-between">
<span className="text-muted-foreground"> :</span>
<span className="text-xs">{new Date(config.metadata.updatedAt).toLocaleString()}</span>
<span className="text-xs">{new Date(config.metadata.updated_at).toLocaleString()}</span>
</div>
)}
</CardContent>
@@ -32,7 +32,7 @@ import {
ApiTestResult,
ExternalCallContext,
} from "@/types/external-call/ExternalCallTypes";
import { ExternalCallAPI } from "@/lib/api/externalCall";
import { ExternalCallAPI, ExternalCallTestRequest } from "@/lib/api/externalCall";
/**
* 🧪 API
@@ -52,17 +52,17 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
const [activeTab, setActiveTab] = useState<string>("request");
const [processedTemplate, setProcessedTemplate] = useState<string>("");
const [testContext, setTestContext] = useState<ExternalCallContext>(() => ({
relationshipId: context?.relationshipId || "test-relationship",
diagramId: context?.diagramId || "test-diagram",
relationship_id: context?.relationship_id || "test-relationship",
diagram_id: context?.diagram_id || "test-diagram",
user_id: context?.user_id || "test-user",
executionId: context?.executionId || `test-${Date.now()}`,
sourceData: context?.sourceData || {
execution_id: context?.execution_id || `test-${Date.now()}`,
source_data: context?.source_data || {
id: 1,
name: "테스트 데이터",
value: 100,
status: "active",
},
targetData: context?.targetData,
target_data: context?.target_data,
timestamp: context?.timestamp || new Date().toISOString(),
metadata: context?.metadata,
}));
@@ -73,13 +73,13 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
// 각 템플릿 변수를 실제 값으로 치환
const replacements = {
"{{sourceData}}": JSON.stringify(context.sourceData, null, 2),
"{{targetData}}": context.targetData ? JSON.stringify(context.targetData, null, 2) : "null",
"{{sourceData}}": JSON.stringify(context.source_data, null, 2),
"{{targetData}}": context.target_data ? JSON.stringify(context.target_data, null, 2) : "null",
"{{timestamp}}": context.timestamp,
"{{relationshipId}}": context.relationshipId,
"{{diagramId}}": context.diagramId,
"{{relationshipId}}": context.relationship_id,
"{{diagramId}}": context.diagram_id,
"{{userId}}": context.user_id,
"{{executionId}}": context.executionId,
"{{executionId}}": context.execution_id,
};
Object.entries(replacements).forEach(([variable, value]) => {
@@ -91,15 +91,15 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
// 템플릿 처리 (설정이나 컨텍스트 변경 시)
useEffect(() => {
if (settings.bodyTemplate) {
const processed = processTemplate(settings.bodyTemplate, testContext);
if (settings.body_template) {
const processed = processTemplate(settings.body_template, testContext);
setProcessedTemplate(processed);
}
}, [settings.bodyTemplate, testContext, processTemplate]);
}, [settings.body_template, testContext, processTemplate]);
// API 테스트 실행
const handleRunTest = useCallback(async () => {
if (!settings.apiUrl) {
if (!settings.api_url) {
toast.error("API URL을 입력해주세요.");
return;
}
@@ -109,19 +109,19 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
try {
// 테스트 요청 데이터 구성 (백엔드 형식에 맞춤)
const testRequest = {
const testRequest: ExternalCallTestRequest = {
settings: {
callType: "rest-api" as const,
apiType: "generic" as const,
url: settings.apiUrl,
method: settings.httpMethod,
url: settings.api_url,
method: settings.http_method,
headers: settings.headers,
body: processedTemplate,
authentication: settings.authentication, // 인증 정보 추가
timeout: settings.timeout,
retryCount: settings.retryCount,
},
templateData: testContext,
retryCount: settings.retry_count,
} as Record<string, unknown>,
templateData: testContext as unknown as Record<string, unknown>,
};
// API 호출
@@ -131,8 +131,8 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
// 백엔드 응답을 ApiTestResult 형태로 변환
const apiTestResult: ApiTestResult = {
success: response.result.success,
statusCode: response.result.statusCode,
responseTime: response.result.executionTime || 0,
status_code: response.result.statusCode,
response_time: response.result.executionTime || 0,
response: response.result.response,
error: response.result.error,
timestamp: new Date().toISOString(),
@@ -151,7 +151,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
} else {
const errorResult: ApiTestResult = {
success: false,
responseTime: 0,
response_time: 0,
error: response.error || "알 수 없는 오류가 발생했습니다.",
timestamp: new Date().toISOString(),
};
@@ -162,7 +162,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
} catch (error) {
const errorResult: ApiTestResult = {
success: false,
responseTime: 0,
response_time: 0,
error: error instanceof Error ? error.message : "네트워크 오류가 발생했습니다.",
timestamp: new Date().toISOString(),
};
@@ -187,11 +187,11 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
// 테스트 컨텍스트 리셋
const handleResetContext = useCallback(() => {
setTestContext({
relationshipId: "test-relationship",
diagramId: "test-diagram",
relationship_id: "test-relationship",
diagram_id: "test-diagram",
user_id: "test-user",
executionId: `test-${Date.now()}`,
sourceData: {
execution_id: `test-${Date.now()}`,
source_data: {
id: 1,
name: "테스트 데이터",
value: 100,
@@ -225,7 +225,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
<Button
onClick={handleRunTest}
disabled={disabled || isLoading || !settings.apiUrl}
disabled={disabled || isLoading || !settings.api_url}
className="min-w-[100px]"
>
{isLoading ? (
@@ -267,7 +267,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
<Network className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> </span>
</div>
<div className="text-muted-foreground text-xs">{testResult.statusCode || "N/A"}</div>
<div className="text-muted-foreground text-xs">{testResult.status_code || "N/A"}</div>
</div>
<div className="space-y-1">
@@ -275,7 +275,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
<Timer className="h-4 w-4 text-amber-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="text-muted-foreground text-xs">{testResult.responseTime}ms</div>
<div className="text-muted-foreground text-xs">{testResult.response_time}ms</div>
</div>
<div className="space-y-1">
@@ -322,8 +322,8 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
handleCopyToClipboard(
JSON.stringify(
{
url: settings.apiUrl,
method: settings.httpMethod,
url: settings.api_url,
method: settings.http_method,
headers: settings.headers,
body: processedTemplate,
},
@@ -344,12 +344,12 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
<div className="col-span-1">
<Label className="text-muted-foreground text-xs">HTTP </Label>
<Badge variant="outline" className="mt-1">
{settings.httpMethod}
{settings.http_method}
</Badge>
</div>
<div className="col-span-3">
<Label className="text-muted-foreground text-xs">URL</Label>
<div className="bg-muted mt-1 rounded p-2 font-mono text-sm break-all">{settings.apiUrl}</div>
<div className="bg-muted mt-1 rounded p-2 font-mono text-sm break-all">{settings.api_url}</div>
</div>
</div>
@@ -364,7 +364,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
</div>
{/* 요청 바디 (POST/PUT/PATCH인 경우) */}
{["POST", "PUT", "PATCH"].includes(settings.httpMethod) && (
{["POST", "PUT", "PATCH"].includes(settings.http_method) && (
<div>
<Label className="text-muted-foreground text-xs"> (릿 )</Label>
<ScrollArea className="mt-1 h-40 w-full rounded border">
@@ -399,21 +399,21 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
{testResult.success ? (
<>
{/* 상태 코드 */}
{testResult.statusCode && (
{testResult.status_code && (
<div>
<Label className="text-muted-foreground text-xs"> </Label>
<div className="mt-1 rounded border bg-emerald-50 p-2">
<span className="font-mono text-sm text-emerald-700">{testResult.statusCode}</span>
<span className="font-mono text-sm text-emerald-700">{testResult.status_code}</span>
</div>
</div>
)}
{/* 응답 시간 */}
{testResult.responseTime !== undefined && (
{testResult.response_time !== undefined && (
<div>
<Label className="text-muted-foreground text-xs"> </Label>
<div className="mt-1 rounded border bg-accent p-2">
<span className="font-mono text-sm text-primary">{testResult.responseTime}ms</span>
<span className="font-mono text-sm text-primary">{testResult.response_time}ms</span>
</div>
</div>
)}
@@ -437,7 +437,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
<div className="space-y-2">
<div className="font-medium"> </div>
<div className="text-sm">{testResult.error}</div>
{testResult.statusCode && <div className="text-sm"> : {testResult.statusCode}</div>}
{testResult.status_code && <div className="text-sm"> : {testResult.status_code}</div>}
</div>
</AlertDescription>
</Alert>
@@ -44,9 +44,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
const addMapping = useCallback(() => {
const newMapping: FieldMapping = {
id: `mapping-${Date.now()}`,
sourceField: "",
targetField: "",
dataType: "string",
source_field: "",
target_field: "",
data_type: "string",
required: false,
};
onMappingsChange([...mappings, newMapping]);
@@ -78,15 +78,15 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
currentTable.fields.forEach((field) => {
// 이미 매핑된 필드는 건너뛰기
const existingMapping = mappings.find((m) =>
direction === "inbound" ? m.targetField === field.name : m.sourceField === field.name,
direction === "inbound" ? m.target_field === field.name : m.source_field === field.name,
);
if (existingMapping) return;
const mapping: FieldMapping = {
id: `auto-${field.name}-${Date.now()}`,
sourceField: direction === "inbound" ? field.name : field.name,
targetField: direction === "inbound" ? field.name : field.name,
dataType: field.dataType,
source_field: direction === "inbound" ? field.name : field.name,
target_field: direction === "inbound" ? field.name : field.name,
data_type: field.data_type,
required: !field.nullable,
};
newMappings.push(mapping);
@@ -108,7 +108,7 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
fields.forEach((fieldName) => {
// 이미 매핑된 필드는 건너뛰기
const existingMapping = mappings.find((m) =>
direction === "inbound" ? m.sourceField === fieldName : m.targetField === fieldName,
direction === "inbound" ? m.source_field === fieldName : m.target_field === fieldName,
);
if (existingMapping) return;
@@ -121,9 +121,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
const mapping: FieldMapping = {
id: `sample-${fieldName}-${Date.now()}`,
sourceField: direction === "inbound" ? fieldName : "",
targetField: direction === "inbound" ? "" : fieldName,
dataType,
source_field: direction === "inbound" ? fieldName : "",
target_field: direction === "inbound" ? "" : fieldName,
data_type: dataType,
required: false,
};
newMappings.push(mapping);
@@ -193,10 +193,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
<Database className="h-3 w-3 text-emerald-500" />
)}
<Input
value={mapping.sourceField}
onChange={(e) => updateMapping(mapping.id, { sourceField: e.target.value })}
value={mapping.source_field}
onChange={(e) => updateMapping(mapping.id, { source_field: e.target.value })}
placeholder={direction === "inbound" ? "API 필드명" : "테이블 컬럼명"}
size="sm"
disabled={readonly}
/>
</div>
@@ -218,12 +217,12 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
)}
{direction === "inbound" && currentTable ? (
<Select
value={mapping.targetField}
value={mapping.target_field}
onValueChange={(value) => {
const field = currentTable.fields.find((f) => f.name === value);
updateMapping(mapping.id, {
targetField: value,
dataType: field?.dataType || mapping.dataType,
target_field: value,
data_type: field?.data_type || mapping.data_type,
});
}}
disabled={readonly}
@@ -237,9 +236,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
<div className="flex items-center gap-2">
{field.name}
<Badge variant="outline" className="text-xs">
{field.dataType}
{field.data_type}
</Badge>
{field.isPrimaryKey && (
{field.is_primary_key && (
<Badge variant="default" className="text-xs">
PK
</Badge>
@@ -251,10 +250,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
</Select>
) : (
<Input
value={mapping.targetField}
onChange={(e) => updateMapping(mapping.id, { targetField: e.target.value })}
value={mapping.target_field}
onChange={(e) => updateMapping(mapping.id, { target_field: e.target.value })}
placeholder={direction === "inbound" ? "테이블 컬럼명" : "API 필드명"}
size="sm"
disabled={readonly}
/>
)}
@@ -265,8 +263,8 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
<div className="col-span-2">
<Label className="text-xs"></Label>
<Select
value={mapping.dataType}
onValueChange={(value: any) => updateMapping(mapping.id, { dataType: value })}
value={mapping.data_type}
onValueChange={(value: any) => updateMapping(mapping.id, { data_type: value })}
disabled={readonly}
>
<SelectTrigger size="sm">
@@ -310,10 +308,9 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={mapping.defaultValue || ""}
onChange={(e) => updateMapping(mapping.id, { defaultValue: e.target.value })}
value={mapping.default_value || ""}
onChange={(e) => updateMapping(mapping.id, { default_value: e.target.value })}
placeholder="기본값"
size="sm"
disabled={readonly}
/>
</div>
@@ -348,11 +345,10 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
value={mapping.transform.value || ""}
onChange={(e) =>
updateMapping(mapping.id, {
transform: { ...mapping.transform, value: e.target.value },
transform: { ...mapping.transform, type: mapping.transform!.type, value: e.target.value },
})
}
placeholder="상수값"
size="sm"
disabled={readonly}
/>
)}
@@ -362,11 +358,10 @@ export const FieldMappingEditor: React.FC<FieldMappingEditorProps> = ({
value={mapping.transform.format || ""}
onChange={(e) =>
updateMapping(mapping.id, {
transform: { ...mapping.transform, format: e.target.value },
transform: { ...mapping.transform, type: mapping.transform!.type, format: e.target.value },
})
}
placeholder="YYYY-MM-DD"
size="sm"
disabled={readonly}
/>
)}
@@ -28,7 +28,7 @@ import {
} from "lucide-react";
// 타입 import
import { RestApiSettings as RestApiSettingsType, RestApiSettingsProps } from "@/types/external-call/ExternalCallTypes";
import { RestApiSettings as RestApiSettingsType, RestApiSettingsProps, AuthenticationConfig } from "@/types/external-call/ExternalCallTypes";
import {
HttpMethod,
AuthenticationType,
@@ -58,7 +58,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
(url: string) => {
onSettingsChange({
...settings,
apiUrl: url,
api_url: url,
});
},
[settings, onSettingsChange],
@@ -69,7 +69,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
(method: HttpMethod) => {
onSettingsChange({
...settings,
httpMethod: method,
http_method: method as RestApiSettingsType["http_method"],
});
},
[settings, onSettingsChange],
@@ -125,7 +125,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
(template: string) => {
onSettingsChange({
...settings,
bodyTemplate: template,
body_template: template,
});
},
[settings, onSettingsChange],
@@ -138,7 +138,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
if (preset) {
onSettingsChange({
...settings,
bodyTemplate: preset.template,
body_template: preset.template,
});
}
},
@@ -147,7 +147,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
// 인증 설정 변경 핸들러
const handleAuthChange = useCallback(
(auth: Partial<AuthenticationType>) => {
(auth: Partial<AuthenticationConfig>) => {
onSettingsChange({
...settings,
authentication: {
@@ -175,7 +175,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
(retryCount: number) => {
onSettingsChange({
...settings,
retryCount,
retry_count: retryCount,
});
},
[settings, onSettingsChange],
@@ -186,17 +186,17 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
const errors: string[] = [];
// URL 검증
if (!settings.apiUrl) {
if (!settings.api_url) {
errors.push("API URL은 필수입니다.");
} else if (!settings.apiUrl.startsWith("http")) {
} else if (!settings.api_url.startsWith("http")) {
errors.push("API URL은 http:// 또는 https://로 시작해야 합니다.");
}
// 바디 템플릿 JSON 검증 (POST/PUT/PATCH 메서드인 경우)
if (["POST", "PUT", "PATCH"].includes(settings.httpMethod) && settings.bodyTemplate) {
if (["POST", "PUT", "PATCH"].includes(settings.http_method) && settings.body_template) {
try {
// 템플릿 변수를 임시 값으로 치환하여 JSON 유효성 검사
const testTemplate = settings.bodyTemplate.replace(/\{\{[^}]+\}\}/g, '"test_value"');
const testTemplate = settings.body_template.replace(/\{\{[^}]+\}\}/g, '"test_value"');
JSON.parse(testTemplate);
} catch {
errors.push("요청 바디 템플릿이 유효한 JSON 형식이 아닙니다.");
@@ -213,7 +213,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
) {
errors.push("Basic 인증에는 사용자명과 비밀번호가 필요합니다.");
}
if (settings.authentication?.type === "api-key" && !settings.authentication.apiKey) {
if (settings.authentication?.type === "api-key" && !settings.authentication.api_key) {
errors.push("API 키가 필요합니다.");
}
@@ -269,7 +269,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
id="apiUrl"
type="url"
placeholder="https://api.example.com/webhook"
value={settings.apiUrl}
value={settings.api_url}
onChange={(e) => handleUrlChange(e.target.value)}
disabled={readonly}
className={validationErrors.some((e) => e.includes("URL")) ? "border-destructive" : ""}
@@ -280,7 +280,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
{/* HTTP 메서드 */}
<div className="space-y-2">
<Label htmlFor="httpMethod">HTTP </Label>
<Select value={settings.httpMethod} onValueChange={handleMethodChange} disabled={readonly}>
<Select value={settings.http_method} onValueChange={handleMethodChange} disabled={readonly}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -329,7 +329,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
type="number"
min="0"
max="10"
value={settings.retryCount || DEFAULT_RETRY_POLICY.maxRetries}
value={settings.retry_count || DEFAULT_RETRY_POLICY.max_retries}
onChange={(e) => handleRetryCountChange(parseInt(e.target.value))}
disabled={readonly}
/>
@@ -434,11 +434,11 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
</div>
</CardHeader>
<CardContent className="space-y-4">
{["POST", "PUT", "PATCH"].includes(settings.httpMethod) ? (
{["POST", "PUT", "PATCH"].includes(settings.http_method) ? (
<>
<Textarea
placeholder="JSON 템플릿을 입력하세요..."
value={settings.bodyTemplate}
value={settings.body_template}
onChange={(e) => handleBodyTemplateChange(e.target.value)}
disabled={readonly}
className="min-h-[200px] font-mono text-sm"
@@ -454,7 +454,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
) : (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>{settings.httpMethod} .</AlertDescription>
<AlertDescription>{settings.http_method} .</AlertDescription>
</Alert>
)}
</CardContent>
@@ -565,8 +565,8 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
id="apiKey"
type={showPassword ? "text" : "password"}
placeholder="API 키를 입력하세요"
value={settings.authentication.apiKey || ""}
onChange={(e) => handleAuthChange({ apiKey: e.target.value })}
value={settings.authentication.api_key || ""}
onChange={(e) => handleAuthChange({ api_key: e.target.value })}
disabled={readonly}
className="pr-10"
/>
@@ -586,10 +586,10 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
<div className="space-y-2">
<Label></Label>
<Select
value={settings.authentication.apiKeyLocation || "header"}
value={settings.authentication.api_key_location || "header"}
onValueChange={(location) =>
handleAuthChange({
apiKeyLocation: location as "header" | "query",
api_key_location: location as "header" | "query",
})
}
disabled={readonly}
@@ -608,13 +608,13 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ settings, onSettingsC
<Label htmlFor="keyName"> </Label>
<Input
id="keyName"
placeholder={settings.authentication.apiKeyLocation === "query" ? "api_key" : "X-API-Key"}
value={settings.authentication.apiKeyHeader || settings.authentication.apiKeyQueryParam || ""}
placeholder={settings.authentication.api_key_location === "query" ? "api_key" : "X-API-Key"}
value={settings.authentication.api_key_header || settings.authentication.api_key_query_param || ""}
onChange={(e) => {
if (settings.authentication?.apiKeyLocation === "query") {
handleAuthChange({ apiKeyQueryParam: e.target.value });
if (settings.authentication?.api_key_location === "query") {
handleAuthChange({ api_key_query_param: e.target.value });
} else {
handleAuthChange({ apiKeyHeader: e.target.value });
handleAuthChange({ api_key_header: e.target.value });
}
}}
disabled={readonly}
@@ -51,7 +51,7 @@ export function FlowToolbar({
const [showSaveDialog, setShowSaveDialog] = useState(false);
const handleSaveRef = useRef<() => void>();
const handleSaveRef = useRef<(() => void) | undefined>(undefined);
useEffect(() => {
handleSaveRef.current = handleSave;
});
@@ -7,7 +7,7 @@ import { CompactNodeShell } from "./CompactNodeShell";
import type { AggregateNodeData } from "@/types/node-editor";
export const AggregateNode = memo(({ data, selected }: NodeProps<AggregateNodeData>) => {
const opCount = data.operations?.length || 0;
const opCount = data.aggregations?.length || 0;
const groupCount = data.groupByFields?.length || 0;
const summary = opCount > 0
? `${opCount}개 연산${groupCount > 0 ? `, ${groupCount}개 그룹` : ""}`
@@ -23,7 +23,7 @@ export const AggregateNode = memo(({ data, selected }: NodeProps<AggregateNodeDa
>
{opCount > 0 && (
<div className="space-y-0.5">
{data.operations!.slice(0, 3).map((op: any, i: number) => (
{data.aggregations!.slice(0, 3).map((op: any, i: number) => (
<div key={i} className="flex items-center gap-1.5">
<span className="rounded bg-violet-500/20 px-1 py-0.5 font-mono text-[9px] font-semibold text-violet-400">
{op.function || op.operation}
@@ -7,7 +7,7 @@ import { CompactNodeShell } from "./CompactNodeShell";
import type { DataTransformNodeData } from "@/types/node-editor";
export const DataTransformNode = memo(({ data, selected }: NodeProps<DataTransformNodeData>) => {
const ruleCount = data.transformRules?.length || 0;
const ruleCount = data.transformations?.length || 0;
const summary = ruleCount > 0
? `${ruleCount}개 변환 규칙`
: "변환 규칙을 설정해 주세요";
@@ -22,7 +22,7 @@ export const DataTransformNode = memo(({ data, selected }: NodeProps<DataTransfo
>
{ruleCount > 0 && (
<div className="space-y-0.5">
{data.transformRules!.slice(0, 3).map((r: any, i: number) => (
{data.transformations!.slice(0, 3).map((r: any, i: number) => (
<div key={i} className="flex items-center gap-1.5">
<div className="h-1 w-1 rounded-full bg-cyan-400" />
<span>{r.sourceField || r.field || `규칙 ${i + 1}`}</span>
@@ -134,7 +134,6 @@ function getNodeTypeLabel(type: NodeType): string {
externalDBSource: "외부 DB 소스",
restAPISource: "REST API 소스",
condition: "조건 분기",
fieldMapping: "필드 매핑",
dataTransform: "데이터 변환",
aggregate: "집계",
formulaTransform: "수식 변환",
@@ -69,8 +69,8 @@ export function AggregateProperties({ nodeId, data }: AggregatePropertiesProps)
const fields: Array<{ name: string; label?: string; type?: string }> = [];
sourceNodes.forEach((node) => {
if (node.data.fields) {
node.data.fields.forEach((field: any) => {
if ((node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
@@ -614,8 +614,8 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
updateNode(nodeId, { targetLookup: undefined });
// target 타입 조건들을 field로 변경
const newConditions = conditions.map((c) =>
(c as any).valueType === "target" ? { ...c, valueType: "field" } : c
);
(c as any).valueType === "target" ? { ...c, valueType: "field" as const } : c
) as typeof conditions;
setConditions(newConditions);
updateNode(nodeId, { conditions: newConditions });
};
@@ -56,15 +56,15 @@ export function DataTransformProperties({ nodeId, data }: DataTransformPropertie
const fields: Array<{ name: string; label?: string }> = [];
sourceNodes.forEach((node) => {
if (node.type === "tableSource" && node.data.fields) {
node.data.fields.forEach((field: any) => {
if (node.type === "tableSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
});
});
} else if (node.type === "externalDBSource" && node.data.fields) {
node.data.fields.forEach((field: any) => {
} else if (node.type === "externalDBSource" && (node.data as any).fields) {
(node.data as any).fields.forEach((field: any) => {
fields.push({
name: field.name,
label: field.label || field.displayName,
@@ -159,10 +159,10 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
console.log("🔗 소스 노드:", sourceNode.type, sourceNode.data);
// 소스 노드 타입에 따라 필드 추출
if (sourceNode.type === "trigger" && sourceNode.data.tableName) {
if ((sourceNode.type as string) === "trigger" && (sourceNode.data as any).tableName) {
// 트리거 노드: 테이블 컬럼 조회
try {
const columns = await tableTypeApi.getColumns(sourceNode.data.tableName);
const columns = await tableTypeApi.getColumns((sourceNode.data as any).tableName);
if (columns && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
@@ -178,10 +178,10 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
} catch (error) {
console.error("트리거 노드 컬럼 로딩 실패:", error);
}
} else if (sourceNode.type === "tableSource" && sourceNode.data.tableName) {
} else if (sourceNode.type === "tableSource" && (sourceNode.data as any).tableName) {
// 테이블 소스 노드
try {
const columns = await tableTypeApi.getColumns(sourceNode.data.tableName);
const columns = await tableTypeApi.getColumns((sourceNode.data as any).tableName);
if (columns && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
@@ -202,9 +202,9 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
const conditionIncomingEdges = edges.filter((e) => e.target === sourceNode.id);
for (const condEdge of conditionIncomingEdges) {
const condSourceNode = nodes.find((n) => n.id === condEdge.source);
if (condSourceNode?.type === "trigger" && condSourceNode.data.tableName) {
if ((condSourceNode?.type as string) === "trigger" && (condSourceNode?.data as any)?.tableName) {
try {
const columns = await tableTypeApi.getColumns(condSourceNode.data.tableName);
const columns = await tableTypeApi.getColumns((condSourceNode!.data as any).tableName);
if (columns && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
@@ -557,7 +557,7 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
setExternalColumns([]);
updateNode(nodeId, {
externalConnectionId: connectionId,
externalConnectionName: selectedConnection?.name,
externalConnectionName: selectedConnection?.connection_name,
externalDbType: selectedConnection?.db_type,
});
}}
@@ -576,7 +576,7 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
<div className="flex items-center gap-2">
<span className="font-medium">{conn.db_type}</span>
<span className="text-muted-foreground">-</span>
<span>{conn.name}</span>
<span>{conn.connection_name}</span>
</div>
</SelectItem>
))
@@ -1077,10 +1077,9 @@ export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesP
<div>
<Label className="text-xs text-muted-foreground"> </Label>
<Input
value={condition.staticValue || condition.value || ""}
value={condition.staticValue || ""}
onChange={(e) => {
handleConditionChange(index, "staticValue", e.target.value || undefined);
handleConditionChange(index, "value", e.target.value);
}}
placeholder="비교할 고정 값"
className="mt-1 h-8 text-xs"
+3 -3
View File
@@ -5,7 +5,7 @@ import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -248,7 +248,7 @@ export default function MailDetailModal({
originalFrom: mail.from,
originalSubject: mail.subject,
originalDate: mail.date,
originalBody: mail.body,
originalBody: mail.text_body ?? mail.html_body,
};
router.push(`/admin/automaticMng/mail/send?action=reply&data=${encodeURIComponent(JSON.stringify(replyData))}`);
onClose();
@@ -267,7 +267,7 @@ export default function MailDetailModal({
originalFrom: mail.from,
originalSubject: mail.subject,
originalDate: mail.date,
originalBody: mail.body,
originalBody: mail.text_body ?? mail.html_body,
originalAttachments: mail.attachments,
};
router.push(`/admin/automaticMng/mail/send?action=forward&data=${encodeURIComponent(JSON.stringify(forwardData))}`);
@@ -104,14 +104,14 @@ export default function MailNotifications() {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
sentMails.items?.forEach((mail) => {
const sentDate = new Date(mail.sentAt);
const sentDate = new Date(mail.sent_at);
if (sentDate > oneHourAgo) {
newNotifications.push({
id: `failed-${mail.id}`,
type: "send_failed",
title: "메일 발송 실패",
message: `${mail.to.join(', ')}에게 보낸 메일이 실패했습니다.`,
timestamp: mail.sentAt,
timestamp: mail.sent_at,
read: false,
url: '/admin/mail/sent', // 보낸메일함으로 이동
});
@@ -133,7 +133,7 @@ export default function MailNotifications() {
receivedMails.forEach((mail) => {
const receivedDate = new Date(mail.date);
if (receivedDate > thirtyMinutesAgo && !mail.isRead) {
if (receivedDate > thirtyMinutesAgo && !mail.is_read) {
newNotifications.push({
id: `new-${mail.id}`,
type: "new_mail",
@@ -162,24 +162,24 @@ export default function MailNotifications() {
});
accounts.forEach((account) => {
if (account.status === 'active' && account.dailyLimit) {
if (account.status === 'active' && account.daily_limit) {
const todaySentCount = sentMails.items?.filter((mail) => {
const sentDate = new Date(mail.sentAt);
const sentDate = new Date(mail.sent_at);
const today = new Date();
return (
mail.accountId === account.id &&
mail.account_id === account.id &&
sentDate.toDateString() === today.toDateString()
);
}).length || 0;
const usagePercent = (todaySentCount / account.dailyLimit) * 100;
const usagePercent = (todaySentCount / account.daily_limit) * 100;
if (usagePercent >= 80) {
newNotifications.push({
id: `limit-${account.id}`,
type: "limit_warning",
title: "일일 발송 제한 경고",
message: `${account.name} 계정이 일일 제한의 ${usagePercent.toFixed(0)}%를 사용했습니다 (${todaySentCount}/${account.dailyLimit})`,
message: `${account.name} 계정이 일일 제한의 ${usagePercent.toFixed(0)}%를 사용했습니다 (${todaySentCount}/${account.daily_limit})`,
timestamp: new Date().toISOString(),
read: false,
url: '/admin/mail/accounts', // 계정 관리로 이동
@@ -97,9 +97,9 @@ export default function MailTemplateCard({
<div className="flex items-center gap-3 text-xs text-muted-foreground pt-2 border-t">
<div className="flex items-center gap-1">
<Calendar className="w-3.5 h-3.5" />
<span>{formatDate(template.createdAt)}</span>
<span>{formatDate(template.created_at)}</span>
</div>
{template.updatedAt !== template.createdAt && (
{template.updated_at !== template.created_at && (
<span className="text-muted-foreground/70"></span>
)}
</div>
@@ -50,9 +50,9 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
type="number"
min={1}
max={10}
value={config.sequenceLength || 3}
value={config.sequence_length || 3}
onChange={(e) =>
onChange({ ...config, sequenceLength: parseInt(e.target.value) || 3 })
onChange({ ...config, sequence_length: parseInt(e.target.value) || 3 })
}
disabled={isPreview}
className="h-8 text-xs sm:h-10 sm:text-sm"
@@ -66,9 +66,9 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
<Input
type="number"
min={1}
value={config.startFrom || 1}
value={config.start_from || 1}
onChange={(e) =>
onChange({ ...config, startFrom: parseInt(e.target.value) || 1 })
onChange({ ...config, start_from: parseInt(e.target.value) || 1 })
}
disabled={isPreview}
className="h-8 text-xs sm:h-10 sm:text-sm"
@@ -91,9 +91,9 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
type="number"
min={1}
max={10}
value={config.numberLength || 4}
value={config.number_length || 4}
onChange={(e) =>
onChange({ ...config, numberLength: parseInt(e.target.value) || 4 })
onChange({ ...config, number_length: parseInt(e.target.value) || 4 })
}
disabled={isPreview}
className="h-8 text-xs sm:h-10 sm:text-sm"
@@ -107,9 +107,9 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
<Input
type="number"
min={0}
value={config.numberValue || 0}
value={config.number_value || 0}
onChange={(e) =>
onChange({ ...config, numberValue: parseInt(e.target.value) || 0 })
onChange({ ...config, number_value: parseInt(e.target.value) || 0 })
}
disabled={isPreview}
className="h-8 text-xs sm:h-10 sm:text-sm"
@@ -139,8 +139,8 @@ export const AutoConfigPanel: React.FC<AutoConfigPanelProps> = ({
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Input
value={config.textValue || ""}
onChange={(e) => onChange({ ...config, textValue: e.target.value })}
value={config.text_value || ""}
onChange={(e) => onChange({ ...config, text_value: e.target.value })}
placeholder="예: PRJ, CODE, PROD"
disabled={isPreview}
className="h-8 text-xs sm:h-10 sm:text-sm"
@@ -205,9 +205,9 @@ const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
const [columnComboboxOpen, setColumnComboboxOpen] = useState(false);
// 체크박스 상태
const useColumnValue = config.useColumnValue || false;
const sourceTableName = config.sourceTableName || "";
const sourceColumnName = config.sourceColumnName || "";
const useColumnValue = config.use_column_value || false;
const sourceTableName = config.source_table_name || "";
const sourceColumnName = config.source_column_name || "";
// 테이블 목록 로드
useEffect(() => {
@@ -294,8 +294,8 @@ const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={config.dateFormat || "YYYYMMDD"}
onValueChange={(value) => onChange({ ...config, dateFormat: value })}
value={config.date_format || "YYYYMMDD"}
onValueChange={(value) => onChange({ ...config, date_format: value })}
disabled={isPreview}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
@@ -324,9 +324,9 @@ const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
onCheckedChange={(checked) => {
onChange({
...config,
useColumnValue: checked,
use_column_value: checked,
// 체크 해제 시 테이블/컬럼 초기화
...(checked ? {} : { sourceTableName: "", sourceColumnName: "" }),
...(checked ? {} : { source_table_name: "", source_column_name: "" }),
});
}}
disabled={isPreview}
@@ -386,8 +386,8 @@ const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
onSelect={() => {
onChange({
...config,
sourceTableName: table.table_name,
sourceColumnName: "", // 테이블 변경 시 컬럼 초기화
source_table_name: table.table_name,
source_column_name: "", // 테이블 변경 시 컬럼 초기화
});
setTableComboboxOpen(false);
}}
@@ -453,7 +453,7 @@ const DateConfigPanel: React.FC<DateConfigPanelProps> = ({
key={column.column_name}
value={`${column.display_name} ${column.column_name}`}
onSelect={() => {
onChange({ ...config, sourceColumnName: column.column_name });
onChange({ ...config, source_column_name: column.column_name });
setColumnComboboxOpen(false);
}}
className="text-xs sm:text-sm"
@@ -510,8 +510,8 @@ interface CategoryValueNode {
interface CategoryConfigPanelProps {
config?: {
categoryKey?: string;
categoryMappings?: CategoryFormatMapping[];
category_key?: string;
category_mappings?: CategoryFormatMapping[];
};
onChange: (config: any) => void;
isPreview?: boolean;
@@ -552,14 +552,14 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
// 수정 모드 진입 중 플래그 (useEffect 초기화 방지)
const isEditingRef = useRef(false);
const categoryKey = config.categoryKey || "";
const mappings = config.categoryMappings || [];
const categoryKey = config.category_key || "";
const mappings = config.category_mappings || [];
// 이미 추가된 카테고리 ID 목록 (수정 중인 항목 제외)
const addedValueIds = useMemo(() => {
return mappings
.filter(m => m.categoryValueId !== editingId)
.map(m => m.categoryValueId);
.filter(m => m.category_value_id !== editingId)
.map(m => m.category_value_id);
}, [mappings, editingId]);
// 카테고리 옵션 로드
@@ -712,23 +712,23 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
if (!selectedInfo || !newFormat.trim()) return;
const newMapping: CategoryFormatMapping = {
categoryValueId: selectedInfo.valueId,
categoryValueCode: selectedInfo.valueCode, // V2Select에서 valueCode를 value로 사용하므로 매칭용 저장
categoryValueLabel: selectedInfo.valueLabel,
categoryValuePath: selectedInfo.valuePath,
category_value_id: selectedInfo.valueId,
category_value_code: selectedInfo.valueCode, // V2Select에서 valueCode를 value로 사용하므로 매칭용 저장
category_value_label: selectedInfo.valueLabel,
category_value_path: selectedInfo.valuePath,
format: newFormat.trim(),
};
let updatedMappings: CategoryFormatMapping[];
if (editingId !== null) {
// 수정 모드: 기존 항목 교체
updatedMappings = mappings.map(m =>
m.categoryValueId === editingId ? newMapping : m
updatedMappings = mappings.map(m =>
m.category_value_id === editingId ? newMapping : m
);
} else {
// 추가 모드: 중복 체크
const exists = mappings.some(m => m.categoryValueId === selectedInfo.valueId);
const exists = mappings.some(m => m.category_value_id === selectedInfo.valueId);
if (exists) {
alert("이미 추가된 카테고리입니다");
return;
@@ -738,7 +738,7 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
onChange({
...config,
categoryMappings: updatedMappings,
category_mappings: updatedMappings,
});
// 초기화
@@ -768,17 +768,17 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
return null;
};
const parentPath = findParentIds(categoryValues, mapping.categoryValueId);
const parentPath = findParentIds(categoryValues, mapping.category_value_id);
if (parentPath && parentPath.length > 0) {
setLevel1Id(parentPath[0] || null);
if (parentPath.length === 2) {
// 3단계: 대분류 > 중분류 > 소분류
setLevel2Id(parentPath[1]);
setLevel3Id(mapping.categoryValueId);
setLevel3Id(mapping.category_value_id);
} else if (parentPath.length === 1) {
// 2단계: 대분류 > 중분류
setLevel2Id(mapping.categoryValueId);
setLevel2Id(mapping.category_value_id);
setLevel3Id(null);
} else {
setLevel2Id(null);
@@ -786,13 +786,13 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
}
} else {
// 루트 레벨 항목 (1단계)
setLevel1Id(mapping.categoryValueId);
setLevel1Id(mapping.category_value_id);
setLevel2Id(null);
setLevel3Id(null);
}
setNewFormat(mapping.format);
setEditingId(mapping.categoryValueId);
setEditingId(mapping.category_value_id);
// 다음 렌더링 사이클에서 플래그 해제
setTimeout(() => {
@@ -813,7 +813,7 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
const handleRemoveMapping = (valueId: number) => {
onChange({
...config,
categoryMappings: mappings.filter(m => m.categoryValueId !== valueId),
category_mappings: mappings.filter(m => m.category_value_id !== valueId),
});
};
@@ -850,7 +850,7 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
key={opt.displayName}
value={opt.displayLabel}
onSelect={() => {
onChange({ ...config, categoryKey: opt.displayName, categoryMappings: [] });
onChange({ ...config, category_key: opt.displayName, category_mappings: [] });
setCategoryKeyOpen(false);
}}
className="text-xs sm:text-sm"
@@ -1064,15 +1064,15 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
<div className="space-y-1">
{mappings.map((m) => (
<div
key={m.categoryValueId}
key={m.category_value_id}
className={cn(
"flex cursor-pointer items-center justify-between rounded px-2 py-1 transition-colors hover:bg-muted",
editingId === m.categoryValueId ? "bg-primary/10 ring-1 ring-primary" : "bg-muted/50"
editingId === m.category_value_id ? "bg-primary/10 ring-1 ring-primary" : "bg-muted/50"
)}
onClick={() => !isPreview && handleEditMapping(m)}
>
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">{m.categoryValuePath || m.categoryValueLabel}</span>
<span className="text-muted-foreground">{m.category_value_path || m.category_value_label}</span>
<span></span>
<span className="font-mono font-medium">{m.format}</span>
</div>
@@ -1081,7 +1081,7 @@ const CategoryConfigPanel: React.FC<CategoryConfigPanelProps> = ({
size="icon"
onClick={(e) => {
e.stopPropagation();
handleRemoveMapping(m.categoryValueId);
handleRemoveMapping(m.category_value_id);
}}
disabled={isPreview}
className="h-5 w-5"
@@ -1157,9 +1157,9 @@ function ReferenceConfigSection({
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={config.referenceColumnName || ""}
value={config.reference_column_name || ""}
onValueChange={(value) =>
onChange({ ...config, referenceColumnName: value })
onChange({ ...config, reference_column_name: value })
}
disabled={isPreview || loadingCols}
>
@@ -47,21 +47,21 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={part.partType}
value={part.part_type}
onValueChange={(value) => {
const newPartType = value as CodePartType;
// 타입 변경 시 해당 타입의 기본 autoConfig 설정
// 타입 변경 시 해당 타입의 기본 auto_config 설정
const defaultAutoConfig: Record<string, any> = {
sequence: { sequenceLength: 3, startFrom: 1 },
number: { numberLength: 4, numberValue: 1 },
date: { dateFormat: "YYYYMMDD" },
text: { textValue: "CODE" },
category: { categoryKey: "", categoryMappings: [] },
reference: { referenceColumnName: "" },
sequence: { sequence_length: 3, start_from: 1 },
number: { number_length: 4, number_value: 1 },
date: { date_format: "YYYYMMDD" },
text: { text_value: "CODE" },
category: { category_key: "", category_mappings: [] },
reference: { reference_column_name: "" },
};
onUpdate({
partType: newPartType,
autoConfig: defaultAutoConfig[newPartType] || {}
onUpdate({
part_type: newPartType,
auto_config: defaultAutoConfig[newPartType] || {}
});
}}
disabled={isPreview}
@@ -82,8 +82,8 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
<div>
<Label className="text-xs font-medium sm:text-sm"> </Label>
<Select
value={part.generationMethod}
onValueChange={(value) => onUpdate({ generationMethod: value as GenerationMethod })}
value={part.generation_method}
onValueChange={(value) => onUpdate({ generation_method: value as GenerationMethod })}
disabled={isPreview}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
@@ -100,18 +100,18 @@ export const NumberingRuleCard: React.FC<NumberingRuleCardProps> = ({
</Select>
</div>
{part.generationMethod === "auto" ? (
{part.generation_method === "auto" ? (
<AutoConfigPanel
partType={part.partType}
config={part.autoConfig}
onChange={(autoConfig) => onUpdate({ autoConfig })}
partType={part.part_type}
config={part.auto_config}
onChange={(auto_config) => onUpdate({ auto_config })}
isPreview={isPreview}
tableName={tableName}
/>
) : (
<ManualConfigPanel
config={part.manualConfig}
onChange={(manualConfig) => onUpdate({ manualConfig })}
config={part.manual_config}
onChange={(manual_config) => onUpdate({ manual_config })}
isPreview={isPreview}
/>
)}
@@ -87,29 +87,29 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
setCurrentRule(JSON.parse(JSON.stringify(rule)));
} else {
const newRule: NumberingRuleConfig = {
ruleId: `rule-${Date.now()}`,
ruleName: `${columnName} 채번`,
rule_id: `rule-${Date.now()}`,
rule_name: `${columnName} 채번`,
parts: [],
separator: "-",
resetPeriod: "none",
currentSequence: 1,
scopeType: "table",
tableName,
columnName,
reset_period: "none",
current_sequence: 1,
scope_type: "table",
table_name: tableName,
column_name: columnName,
};
setCurrentRule(newRule);
}
} catch {
const newRule: NumberingRuleConfig = {
ruleId: `rule-${Date.now()}`,
ruleName: `${columnName} 채번`,
rule_id: `rule-${Date.now()}`,
rule_name: `${columnName} 채번`,
parts: [],
separator: "-",
resetPeriod: "none",
currentSequence: 1,
scopeType: "table",
tableName,
columnName,
reset_period: "none",
current_sequence: 1,
scope_type: "table",
table_name: tableName,
column_name: columnName,
};
setCurrentRule(newRule);
} finally {
@@ -19,24 +19,24 @@ export function computePartDisplayItems(config: NumberingRuleConfig): PartDispla
const globalSep = config.separator ?? "-";
return sorted.map((part) => ({
order: part.order,
partType: part.partType,
partType: part.part_type,
displayValue: getPartDisplayValue(part),
}));
}
function getPartDisplayValue(part: NumberingRulePart): string {
if (part.generationMethod === "manual") {
return part.manualConfig?.value || "XXX";
if (part.generation_method === "manual") {
return part.manual_config?.value || "XXX";
}
const c = part.autoConfig || {};
switch (part.partType) {
const c = part.auto_config || {};
switch (part.part_type) {
case "sequence":
return String(c.startFrom ?? 1).padStart(c.sequenceLength ?? 3, "0");
return String(c.start_from ?? 1).padStart(c.sequence_length ?? 3, "0");
case "number":
return String(c.numberValue ?? 0).padStart(c.numberLength ?? 4, "0");
return String(c.number_value ?? 0).padStart(c.number_length ?? 4, "0");
case "date": {
const format = c.dateFormat || "YYYYMMDD";
if (c.useColumnValue && c.sourceColumnName) {
const format = c.date_format || "YYYYMMDD";
if (c.use_column_value && c.source_column_name) {
return format === "YYYY" ? "[YYYY]" : format === "YY" ? "[YY]" : format === "YYYYMM" ? "[YYYYMM]" : format === "YYMM" ? "[YYMM]" : format === "YYMMDD" ? "[YYMMDD]" : "[DATE]";
}
const now = new Date();
@@ -52,7 +52,7 @@ function getPartDisplayValue(part: NumberingRulePart): string {
return `${y}${m}${d}`;
}
case "text":
return c.textValue || "TEXT";
return c.text_value || "TEXT";
default:
return "XXX";
}
@@ -119,7 +119,7 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
result += item.displayValue;
if (idx < partItems.length - 1) {
const part = sortedParts.find((p) => p.order === item.order);
result += part?.separatorAfter ?? globalSep;
result += part?.separator_after ?? globalSep;
}
});
return result;
@@ -138,7 +138,7 @@ export const NumberingRulePreview: React.FC<NumberingRulePreviewProps> = ({
<span className={getPartTypeColorClass(item.partType)}>{item.displayValue}</span>
{idx < partItems.length - 1 && (
<span className="text-muted-foreground">
{sortedParts.find((p) => p.order === item.order)?.separatorAfter ?? globalSep}
{sortedParts.find((p) => p.order === item.order)?.separator_after ?? globalSep}
</span>
)}
</React.Fragment>
@@ -904,7 +904,7 @@ export default function PopDesigner({
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h2 className="text-sm font-medium">{selectedScreen?.screenName}</h2>
<h2 className="text-sm font-medium">{selectedScreen?.screen_name}</h2>
<p className="text-xs text-muted-foreground">
(v5)
</p>
@@ -266,7 +266,7 @@ function SimpleConnectionForm({
.then((res) => {
const cols = res.success && res.data?.columns;
if (Array.isArray(cols)) {
setSubColumns(cols.map((c) => c.columnName || "").filter(Boolean));
setSubColumns(cols.map((c) => c.column_name || "").filter(Boolean));
}
})
.catch(() => setSubColumns([]))
@@ -380,9 +380,9 @@ function TreeNode({
{/* 트리 연결 표시 */}
<span className="text-muted-foreground/50 text-xs mr-1"></span>
<Monitor className="h-4 w-4 text-primary shrink-0" />
<span className="flex-1 text-sm truncate">{screen.screenName}</span>
<span className="flex-1 text-sm truncate">{screen.screen_name}</span>
<span className="text-[10px] text-muted-foreground shrink-0">#{screen.screen_id}</span>
{/* 더보기 메뉴 (폴더와 동일한 스타일) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -694,7 +694,7 @@ export function PopCategoryTree({
});
if (response.data.success) {
toast.success(`"${screen.screenName}"을(를) "${(targetGroup as any)._displayName || targetGroup.group_name}"으로 이동했습니다.`);
toast.success(`"${screen.screen_name}"을(를) "${(targetGroup as any)._displayName || targetGroup.group_name}"으로 이동했습니다.`);
loadGroups(); // 그룹 목록 새로고침
} else {
throw new Error(response.data.message || "이동 실패");
@@ -718,7 +718,7 @@ export function PopCategoryTree({
}
await apiClient.delete(`/screen-groups/group-screens/${screenLink.id}`);
toast.success(`"${screen.screenName}"을(를) 그룹에서 제거했습니다.`);
toast.success(`"${screen.screen_name}"을(를) 그룹에서 제거했습니다.`);
loadGroups();
} catch (error: any) {
console.error("화면 제거 실패:", error);
@@ -739,7 +739,7 @@ export function PopCategoryTree({
try {
// 화면 삭제 API 호출 (휴지통으로 이동)
await apiClient.delete(`/screen-management/screens/${deletingScreen.screen_id}`);
toast.success(`"${deletingScreen.screenName}" 화면이 휴지통으로 이동되었습니다.`);
toast.success(`"${deletingScreen.screen_name}" 화면이 휴지통으로 이동되었습니다.`);
// 화면 목록 새로고침 (부모 컴포넌트에서 처리해야 함)
loadGroups();
@@ -1076,7 +1076,7 @@ export function PopCategoryTree({
onDoubleClick={() => onScreenDesign(screen)}
>
<Monitor className="h-4 w-4 text-muted-foreground/70 shrink-0" />
<span className="flex-1 text-sm truncate">{screen.screenName}</span>
<span className="flex-1 text-sm truncate">{screen.screen_name}</span>
<span className="text-[10px] text-muted-foreground shrink-0">#{screen.screen_id}</span>
<DropdownMenu>
@@ -1238,7 +1238,7 @@ export function PopCategoryTree({
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
"{movingScreen?.screenName}" .
"{movingScreen?.screen_name}" .
</DialogDescription>
</DialogHeader>
@@ -1314,7 +1314,7 @@ export function PopCategoryTree({
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
"{deletingScreen?.screenName}" ?
"{deletingScreen?.screen_name}" ?
<br />
<span className="text-muted-foreground text-xs">
, .
@@ -108,8 +108,8 @@ export function PopDeployModal({
setGroupEntries(
groupScreens.map((s) => ({
screen_id: s.screen_id!,
screenName: s.screen_name ?? s.screenName,
newScreenName: s.screen_name ?? s.screenName,
screenName: s.screen_name,
newScreenName: s.screen_name,
newScreenCode: "",
included: true,
})),
@@ -117,7 +117,7 @@ export function PopDeployModal({
setScreenName("");
setScreenCode("");
} else if (screen) {
setScreenName(screen.screen_name ?? screen.screenName);
setScreenName(screen.screen_name);
setScreenCode("");
setGroupEntries([]);
analyzeLinks(screen.screen_id!);
@@ -176,14 +176,14 @@ export function PopDeployModal({
);
return {
screen_id: linkedId,
screenName: (linkedScreen?.screen_name ?? linkedScreen?.screenName) || `화면 ${linkedId}`,
screenCode: (linkedScreen?.screen_code ?? linkedScreen?.screenCode) || "",
screenName: linkedScreen?.screen_name || `화면 ${linkedId}`,
screenCode: linkedScreen?.screen_code || "",
references: refs.map((r) => ({
componentId: r.componentId,
referenceType: r.referenceType,
})),
deploy: true,
newScreenName: (linkedScreen?.screen_name ?? linkedScreen?.screenName) || `화면 ${linkedId}`,
newScreenName: linkedScreen?.screen_name || `화면 ${linkedId}`,
newScreenCode: "",
};
},
@@ -283,7 +283,7 @@ export function PopDeployModal({
{isGroupMode
? `"${groupName}" 카테고리의 화면 ${groupScreens!.length}개를 다른 회사로 복사합니다.`
: screen
? `"${screen.screen_name ?? screen.screenName}" (ID: ${screen.screen_id}) 화면을 다른 회사로 복사합니다.`
? `"${screen.screen_name}" (ID: ${screen.screen_id}) 화면을 다른 회사로 복사합니다.`
: "화면을 선택해주세요."}
</DialogDescription>
</DialogHeader>
@@ -98,7 +98,7 @@ export function PopScreenPreview({ screen, className }: PopScreenPreviewProps) {
<h3 className="text-sm font-medium"></h3>
{screen && (
<span className="text-xs text-muted-foreground truncate max-w-[150px]">
{screen.screen_name ?? screen.screenName}
{screen.screen_name}
</span>
)}
</div>
@@ -88,7 +88,7 @@ export function PopScreenSettingModal({
if (!open || !screen) return;
// 화면 정보 설정
setScreenName((screen.screen_name ?? screen.screenName) || "");
setScreenName(screen.screen_name || "");
setScreenDescription(screen.description || "");
setScreenIcon("");
setSelectedCategoryId("");
@@ -172,7 +172,7 @@ export function PopScreenSettingModal({
};
// screen_definitions 테이블에 화면명/설명 업데이트
if (screenName !== (screen.screen_name ?? screen.screenName) || screenDescription !== (screen.description || "")) {
if (screenName !== (screen.screen_name || "") || screenDescription !== (screen.description || "")) {
await screenApi.updateScreenInfo(screen.screen_id, {
screenName,
description: screenDescription,
@@ -213,7 +213,7 @@ export function PopScreenSettingModal({
<DialogHeader className="px-6 pt-6 pb-4 shrink-0">
<DialogTitle className="text-base sm:text-lg">POP </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{screen.screen_name ?? screen.screenName} [{screen.screen_code ?? screen.screenCode}]
{screen.screen_name} [{screen.screen_code}]
</DialogDescription>
</DialogHeader>
@@ -28,7 +28,7 @@ export function AnimatedFlowEdge({
const strokeColor = (style?.stroke as string) || "hsl(var(--primary))";
const strokeW = (style?.strokeWidth as number) || 2;
const isActive = data?.active !== false;
const duration = data?.duration || "3s";
const duration = (data?.duration as string | undefined) || "3s";
const filterId = `edge-glow-${id}`;
return (
@@ -263,8 +263,8 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated, isPop
// 날짜 필드 보정
const mapped: ScreenDefinition = {
...created,
createdDate: created.createdDate ? new Date(created.createdDate as any) : new Date(),
updatedDate: created.updatedDate ? new Date(created.updatedDate as any) : new Date(),
createdDate: created.created_date ? new Date(created.created_date as any) : new Date(),
updatedDate: created.updated_date ? new Date(created.updated_date as any) : new Date(),
} as ScreenDefinition;
onCreated?.(mapped);
+1 -1
View File
@@ -84,7 +84,7 @@ const findSaveButtonInComponents = (components: any[]): any | null => {
export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const { user, userId, companyCode } = useAuth();
const tabId = useTabId();
const activeTabId = useTabStore((s) => s[s.mode].activeTabId);
const activeTabId = useTabStore((s) => s[s.mode].active_tab_id);
const isTabActive = !tabId || tabId === activeTabId;
const [modalState, setModalState] = useState<EditModalState>({
isOpen: false,
@@ -35,13 +35,13 @@ export const GridLayoutBuilder: React.FC<GridLayoutBuilderProps> = ({
(options?: CreateRowOptions) => {
const newRow: LayoutRow = {
id: `row-${Date.now()}`,
rowIndex: layout.rows.length,
row_index: layout.rows.length,
height: options?.height || "auto",
fixedHeight: options?.fixedHeight,
fixed_height: options?.fixed_height,
gap: options?.gap || "sm",
padding: options?.padding || "sm",
alignment: options?.alignment || "start",
verticalAlignment: "middle",
vertical_alignment: "middle",
components: [],
};
@@ -90,7 +90,7 @@ export const GridLayoutBuilder: React.FC<GridLayoutBuilderProps> = ({
// 인덱스 재정렬
updatedRows.forEach((row, index) => {
row.rowIndex = index;
row.row_index = index;
});
onUpdateLayout({
@@ -140,8 +140,8 @@ export const GridLayoutBuilder: React.FC<GridLayoutBuilderProps> = ({
// 글로벌 컨테이너 클래스
const globalContainerClasses = cn(
"mx-auto relative",
layout.globalSettings.containerMaxWidth === "full" ? "w-full" : `max-w-${layout.globalSettings.containerMaxWidth}`,
GAP_PRESETS[layout.globalSettings.containerPadding].class.replace("gap-", "px-"),
layout.global_settings.container_max_width === "full" ? "w-full" : `max-w-${layout.global_settings.container_max_width}`,
GAP_PRESETS[layout.global_settings.container_padding].class.replace("gap-", "px-"),
);
return (
@@ -254,7 +254,7 @@ export const GridLayoutBuilder: React.FC<GridLayoutBuilderProps> = ({
<span>: {layout.components.size}</span>
<span>
:{" "}
{layout.globalSettings.containerMaxWidth === "full" ? "전체" : layout.globalSettings.containerMaxWidth}
{layout.global_settings.container_max_width === "full" ? "전체" : layout.global_settings.container_max_width}
</span>
</div>
<div className="flex items-center gap-2">
@@ -80,7 +80,7 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
onGroupUngroup(selectedComponents[0].id);
onGroupStateChange({
...groupState,
selectedComponents: [],
selected_components: [],
isGrouping: false,
});
}
@@ -93,7 +93,7 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
setShowCreateDialog(false);
onGroupStateChange({
...groupState,
selectedComponents: [],
selected_components: [],
isGrouping: false,
});
}
@@ -169,7 +169,7 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
onClick={() =>
onGroupStateChange({
...groupState,
selectedComponents: [],
selected_components: [],
isGrouping: false,
})
}
@@ -8,7 +8,20 @@ import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { CheckSquare, Plus, Trash2 } from "lucide-react";
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
import { WidgetComponent, CheckboxTypeConfig } from "@/types/screen";
import { WidgetComponent } from "@/types/screen";
interface CheckboxTypeConfig {
label?: string;
checkedValue?: string;
uncheckedValue?: string;
defaultChecked?: boolean;
options: CheckboxOption[];
isGroup?: boolean;
groupLabel?: string;
required?: boolean;
readonly?: boolean;
inline?: boolean;
}
interface CheckboxOption {
label: string;
@@ -23,7 +36,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onUpdateProperty,
}) => {
const widget = component as WidgetComponent;
const config = (widget.webTypeConfig as CheckboxTypeConfig) || {};
const config = (widget.web_type_config as CheckboxTypeConfig) || {};
// 로컬 상태
const [localConfig, setLocalConfig] = useState<CheckboxTypeConfig>({
@@ -58,7 +71,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
const currentConfig = (widget.webTypeConfig as CheckboxTypeConfig) || {};
const currentConfig = (widget.web_type_config as CheckboxTypeConfig) || {};
setLocalConfig({
label: currentConfig.label || "",
checkedValue: currentConfig.checkedValue || "Y",
@@ -79,13 +92,13 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
uncheckedValue: currentConfig.uncheckedValue || "N",
groupLabel: currentConfig.groupLabel || "",
});
}, [widget.webTypeConfig]);
}, [widget.web_type_config]);
// 설정 업데이트 핸들러
const updateConfig = (field: keyof CheckboxTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
};
// 체크박스 유형 변경
@@ -132,7 +145,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 옵션 업데이트 완료 (onBlur)
const handleOptionBlur = () => {
onUpdateProperty("webTypeConfig", localConfig);
onUpdateProperty("web_type_config", localConfig);
};
return (
@@ -301,7 +314,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
newOptions[index] = { ...newOptions[index], checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
}}
/>
<Input
@@ -325,7 +338,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
newOptions[index] = { ...newOptions[index], disabled: !checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
}}
/>
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
@@ -9,7 +9,24 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Textarea } from "@/components/ui/textarea";
import { Code, Monitor, Moon, Sun } from "lucide-react";
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
import { WidgetComponent, CodeTypeConfig } from "@/types/screen";
import { WidgetComponent } from "@/types/screen";
interface CodeTypeConfig {
language?: string;
theme?: string;
showLineNumbers?: boolean;
wordWrap?: boolean;
fontSize?: number;
tabSize?: number;
readOnly?: boolean;
showMinimap?: boolean;
autoComplete?: boolean;
bracketMatching?: boolean;
defaultValue?: string;
placeholder?: string;
height?: number;
required?: boolean;
}
export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
component,
@@ -17,7 +34,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onUpdateProperty,
}) => {
const widget = component as WidgetComponent;
const config = (widget.webTypeConfig as CodeTypeConfig) || {};
const config = (widget.web_type_config as CodeTypeConfig) || {};
// 로컬 상태
const [localConfig, setLocalConfig] = useState<CodeTypeConfig>({
@@ -39,7 +56,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
const currentConfig = (widget.webTypeConfig as CodeTypeConfig) || {};
const currentConfig = (widget.web_type_config as CodeTypeConfig) || {};
setLocalConfig({
language: currentConfig.language || "javascript",
theme: currentConfig.theme || "light",
@@ -56,13 +73,13 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
height: currentConfig.height || 300,
required: currentConfig.required || false,
});
}, [widget.webTypeConfig]);
}, [widget.web_type_config]);
// 설정 업데이트 핸들러
const updateConfig = (field: keyof CodeTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
};
// 지원되는 언어 목록
@@ -38,7 +38,7 @@ const FilterItemCollapsible: React.FC<FilterItemCollapsibleProps> = ({
onRemove,
children,
}) => {
const [isOpen, setIsOpen] = useState(!filter.columnName); // 설정 안 된 필터는 열린 상태로
const [isOpen, setIsOpen] = useState(!filter.column_name); // 설정 안 된 필터는 열린 상태로
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
@@ -103,7 +103,7 @@ export function DataFilterConfigPanel({
config || {
enabled: false,
filters: [],
matchType: "all",
match_type: "all",
},
);
@@ -117,9 +117,9 @@ export function DataFilterConfigPanel({
// 🆕 기존 필터 중 카테고리 타입인 것들의 값을 로드
config.filters?.forEach((filter) => {
if (filter.valueType === "category" && filter.columnName) {
console.log("🔄 기존 카테고리 필터 감지, 값 로딩:", filter.columnName);
loadCategoryValues(filter.columnName);
if (filter.value_type === "category" && filter.column_name) {
console.log("🔄 기존 카테고리 필터 감지, 값 로딩:", filter.column_name);
loadCategoryValues(filter.column_name);
}
});
}
@@ -149,7 +149,7 @@ export function DataFilterConfigPanel({
console.log("📦 카테고리 값 로드 응답:", response);
if (response.success && response.data) {
if (response.success && 'data' in response && response.data) {
const values = response.data.map((item: any) => ({
value: item.value_code,
label: item.value_label,
@@ -174,7 +174,7 @@ export function DataFilterConfigPanel({
};
const handleMatchTypeChange = (matchType: "all" | "any") => {
const newConfig = { ...localConfig, matchType };
const newConfig = { ...localConfig, match_type: matchType };
setLocalConfig(newConfig);
onConfigChange(newConfig);
};
@@ -182,10 +182,10 @@ export function DataFilterConfigPanel({
const handleAddFilter = () => {
const newFilter: ColumnFilter = {
id: `filter-${Date.now()}`,
columnName: columns[0]?.columnName || "",
column_name: columns[0]?.column_name || "",
operator: "equals",
value: "",
valueType: "static",
value_type: "static",
};
const newConfig = {
...localConfig,
@@ -215,9 +215,9 @@ export function DataFilterConfigPanel({
// 선택된 컬럼의 input_type 찾기 (데이터베이스의 실제 input_type)
const getColumnInputType = (columnName: string) => {
const column = columns.find((col) => col.columnName === columnName);
const column = columns.find((col) => col.column_name === columnName);
// input_type (소문자) 필드 사용 - 이것이 실제 카테고리/엔티티 타입 정보
return column?.input_type || column?.webType || "text";
return column?.input_type || column?.web_type || "text";
};
// 카테고리/코드 타입인지 확인
@@ -247,7 +247,7 @@ export function DataFilterConfigPanel({
{localConfig.filters.length > 1 && (
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select value={localConfig.matchType} onValueChange={handleMatchTypeChange}>
<Select value={localConfig.match_type} onValueChange={handleMatchTypeChange}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
@@ -283,10 +283,10 @@ export function DataFilterConfigPanel({
// 컬럼 라벨 찾기
const columnLabel =
columns.find((c) => c.columnName === filter.columnName)?.columnLabel || filter.columnName;
columns.find((c) => c.column_name === filter.column_name)?.columnLabel || filter.column_name;
// 필터 요약 텍스트 생성
const filterSummary = filter.columnName
const filterSummary = filter.column_name
? `${columnLabel} ${operatorLabels[filter.operator] || filter.operator}${
filter.operator !== "is_null" && filter.operator !== "is_not_null" && filter.value
? ` ${filter.value}`
@@ -307,9 +307,9 @@ export function DataFilterConfigPanel({
<div>
<Label className="text-xs"></Label>
<Select
value={filter.columnName}
value={filter.column_name}
onValueChange={(value) => {
const column = columns.find((col) => col.columnName === value);
const column = columns.find((col) => col.column_name === value);
console.log("🔍 컬럼 선택:", {
columnName: value,
@@ -331,7 +331,7 @@ export function DataFilterConfigPanel({
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, columnName: value, valueType, value: "" } : f,
f.id === filter.id ? { ...f, column_name: value, value_type: valueType, value: "" } : f,
),
};
@@ -351,8 +351,8 @@ export function DataFilterConfigPanel({
</SelectTrigger>
<SelectContent>
{columns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
<SelectItem key={col.column_name} value={col.column_name}>
{col.display_name || col.column_name}
{(col.input_type === "category" || col.input_type === "code") && (
<span className="text-muted-foreground ml-2 text-xs">({col.input_type})</span>
)}
@@ -374,7 +374,7 @@ export function DataFilterConfigPanel({
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, operator: value, valueType: "dynamic", value: "TODAY" } : f,
f.id === filter.id ? { ...f, operator: value, value_type: "dynamic", value: "TODAY" } : f,
),
};
setLocalConfig(newConfig);
@@ -421,14 +421,14 @@ export function DataFilterConfigPanel({
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.rangeConfig?.startColumn || ""}
value={filter.range_config?.start_column || ""}
onValueChange={(value) => {
const newRangeConfig = {
...filter.rangeConfig,
startColumn: value,
endColumn: filter.rangeConfig?.endColumn || "",
...filter.range_config,
start_column: value,
end_column: filter.range_config?.end_column || "",
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
handleFilterChange(filter.id, "range_config", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
@@ -438,12 +438,12 @@ export function DataFilterConfigPanel({
{columns
.filter(
(col) =>
col.dataType?.toLowerCase().includes("date") ||
col.dataType?.toLowerCase().includes("time"),
col.data_type?.toLowerCase().includes("date") ||
col.data_type?.toLowerCase().includes("time"),
)
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
<SelectItem key={col.column_name} value={col.column_name}>
{col.display_name || col.column_name}
</SelectItem>
))}
</SelectContent>
@@ -452,14 +452,14 @@ export function DataFilterConfigPanel({
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.rangeConfig?.endColumn || ""}
value={filter.range_config?.end_column || ""}
onValueChange={(value) => {
const newRangeConfig = {
...filter.rangeConfig,
startColumn: filter.rangeConfig?.startColumn || "",
endColumn: value,
...filter.range_config,
start_column: filter.range_config?.start_column || "",
end_column: value,
};
handleFilterChange(filter.id, "rangeConfig", newRangeConfig);
handleFilterChange(filter.id, "range_config", newRangeConfig);
}}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
@@ -469,12 +469,12 @@ export function DataFilterConfigPanel({
{columns
.filter(
(col) =>
col.dataType?.toLowerCase().includes("date") ||
col.dataType?.toLowerCase().includes("time"),
col.data_type?.toLowerCase().includes("date") ||
col.data_type?.toLowerCase().includes("time"),
)
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
<SelectItem key={col.column_name} value={col.column_name}>
{col.display_name || col.column_name}
</SelectItem>
))}
</SelectContent>
@@ -484,18 +484,18 @@ export function DataFilterConfigPanel({
)}
{/* 값 타입 선택 (카테고리/코드 컬럼 또는 date_range_contains) */}
{(isCategoryOrCodeColumn(filter.columnName) || filter.operator === "date_range_contains") && (
{(isCategoryOrCodeColumn(filter.column_name) || filter.operator === "date_range_contains") && (
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.valueType}
value={filter.value_type}
onValueChange={(value: any) => {
// dynamic 선택 시 한 번에 valueType과 value를 설정
if (value === "dynamic" && filter.operator === "date_range_contains") {
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, valueType: value, value: "TODAY" } : f,
f.id === filter.id ? { ...f, value_type: value, value: "TODAY" } : f,
),
};
setLocalConfig(newConfig);
@@ -505,7 +505,7 @@ export function DataFilterConfigPanel({
const newConfig = {
...localConfig,
filters: localConfig.filters.map((f) =>
f.id === filter.id ? { ...f, valueType: value, value: "" } : f,
f.id === filter.id ? { ...f, value_type: value, value: "" } : f,
),
};
setLocalConfig(newConfig);
@@ -521,7 +521,7 @@ export function DataFilterConfigPanel({
{filter.operator === "date_range_contains" && (
<SelectItem value="dynamic"> ( )</SelectItem>
)}
{isCategoryOrCodeColumn(filter.columnName) && (
{isCategoryOrCodeColumn(filter.column_name) && (
<>
<SelectItem value="category"> </SelectItem>
<SelectItem value="code"> </SelectItem>
@@ -535,11 +535,11 @@ export function DataFilterConfigPanel({
{/* 값 입력 (NULL 체크 및 date_range_contains의 dynamic 제외) */}
{filter.operator !== "is_null" &&
filter.operator !== "is_not_null" &&
!(filter.operator === "date_range_contains" && filter.valueType === "dynamic") && (
!(filter.operator === "date_range_contains" && filter.value_type === "dynamic") && (
<div>
<Label className="text-xs"></Label>
{/* 카테고리 타입이고 값 타입이 category인 경우 셀렉트박스 */}
{filter.valueType === "category" && categoryValues[filter.columnName] ? (
{filter.value_type === "category" && categoryValues[filter.column_name] ? (
<Select
value={
filter.operator === "in" || filter.operator === "not_in"
@@ -569,11 +569,11 @@ export function DataFilterConfigPanel({
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue
placeholder={loadingCategories[filter.columnName] ? "로딩 중..." : "값 선택"}
placeholder={loadingCategories[filter.column_name] ? "로딩 중..." : "값 선택"}
/>
</SelectTrigger>
<SelectContent>
{categoryValues[filter.columnName].map((option) => (
{categoryValues[filter.column_name].map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -616,7 +616,7 @@ export function DataFilterConfigPanel({
/>
)}
<p className="text-muted-foreground mt-1 text-[10px]">
{filter.valueType === "category" && categoryValues[filter.columnName]
{filter.value_type === "category" && categoryValues[filter.column_name]
? "카테고리 값을 선택하세요"
: filter.operator === "in" || filter.operator === "not_in"
? "여러 값은 쉼표(,)로 구분하세요"
@@ -630,7 +630,7 @@ export function DataFilterConfigPanel({
)}
{/* date_range_contains의 dynamic 타입 안내 */}
{filter.operator === "date_range_contains" && filter.valueType === "dynamic" && (
{filter.operator === "date_range_contains" && filter.value_type === "dynamic" && (
<div className="rounded-md bg-primary/10 p-2">
<p className="text-[10px] text-primary"> .</p>
</div>
@@ -9,7 +9,17 @@ import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Radio, Plus, Trash2 } from "lucide-react";
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
import { WidgetComponent, RadioTypeConfig } from "@/types/screen";
import { WidgetComponent } from "@/types/screen";
interface RadioTypeConfig {
options: Array<{ label: string; value: string; disabled?: boolean }>;
groupName?: string;
defaultValue?: string;
required?: boolean;
readonly?: boolean;
inline?: boolean;
groupLabel?: string;
}
interface RadioOption {
label: string;
@@ -23,7 +33,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onUpdateProperty,
}) => {
const widget = component as WidgetComponent;
const config = (widget.webTypeConfig as RadioTypeConfig) || {};
const config = (widget.web_type_config as unknown as RadioTypeConfig) || {};
// 로컬 상태
const [localConfig, setLocalConfig] = useState<RadioTypeConfig>({
@@ -52,7 +62,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
const currentConfig = (widget.webTypeConfig as RadioTypeConfig) || {};
const currentConfig = (widget.web_type_config as RadioTypeConfig) || {};
setLocalConfig({
options: currentConfig.options || [
{ label: "옵션 1", value: "option1" },
@@ -71,13 +81,13 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
groupLabel: currentConfig.groupLabel || "",
groupName: currentConfig.groupName || "",
});
}, [widget.webTypeConfig]);
}, [widget.web_type_config]);
// 설정 업데이트 핸들러
const updateConfig = (field: keyof RadioTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
};
// 옵션 추가
@@ -124,7 +134,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 옵션 업데이트 완료 (onBlur)
const handleOptionBlur = () => {
onUpdateProperty("webTypeConfig", localConfig);
onUpdateProperty("web_type_config", localConfig);
};
// 벌크 옵션 추가
@@ -333,7 +343,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
newOptions[index] = { ...newOptions[index], disabled: !checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
onUpdateProperty("web_type_config", newConfig);
}}
/>
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">

Some files were not shown because too many files have changed in this diff Show More