[agent-pipeline] pipe-20260329112709-ncml round-1
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
// 레지스트리 통계
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("📋 변경이력이 없습니다.");
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+9
-10
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user