Merge remote-tracking branch 'origin/main' into ksh

This commit is contained in:
SeongHyun Kim
2025-12-15 18:43:32 +09:00
3 changed files with 156 additions and 109 deletions

View File

@@ -919,21 +919,27 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
}
// 🆕 분할 패널 우측이면 screenContext.formData와 props.formData를 병합
// screenContext.formData: RepeaterFieldGroup 등 컴포넌트가 직접 업데이트한 데이터
// props.formData: 부모에서 전달된 폼 데이터
// 🆕 분할 패널 우측이면 여러 소스에서 formData를 병합
// 우선순위: props.formData > screenContext.formData > splitPanelParentData
const screenContextFormData = screenContext?.formData || {};
const propsFormData = formData || {};
// 병합: props.formData를 기본으로 하고, screenContext.formData로 오버라이드
// (RepeaterFieldGroup 데이터는 screenContext에 있음)
const effectiveFormData = { ...propsFormData, ...screenContextFormData };
// 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드
// (일반 폼 필드는 props.formData, RepeaterFieldGroup screenContext.formData에 있음)
let effectiveFormData = { ...propsFormData, ...screenContextFormData };
// 🆕 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용
if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) {
effectiveFormData = { ...splitPanelParentData };
console.log("🔍 [ButtonPrimary] 분할 패널 우측 - splitPanelParentData 사용:", Object.keys(effectiveFormData));
}
console.log("🔍 [ButtonPrimary] formData 선택:", {
hasScreenContextFormData: Object.keys(screenContextFormData).length > 0,
screenContextKeys: Object.keys(screenContextFormData),
hasPropsFormData: Object.keys(propsFormData).length > 0,
propsFormDataKeys: Object.keys(propsFormData),
hasSplitPanelParentData: !!splitPanelParentData && Object.keys(splitPanelParentData).length > 0,
splitPanelPosition,
effectiveFormDataKeys: Object.keys(effectiveFormData),
});

View File

@@ -53,7 +53,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 자동생성된 값 상태
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
// API 호출 중복 방지를 위한 ref
const isGeneratingRef = React.useRef(false);
const hasGeneratedRef = React.useRef(false);
@@ -104,7 +104,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
const currentFormValue = formData?.[component.columnName];
const currentComponentValue = component.value;
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
isGeneratingRef.current = true; // 생성 시작 플래그
@@ -145,7 +144,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
if (isInteractive && onFormDataChange && component.columnName) {
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
onFormDataChange(component.columnName, generatedValue);
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
@@ -181,12 +180,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
width: "100%",
// 숨김 기능: 편집 모드에서만 연하게 표시
...(isHidden &&
isDesignMode && {
opacity: 0.4,
backgroundColor: "hsl(var(--muted))",
pointerEvents: "auto",
}),
...(isHidden &&
isDesignMode && {
opacity: 0.4,
backgroundColor: "hsl(var(--muted))",
pointerEvents: "auto",
}),
};
// 디자인 모드 스타일
@@ -361,7 +360,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
@@ -386,15 +385,17 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
/>
{/* @ 구분자 */}
<span className="text-base font-medium text-muted-foreground">@</span>
<span className="text-muted-foreground text-base font-medium">@</span>
{/* 도메인 선택/입력 (Combobox) */}
<Popover open={emailDomainOpen} onOpenChange={setEmailDomainOpen}>
@@ -406,14 +407,18 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
disabled={componentConfig.disabled || false}
className={cn(
"flex h-full flex-1 items-center justify-between rounded-md border px-3 py-2 text-sm transition-all duration-200",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "cursor-not-allowed bg-muted text-muted-foreground opacity-50" : "bg-background text-foreground",
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
"hover:border-ring/80",
emailDomainOpen && "border-ring ring-2 ring-ring/50",
emailDomainOpen && "border-ring ring-ring/50 ring-2",
)}
>
<span className={cn("truncate", !emailDomain && "text-muted-foreground")}>{emailDomain || "도메인 선택"}</span>
<span className={cn("truncate", !emailDomain && "text-muted-foreground")}>
{emailDomain || "도메인 선택"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
@@ -470,7 +475,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
@@ -496,14 +501,16 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
/>
<span className="text-base font-medium text-muted-foreground">-</span>
<span className="text-muted-foreground text-base font-medium">-</span>
{/* 두 번째 부분 */}
<input
@@ -524,14 +531,16 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
/>
<span className="text-base font-medium text-muted-foreground">-</span>
<span className="text-muted-foreground text-base font-medium">-</span>
{/* 세 번째 부분 */}
<input
@@ -552,10 +561,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
/>
</div>
@@ -569,7 +580,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
@@ -591,10 +602,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full w-[100px] cursor-pointer rounded-md border px-2 py-2 text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
>
<option value="https://">https://</option>
@@ -619,10 +632,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
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",
)}
/>
</div>
@@ -636,7 +651,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
@@ -669,11 +684,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}}
className={cn(
"box-border h-full w-full max-w-full resize-none rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"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-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
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",
)}
/>
</div>
@@ -692,13 +709,15 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
{/* 수동/자동 모드 표시 배지 */}
{testAutoGeneration.enabled && testAutoGeneration.type === "numbering_rule" && isInteractive && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<span className={cn(
"text-[10px] px-2 py-0.5 rounded-full font-medium",
isManualMode
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
)}>
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 items-center gap-1">
<span
className={cn(
"rounded-full px-2 py-0.5 text-[10px] font-medium",
isManualMode
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
)}
>
{isManualMode ? "수동" : "자동"}
</span>
</div>
@@ -706,12 +725,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<input
type={inputType}
defaultValue={(() => {
value={(() => {
let displayValue = "";
if (isInteractive && formData && component.columnName) {
// 인터랙티브 모드: formData 우선, 없으면 자동생성 값
const rawValue = formData[component.columnName] || autoGeneratedValue || "";
const rawValue = formData[component.columnName] ?? autoGeneratedValue ?? "";
// 객체인 경우 빈 문자열로 변환 (에러 방지)
displayValue = typeof rawValue === "object" ? "" : String(rawValue);
} else {
@@ -724,31 +743,33 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
})()}
placeholder={
testAutoGeneration.enabled && testAutoGeneration.type !== "none"
? isManualMode
? isManualMode
? "수동 입력 모드"
: `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}`
: componentConfig.placeholder || defaultPlaceholder
}
pattern={validationPattern}
title={
webType === "tel"
? "전화번호 형식: 010-1234-5678"
webType === "tel"
? "전화번호 형식: 010-1234-5678"
: isManualMode
? `${component.label} (수동 입력 모드 - 채번 규칙 미적용)`
: component.label
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
: component.columnName || undefined
: component.label
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
: component.columnName || undefined
}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
readOnly={componentConfig.readonly || false}
className={cn(
"box-border h-full w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-all duration-200 outline-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"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-2 ring-ring/50" : "border-input",
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
"disabled:cursor-not-allowed"
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",
)}
onClick={(e) => {
handleClick(e);
@@ -774,9 +795,9 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
console.log("🔄 수동 모드로 전환:", {
field: component.columnName,
original: originalAutoGeneratedValue,
modified: newValue
modified: newValue,
});
// 🆕 채번 규칙 ID 제거 (수동 모드이므로 더 이상 채번 규칙 사용 안 함)
if (isInteractive && onFormDataChange && component.columnName) {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
@@ -789,9 +810,9 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
setIsManualMode(false);
console.log("🔄 자동 모드로 복구:", {
field: component.columnName,
value: newValue
value: newValue,
});
// 채번 규칙 ID 복구
if (isInteractive && onFormDataChange && component.columnName) {
const ruleId = testAutoGeneration.options?.numberingRuleId;