insert 매핑 시 날짜 기본값 설정
This commit is contained in:
parent
33600ce667
commit
44ed594dd7
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -22,116 +22,146 @@ interface WebTypeInputProps {
|
|||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
tableName?: string; // 테이블명을 별도로 전달받음
|
||||
}
|
||||
|
||||
export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onChange, className = "", placeholder }) => {
|
||||
export const WebTypeInput: React.FC<WebTypeInputProps> = ({
|
||||
column,
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
placeholder,
|
||||
tableName,
|
||||
}) => {
|
||||
const webType = column.webType || "text";
|
||||
const [entityOptions, setEntityOptions] = useState<EntityReferenceOption[]>([]);
|
||||
const [codeOptions, setCodeOptions] = useState<EntityReferenceOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// detailSettings 안전하게 파싱
|
||||
let detailSettings: any = {};
|
||||
let fallbackCodeCategory = "";
|
||||
// detailSettings 안전하게 파싱 (메모이제이션)
|
||||
const { detailSettings, fallbackCodeCategory } = useMemo(() => {
|
||||
let parsedSettings: Record<string, unknown> = {};
|
||||
let fallbackCategory = "";
|
||||
|
||||
if (column.detailSettings && typeof column.detailSettings === "string") {
|
||||
// JSON 형태인지 확인 ('{' 또는 '[' 로 시작하는지)
|
||||
const trimmed = column.detailSettings.trim();
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
try {
|
||||
detailSettings = JSON.parse(column.detailSettings);
|
||||
} catch (error) {
|
||||
console.warn(`detailSettings JSON 파싱 실패 (${column.columnName}):`, column.detailSettings, error);
|
||||
detailSettings = {};
|
||||
}
|
||||
} else {
|
||||
// JSON이 아닌 일반 문자열인 경우, code 타입이면 codeCategory로 사용
|
||||
if (webType === "code") {
|
||||
// "공통코드: 상태" 형태에서 실제 코드 추출 시도
|
||||
if (column.detailSettings.includes(":")) {
|
||||
const parts = column.detailSettings.split(":");
|
||||
if (parts.length >= 2) {
|
||||
fallbackCodeCategory = parts[1].trim();
|
||||
} else {
|
||||
fallbackCodeCategory = column.detailSettings;
|
||||
}
|
||||
} else {
|
||||
fallbackCodeCategory = column.detailSettings;
|
||||
if (column.detailSettings && typeof column.detailSettings === "string") {
|
||||
// JSON 형태인지 확인 ('{' 또는 '[' 로 시작하는지)
|
||||
const trimmed = column.detailSettings.trim();
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
try {
|
||||
parsedSettings = JSON.parse(column.detailSettings);
|
||||
} catch {
|
||||
parsedSettings = {};
|
||||
}
|
||||
console.log(`📝 detailSettings에서 codeCategory 추출: "${column.detailSettings}" -> "${fallbackCodeCategory}"`);
|
||||
} else {
|
||||
// JSON이 아닌 일반 문자열인 경우, code 타입이면 codeCategory로 사용
|
||||
if (webType === "code") {
|
||||
// "공통코드: 상태" 형태에서 실제 코드 추출 시도
|
||||
if (column.detailSettings.includes(":")) {
|
||||
const parts = column.detailSettings.split(":");
|
||||
if (parts.length >= 2) {
|
||||
fallbackCategory = parts[1].trim();
|
||||
} else {
|
||||
fallbackCategory = column.detailSettings;
|
||||
}
|
||||
} else {
|
||||
fallbackCategory = column.detailSettings;
|
||||
}
|
||||
}
|
||||
parsedSettings = {};
|
||||
}
|
||||
detailSettings = {};
|
||||
} else if (column.detailSettings && typeof column.detailSettings === "object") {
|
||||
parsedSettings = column.detailSettings;
|
||||
}
|
||||
} else if (column.detailSettings && typeof column.detailSettings === "object") {
|
||||
detailSettings = column.detailSettings;
|
||||
}
|
||||
|
||||
// Entity 타입일 때 참조 데이터 로드
|
||||
useEffect(() => {
|
||||
console.log("🔍 WebTypeInput useEffect:", {
|
||||
webType,
|
||||
columnName: column.columnName,
|
||||
tableName: column.tableName,
|
||||
referenceTable: column.referenceTable,
|
||||
displayColumn: column.displayColumn,
|
||||
codeCategory: column.codeCategory,
|
||||
detailSettings,
|
||||
fallbackCodeCategory,
|
||||
});
|
||||
return { detailSettings: parsedSettings, fallbackCodeCategory: fallbackCategory };
|
||||
}, [column.detailSettings, webType]);
|
||||
|
||||
// webType이 entity이거나, referenceTable이 있으면 entity로 처리
|
||||
if ((webType === "entity" || column.referenceTable) && column.tableName && column.columnName) {
|
||||
console.log("🚀 Entity 데이터 로드 시작:", column.tableName, column.columnName);
|
||||
loadEntityData();
|
||||
} else if (webType === "code" && (column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory)) {
|
||||
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
|
||||
console.log("🚀 Code 데이터 로드 시작:", codeCategory);
|
||||
loadCodeData();
|
||||
} else {
|
||||
console.log("❌ 조건 불충족 - API 호출 안함", {
|
||||
webType,
|
||||
hasReferenceTable: !!column.referenceTable,
|
||||
hasTableName: !!column.tableName,
|
||||
hasColumnName: !!column.columnName,
|
||||
hasCodeCategory: !!(column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory),
|
||||
});
|
||||
}
|
||||
}, [webType, column.tableName, column.columnName, column.codeCategory, column.referenceTable, fallbackCodeCategory]);
|
||||
|
||||
const loadEntityData = async () => {
|
||||
const loadEntityData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log("📡 Entity API 호출:", column.tableName, column.columnName);
|
||||
const data = await EntityReferenceAPI.getEntityReferenceData(column.tableName, column.columnName, { limit: 100 });
|
||||
console.log("✅ Entity API 응답:", data);
|
||||
const data = await EntityReferenceAPI.getEntityReferenceData(tableName!, column.columnName, { limit: 100 });
|
||||
setEntityOptions(data.options);
|
||||
} catch (error) {
|
||||
console.error("❌ 엔티티 참조 데이터 로드 실패:", error);
|
||||
} catch {
|
||||
setEntityOptions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [tableName, column.columnName]);
|
||||
|
||||
const loadCodeData = async () => {
|
||||
const loadCodeData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
|
||||
const codeCategory = column.codeCategory || (detailSettings.codeCategory as string) || fallbackCodeCategory;
|
||||
if (codeCategory) {
|
||||
console.log("📡 Code API 호출:", codeCategory);
|
||||
const data = await EntityReferenceAPI.getCodeReferenceData(codeCategory, { limit: 100 });
|
||||
console.log("✅ Code API 응답:", data);
|
||||
setCodeOptions(data.options);
|
||||
} else {
|
||||
console.warn("⚠️ codeCategory가 없어서 API 호출 안함");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("공통 코드 데이터 로드 실패:", error);
|
||||
} catch {
|
||||
setCodeOptions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [column.codeCategory, detailSettings.codeCategory, fallbackCodeCategory]);
|
||||
|
||||
// Entity 타입일 때 참조 데이터 로드
|
||||
useEffect(() => {
|
||||
// webType이 entity이거나, referenceTable이 있으면 entity로 처리
|
||||
if ((webType === "entity" || column.referenceTable) && tableName && column.columnName) {
|
||||
loadEntityData();
|
||||
} else if (webType === "code" && (column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory)) {
|
||||
loadCodeData();
|
||||
}
|
||||
}, [
|
||||
webType,
|
||||
tableName,
|
||||
column.columnName,
|
||||
column.codeCategory,
|
||||
column.referenceTable,
|
||||
fallbackCodeCategory,
|
||||
detailSettings.codeCategory,
|
||||
loadEntityData,
|
||||
loadCodeData,
|
||||
]);
|
||||
|
||||
// 날짜/시간 타입일 때 기본값으로 현재 날짜/시간 설정
|
||||
useEffect(() => {
|
||||
const dateTimeTypes = ["date", "datetime", "timestamp"];
|
||||
|
||||
// 컬럼명이나 데이터 타입으로 날짜 필드 판단
|
||||
const isDateColumn =
|
||||
dateTimeTypes.includes(webType) ||
|
||||
column.columnName?.toLowerCase().includes("date") ||
|
||||
column.columnName?.toLowerCase().includes("time") ||
|
||||
column.columnName === "regdate" ||
|
||||
column.columnName === "created_at" ||
|
||||
column.columnName === "updated_at";
|
||||
|
||||
if (isDateColumn && (!value || value === "")) {
|
||||
const now = new Date();
|
||||
let formattedValue = "";
|
||||
|
||||
if (webType === "date") {
|
||||
// 데이터베이스 타입이나 컬럼명으로 시간 포함 여부 판단
|
||||
const isTimestampType =
|
||||
column.dataType?.toLowerCase().includes("timestamp") ||
|
||||
column.columnName?.toLowerCase().includes("time") ||
|
||||
column.columnName === "regdate" ||
|
||||
column.columnName === "created_at" ||
|
||||
column.columnName === "updated_at";
|
||||
|
||||
if (isTimestampType) {
|
||||
formattedValue = format(now, "yyyy-MM-dd HH:mm:ss");
|
||||
} else {
|
||||
formattedValue = format(now, "yyyy-MM-dd");
|
||||
}
|
||||
} else {
|
||||
// 컬럼명 기반 판단 시에도 시간 포함
|
||||
formattedValue = format(now, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
onChange(formattedValue);
|
||||
}
|
||||
}, [webType, value, onChange, column.columnName, column.dataType]);
|
||||
|
||||
// 공통 props
|
||||
const commonProps = {
|
||||
|
|
@ -160,35 +190,63 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
type="number"
|
||||
placeholder={placeholder || "숫자 입력"}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
min={detailSettings.min}
|
||||
max={detailSettings.max}
|
||||
step={detailSettings.step || "any"}
|
||||
min={detailSettings.min as number}
|
||||
max={detailSettings.max as number}
|
||||
step={(detailSettings.step as string) || "any"}
|
||||
/>
|
||||
);
|
||||
|
||||
case "date":
|
||||
const dateValue = value ? new Date(value) : undefined;
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`justify-start text-left font-normal ${className} ${!value && "text-muted-foreground"}`}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? format(dateValue, "PPP", { locale: ko }) : placeholder || "날짜 선택"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateValue}
|
||||
onSelect={(date) => onChange(date ? format(date, "yyyy-MM-dd") : "")}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
// 데이터베이스 타입이나 컬럼명으로 시간 포함 여부 판단
|
||||
const isTimestampType =
|
||||
column.dataType?.toLowerCase().includes("timestamp") ||
|
||||
column.columnName?.toLowerCase().includes("time") ||
|
||||
column.columnName === "regdate" ||
|
||||
column.columnName === "created_at" ||
|
||||
column.columnName === "updated_at";
|
||||
|
||||
if (isTimestampType) {
|
||||
// timestamp 타입이면 datetime-local input 사용 (시간까지 입력 가능)
|
||||
const datetimeValue = value ? value.replace(" ", "T").substring(0, 16) : "";
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="datetime-local"
|
||||
value={datetimeValue}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
// datetime-local 형식 (YYYY-MM-DDTHH:mm)을 DB 형식 (YYYY-MM-DD HH:mm:ss)으로 변환
|
||||
const formattedValue = inputValue ? `${inputValue.replace("T", " ")}:00` : "";
|
||||
onChange(formattedValue);
|
||||
}}
|
||||
placeholder={placeholder || "날짜와 시간 선택"}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// 순수 date 타입이면 달력 팝업 사용
|
||||
const dateValue = value ? new Date(value) : undefined;
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`justify-start text-left font-normal ${className} ${!value && "text-muted-foreground"}`}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? format(dateValue, "PPP", { locale: ko }) : placeholder || "날짜 선택"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateValue}
|
||||
onSelect={(date) => onChange(date ? format(date, "yyyy-MM-dd") : "")}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
|
|
@ -196,19 +254,19 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
{...commonProps}
|
||||
placeholder={placeholder || "여러 줄 텍스트 입력"}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={detailSettings.rows || 3}
|
||||
rows={(detailSettings.rows as number) || 3}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
const selectOptions = detailSettings.options || [];
|
||||
const selectOptions = (detailSettings.options as { value: string; label?: string }[]) || [];
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onChange}>
|
||||
<SelectTrigger className={className}>
|
||||
<SelectValue placeholder={placeholder || "선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectOptions.map((option: any) => (
|
||||
{selectOptions.map((option: { value: string; label?: string }) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label || option.value}
|
||||
</SelectItem>
|
||||
|
|
@ -226,16 +284,16 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
onCheckedChange={(checked) => onChange(checked ? "true" : "false")}
|
||||
/>
|
||||
<Label htmlFor={`checkbox-${column.columnName}`} className="text-sm">
|
||||
{detailSettings.label || column.columnLabel || column.columnName}
|
||||
{(detailSettings.label as string) || column.columnLabel || column.columnName}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "radio":
|
||||
const radioOptions = detailSettings.options || [];
|
||||
const radioOptions = (detailSettings.options as { value: string; label?: string }[]) || [];
|
||||
return (
|
||||
<RadioGroup value={value || ""} onValueChange={onChange} className={className}>
|
||||
{radioOptions.map((option: any) => (
|
||||
{radioOptions.map((option: { value: string; label?: string }) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={option.value} id={`radio-${column.columnName}-${option.value}`} />
|
||||
<Label htmlFor={`radio-${column.columnName}-${option.value}`} className="text-sm">
|
||||
|
|
@ -248,7 +306,7 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
|
||||
case "code":
|
||||
// 공통코드 선택 - 실제 API에서 코드 목록 가져옴
|
||||
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
|
||||
const codeCategory = column.codeCategory || (detailSettings.codeCategory as string) || fallbackCodeCategory;
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onChange} disabled={loading}>
|
||||
<SelectTrigger className={className}>
|
||||
|
|
@ -273,7 +331,7 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
|
||||
case "entity":
|
||||
// 엔티티 참조 - 실제 참조 테이블에서 데이터 가져옴
|
||||
const referenceTable = column.referenceTable || (detailSettings as any).referenceTable;
|
||||
const referenceTable = column.referenceTable || (detailSettings.referenceTable as string);
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onChange} disabled={loading}>
|
||||
<SelectTrigger className={className}>
|
||||
|
|
@ -307,8 +365,8 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onCha
|
|||
onChange(file.name); // 실제로는 파일 업로드 처리 필요
|
||||
}
|
||||
}}
|
||||
accept={detailSettings.accept}
|
||||
multiple={detailSettings.multiple}
|
||||
accept={detailSettings.accept as string}
|
||||
multiple={detailSettings.multiple as boolean}
|
||||
/>
|
||||
{value && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ColumnInfo } from "@/lib/api/dataflow";
|
||||
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
||||
import { WebTypeInput } from "../condition/WebTypeInput";
|
||||
|
||||
interface ColumnMapping {
|
||||
toColumnName: string;
|
||||
|
|
@ -303,14 +304,12 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
|
|||
|
||||
{!isMapped && onDefaultValueChange && (
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
type={getInputTypeForDataType(column.dataType?.toLowerCase() || "string")}
|
||||
placeholder="기본값 입력..."
|
||||
<WebTypeInput
|
||||
column={column}
|
||||
value={mapping?.defaultValue || ""}
|
||||
onChange={(e) => onDefaultValueChange(column.columnName, e.target.value)}
|
||||
onChange={(value) => onDefaultValueChange(column.columnName, value)}
|
||||
className="h-6 border-gray-200 text-xs focus:border-green-400 focus:ring-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={isSelected || !!oppositeSelectedColumn}
|
||||
placeholder="기본값 입력..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue