insert 매핑 시 날짜 기본값 설정

This commit is contained in:
hyeonsu 2025-09-21 11:10:18 +09:00
parent 33600ce667
commit 44ed594dd7
2 changed files with 178 additions and 121 deletions

View File

@ -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">

View File

@ -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>
)}