- Added new entries to .gitignore for multi-agent MCP task queue and related rules. - Removed "즉시 저장" (quick insert) options from the ScreenSettingModal and BasicTab components to streamline button configurations. - Cleaned up unused event options in the V2ButtonConfigPanel to enhance clarity and maintainability. These changes aim to improve project organization and simplify the user interface by eliminating redundant options.
474 lines
17 KiB
TypeScript
474 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { ComponentRendererProps } from "@/types/component";
|
|
import { DateInputConfig } from "./types";
|
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
|
import { AutoGenerationConfig } from "@/types/screen";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface DateInputComponentProps extends ComponentRendererProps {
|
|
config?: DateInputConfig;
|
|
value?: any; // 외부에서 전달받는 값
|
|
autoGeneration?: AutoGenerationConfig;
|
|
hidden?: boolean;
|
|
}
|
|
|
|
/**
|
|
* DateInput 컴포넌트
|
|
* date-input 컴포넌트입니다
|
|
*/
|
|
export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
|
component,
|
|
isDesignMode = false,
|
|
isSelected = false,
|
|
isInteractive = false,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
config,
|
|
className,
|
|
style,
|
|
formData,
|
|
onFormDataChange,
|
|
value: externalValue, // 외부에서 전달받은 값
|
|
autoGeneration,
|
|
hidden,
|
|
...props
|
|
}) => {
|
|
// 컴포넌트 설정
|
|
const componentConfig = {
|
|
...config,
|
|
...component.config,
|
|
} as DateInputConfig;
|
|
|
|
// 🎯 자동생성 상태 관리
|
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
|
|
|
// 자동생성 설정 (props 우선, 컴포넌트 설정 폴백)
|
|
const finalAutoGeneration = autoGeneration || component.autoGeneration;
|
|
const finalHidden = hidden !== undefined ? hidden : component.hidden;
|
|
|
|
// 자동생성 로직
|
|
useEffect(() => {
|
|
if (finalAutoGeneration?.enabled) {
|
|
const n = new Date();
|
|
const today = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, "0")}-${String(n.getDate()).padStart(2, "0")}`;
|
|
setAutoGeneratedValue(today);
|
|
|
|
// 인터랙티브 모드에서 폼 데이터에도 설정
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
onFormDataChange(component.columnName, today);
|
|
}
|
|
}
|
|
|
|
// 원래 자동생성 로직 (주석 처리)
|
|
/*
|
|
if (finalAutoGeneration?.enabled && finalAutoGeneration.type !== "none") {
|
|
const fieldName = component.columnName || component.id;
|
|
const generatedValue = AutoGenerationUtils.generateValue(finalAutoGeneration, fieldName);
|
|
|
|
console.log("🎯 DateInputComponent 자동생성 시도:", {
|
|
componentId: component.id,
|
|
fieldName,
|
|
type: finalAutoGeneration.type,
|
|
options: finalAutoGeneration.options,
|
|
generatedValue,
|
|
isInteractive,
|
|
isDesignMode,
|
|
});
|
|
|
|
if (generatedValue) {
|
|
console.log("✅ DateInputComponent 자동생성 성공:", generatedValue);
|
|
setAutoGeneratedValue(generatedValue);
|
|
|
|
// 인터랙티브 모드에서 폼 데이터 업데이트
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
const currentValue = formData?.[component.columnName];
|
|
if (!currentValue) {
|
|
console.log("📤 DateInputComponent -> onFormDataChange 호출:", component.columnName, generatedValue);
|
|
onFormDataChange(component.columnName, generatedValue);
|
|
} else {
|
|
console.log("⚠️ DateInputComponent 기존 값이 있어서 자동생성 스킵:", currentValue);
|
|
}
|
|
} else {
|
|
console.log("⚠️ DateInputComponent 자동생성 조건 불만족:", {
|
|
isInteractive,
|
|
hasOnFormDataChange: !!onFormDataChange,
|
|
hasColumnName: !!component.columnName,
|
|
});
|
|
}
|
|
} else {
|
|
console.log("❌ DateInputComponent 자동생성 실패: generatedValue가 null");
|
|
}
|
|
} else {
|
|
console.log("⚠️ DateInputComponent 자동생성 비활성화:", {
|
|
enabled: finalAutoGeneration?.enabled,
|
|
type: finalAutoGeneration?.type,
|
|
});
|
|
}
|
|
*/
|
|
}, [
|
|
finalAutoGeneration?.enabled,
|
|
finalAutoGeneration?.type,
|
|
finalAutoGeneration?.options,
|
|
component.id,
|
|
component.columnName,
|
|
isInteractive,
|
|
]);
|
|
|
|
// 날짜 값 계산 및 디버깅
|
|
const fieldName = component.columnName || component.id;
|
|
|
|
// 값 우선순위: externalValue > formData > autoGeneratedValue > component.value
|
|
let rawValue: any;
|
|
if (externalValue !== undefined) {
|
|
rawValue = externalValue;
|
|
} else if (isInteractive && formData && component.columnName && formData[component.columnName]) {
|
|
rawValue = formData[component.columnName];
|
|
} else if (autoGeneratedValue) {
|
|
rawValue = autoGeneratedValue;
|
|
} else {
|
|
rawValue = component.value;
|
|
}
|
|
|
|
// 날짜 형식 변환 함수 (HTML input[type="date"]는 YYYY-MM-DD 형식만 허용)
|
|
const formatDateForInput = (dateValue: any): string => {
|
|
if (!dateValue) return "";
|
|
|
|
const dateStr = String(dateValue);
|
|
|
|
// 이미 YYYY-MM-DD 형식인 경우
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
return dateStr;
|
|
}
|
|
|
|
// YYYY-MM-DD HH:mm:ss 형식에서 날짜 부분만 추출
|
|
if (/^\d{4}-\d{2}-\d{2}\s/.test(dateStr)) {
|
|
return dateStr.split(" ")[0];
|
|
}
|
|
|
|
// YYYY/MM/DD 형식
|
|
if (/^\d{4}\/\d{2}\/\d{2}$/.test(dateStr)) {
|
|
return dateStr.replace(/\//g, "-");
|
|
}
|
|
|
|
// MM/DD/YYYY 형식
|
|
if (/^\d{2}\/\d{2}\/\d{4}$/.test(dateStr)) {
|
|
const [month, day, year] = dateStr.split("/");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// DD-MM-YYYY 형식
|
|
if (/^\d{2}-\d{2}-\d{4}$/.test(dateStr)) {
|
|
const [day, month, year] = dateStr.split("-");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// ISO 8601 날짜 (2023-12-31T00:00:00.000Z 등)
|
|
// 🆕 UTC 시간을 로컬 시간으로 변환하여 날짜 추출 (타임존 이슈 해결)
|
|
if (/^\d{4}-\d{2}-\d{2}T/.test(dateStr)) {
|
|
const date = new Date(dateStr);
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// 다른 형식의 날짜 문자열이나 Date 객체 처리
|
|
try {
|
|
const date = new Date(dateValue);
|
|
if (isNaN(date.getTime())) {
|
|
console.warn("🚨 DateInputComponent - 유효하지 않은 날짜:", dateValue);
|
|
return "";
|
|
}
|
|
|
|
// YYYY-MM-DD 형식으로 변환 (로컬 시간대 사용)
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const formattedDate = `${year}-${month}-${day}`;
|
|
|
|
console.log("📅 날짜 형식 변환:", {
|
|
원본: dateValue,
|
|
변환후: formattedDate,
|
|
});
|
|
|
|
return formattedDate;
|
|
} catch (error) {
|
|
console.error("🚨 DateInputComponent - 날짜 변환 오류:", error, "원본:", dateValue);
|
|
return "";
|
|
}
|
|
};
|
|
|
|
const formattedValue = formatDateForInput(rawValue);
|
|
|
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
|
const componentStyle: React.CSSProperties = {
|
|
width: "100%",
|
|
height: "100%",
|
|
...component.style,
|
|
...style,
|
|
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
|
width: "100%",
|
|
};
|
|
|
|
// 디자인 모드 스타일
|
|
if (isDesignMode) {
|
|
componentStyle.border = "1px dashed hsl(var(--border))";
|
|
componentStyle.borderColor = isSelected ? "hsl(var(--ring))" : "hsl(var(--border))";
|
|
}
|
|
|
|
// 이벤트 핸들러
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
onClick?.();
|
|
};
|
|
|
|
// DOM에 전달하면 안 되는 React-specific props 필터링
|
|
const {
|
|
selectedScreen,
|
|
onZoneComponentDrop,
|
|
onZoneClick,
|
|
componentConfig: _componentConfig,
|
|
component: _component,
|
|
isSelected: _isSelected,
|
|
onClick: _onClick,
|
|
onDragStart: _onDragStart,
|
|
onDragEnd: _onDragEnd,
|
|
size: _size,
|
|
position: _position,
|
|
style: _style,
|
|
screenId: _screenId,
|
|
tableName: _tableName,
|
|
onRefresh: _onRefresh,
|
|
onClose: _onClose,
|
|
...domProps
|
|
} = props;
|
|
|
|
// DOM 안전한 props만 필터링
|
|
const safeDomProps = filterDOMProps(domProps);
|
|
|
|
// webType에 따른 실제 input type 결정
|
|
const webType = component.componentConfig?.webType || "date";
|
|
const inputType = (() => {
|
|
switch (webType) {
|
|
case "datetime":
|
|
return "datetime-local";
|
|
case "time":
|
|
return "time";
|
|
case "month":
|
|
return "month";
|
|
case "year":
|
|
return "number";
|
|
case "date":
|
|
default:
|
|
return "date";
|
|
}
|
|
})();
|
|
|
|
// daterange 시작일/종료일 분리 (최상위에서 계산)
|
|
const [dateRangeStart, dateRangeEnd] = React.useMemo(() => {
|
|
if (webType === "daterange" && typeof rawValue === "string" && rawValue.includes("~")) {
|
|
return rawValue.split("~").map((d) => d.trim());
|
|
}
|
|
return ["", ""];
|
|
}, [webType, rawValue]);
|
|
|
|
// 입력 필드에 직접 적용할 폰트 크기
|
|
const inputFontSize = component.style?.fontSize;
|
|
|
|
// daterange 타입 전용 UI
|
|
if (webType === "daterange") {
|
|
return (
|
|
<div className={`relative h-full w-full ${className || ""}`} {...safeDomProps}>
|
|
{/* 라벨 렌더링 */}
|
|
{component.label && component.style?.labelDisplay !== false && (
|
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
|
{component.label}
|
|
{component.required && <span className="text-destructive">*</span>}
|
|
</label>
|
|
)}
|
|
|
|
<div className="box-border flex h-full w-full items-center gap-2">
|
|
{/* 시작일 */}
|
|
<input
|
|
type="date"
|
|
value={dateRangeStart}
|
|
disabled={componentConfig.disabled || false}
|
|
readOnly={componentConfig.readonly || false}
|
|
onChange={(e) => {
|
|
const newStartDate = e.target.value;
|
|
const newValue = `${newStartDate} ~ ${dateRangeEnd}`;
|
|
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
onFormDataChange(component.columnName, newValue);
|
|
}
|
|
}}
|
|
className={cn(
|
|
"h-full min-h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
"placeholder:text-muted-foreground",
|
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
|
componentConfig.disabled
|
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
|
: "bg-background text-foreground",
|
|
"disabled:cursor-not-allowed",
|
|
)}
|
|
style={inputFontSize ? { fontSize: inputFontSize } : undefined}
|
|
/>
|
|
|
|
{/* 구분자 */}
|
|
<span className="text-muted-foreground text-base font-medium">~</span>
|
|
|
|
{/* 종료일 */}
|
|
<input
|
|
type="date"
|
|
value={dateRangeEnd}
|
|
disabled={componentConfig.disabled || false}
|
|
readOnly={componentConfig.readonly || false}
|
|
onChange={(e) => {
|
|
const newEndDate = e.target.value;
|
|
const newValue = `${dateRangeStart} ~ ${newEndDate}`;
|
|
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
onFormDataChange(component.columnName, newValue);
|
|
}
|
|
}}
|
|
className={cn(
|
|
"h-full min-h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
"placeholder:text-muted-foreground",
|
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
|
componentConfig.disabled
|
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
|
: "bg-background text-foreground",
|
|
"disabled:cursor-not-allowed",
|
|
)}
|
|
style={inputFontSize ? { fontSize: inputFontSize } : undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// year 타입 전용 UI (number input with YYYY format)
|
|
if (webType === "year") {
|
|
return (
|
|
<div className={`relative h-full w-full ${className || ""}`} {...safeDomProps}>
|
|
{/* 라벨 렌더링 */}
|
|
{component.label && component.style?.labelDisplay !== false && (
|
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
|
{component.label}
|
|
{component.required && <span className="text-destructive">*</span>}
|
|
</label>
|
|
)}
|
|
|
|
<input
|
|
type="number"
|
|
value={rawValue}
|
|
placeholder="YYYY"
|
|
min="1900"
|
|
max="2100"
|
|
disabled={componentConfig.disabled || false}
|
|
readOnly={componentConfig.readonly || false}
|
|
onChange={(e) => {
|
|
const year = e.target.value;
|
|
if (year.length <= 4) {
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
onFormDataChange(component.columnName, year);
|
|
}
|
|
}
|
|
}}
|
|
className={cn(
|
|
"box-border h-full min-h-full w-full rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
"placeholder:text-muted-foreground",
|
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
|
componentConfig.disabled
|
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
|
: "bg-background text-foreground",
|
|
"disabled:cursor-not-allowed",
|
|
)}
|
|
style={inputFontSize ? { fontSize: inputFontSize } : undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`relative h-full w-full ${className || ""}`} {...safeDomProps}>
|
|
{/* 라벨 렌더링 */}
|
|
{component.label && component.style?.labelDisplay !== false && (
|
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
|
{component.label}
|
|
{component.required && <span className="text-destructive">*</span>}
|
|
</label>
|
|
)}
|
|
|
|
<input
|
|
type={inputType}
|
|
value={formattedValue}
|
|
placeholder={
|
|
finalAutoGeneration?.enabled
|
|
? `자동생성: ${AutoGenerationUtils.getTypeDescription(finalAutoGeneration.type)}`
|
|
: componentConfig.placeholder || ""
|
|
}
|
|
disabled={componentConfig.disabled || false}
|
|
required={componentConfig.required || false}
|
|
readOnly={componentConfig.readonly || finalAutoGeneration?.enabled || false}
|
|
className={cn(
|
|
"box-border h-full min-h-full w-full rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
"placeholder:text-muted-foreground",
|
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
|
componentConfig.disabled
|
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
|
: "bg-background text-foreground",
|
|
"disabled:cursor-not-allowed",
|
|
)}
|
|
style={inputFontSize ? { fontSize: inputFontSize } : undefined}
|
|
onClick={handleClick}
|
|
onDragStart={onDragStart}
|
|
onDragEnd={onDragEnd}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
console.log("🎯 DateInputComponent onChange 호출:", {
|
|
componentId: component.id,
|
|
columnName: component.columnName,
|
|
newValue,
|
|
isInteractive,
|
|
hasOnFormDataChange: !!onFormDataChange,
|
|
hasOnChange: !!props.onChange,
|
|
});
|
|
|
|
// isInteractive 모드에서는 formData 업데이트
|
|
if (isInteractive && onFormDataChange && component.columnName) {
|
|
console.log(`📤 DateInputComponent -> onFormDataChange 호출: ${component.columnName} = "${newValue}"`);
|
|
onFormDataChange(component.columnName, newValue);
|
|
}
|
|
// 디자인 모드에서는 component.onChange 호출
|
|
else if (component.onChange) {
|
|
console.log(`📤 DateInputComponent -> component.onChange 호출: ${newValue}`);
|
|
component.onChange(newValue);
|
|
}
|
|
// props.onChange가 있으면 호출 (호환성)
|
|
else if (props.onChange) {
|
|
console.log(`📤 DateInputComponent -> props.onChange 호출: ${newValue}`);
|
|
props.onChange(newValue);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* DateInput 래퍼 컴포넌트
|
|
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
|
*/
|
|
export const DateInputWrapper: React.FC<DateInputComponentProps> = (props) => {
|
|
return <DateInputComponent {...props} />;
|
|
};
|