오류 수정

This commit is contained in:
kjs 2025-11-26 14:44:49 +09:00
parent 611fe9f788
commit 13fe9c97fe
10 changed files with 712 additions and 243 deletions

View File

@ -47,6 +47,7 @@ import dynamic from "next/dynamic";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { DynamicWebTypeRenderer } from "@/lib/registry"; import { DynamicWebTypeRenderer } from "@/lib/registry";
import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils";
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
// InteractiveScreenViewer를 동적으로 import (SSR 비활성화) // InteractiveScreenViewer를 동적으로 import (SSR 비활성화)
const InteractiveScreenViewer = dynamic( const InteractiveScreenViewer = dynamic(
@ -1315,15 +1316,16 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
<DialogHeader> <DialogHeader>
<DialogTitle> - {screenToPreview?.screenName}</DialogTitle> <DialogTitle> - {screenToPreview?.screenName}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6"> <TableOptionsProvider>
{isLoadingPreview ? ( <div className="flex flex-1 items-center justify-center overflow-hidden bg-gradient-to-br from-gray-50 to-slate-100 p-6">
<div className="flex h-full items-center justify-center"> {isLoadingPreview ? (
<div className="text-center"> <div className="flex h-full items-center justify-center">
<div className="mb-2 text-lg font-medium"> ...</div> <div className="text-center">
<div className="text-muted-foreground text-sm"> .</div> <div className="mb-2 text-lg font-medium"> ...</div>
<div className="text-muted-foreground text-sm"> .</div>
</div>
</div> </div>
</div> ) : previewLayout && previewLayout.components ? (
) : previewLayout && previewLayout.components ? (
(() => { (() => {
const screenWidth = previewLayout.screenResolution?.width || 1200; const screenWidth = previewLayout.screenResolution?.width || 1200;
const screenHeight = previewLayout.screenResolution?.height || 800; const screenHeight = previewLayout.screenResolution?.height || 800;
@ -1536,7 +1538,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
</div> </div>
</div> </div>
)} )}
</div> </div>
</TableOptionsProvider>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setPreviewDialogOpen(false)}> <Button variant="outline" onClick={() => setPreviewDialogOpen(false)}>

View File

@ -47,6 +47,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 새 옵션 추가용 상태 // 새 옵션 추가용 상태
const [newOptionLabel, setNewOptionLabel] = useState(""); const [newOptionLabel, setNewOptionLabel] = useState("");
const [newOptionValue, setNewOptionValue] = useState(""); const [newOptionValue, setNewOptionValue] = useState("");
// 입력 필드용 로컬 상태
const [localInputs, setLocalInputs] = useState({
label: config.label || "",
checkedValue: config.checkedValue || "Y",
uncheckedValue: config.uncheckedValue || "N",
groupLabel: config.groupLabel || "",
});
// 컴포넌트 변경 시 로컬 상태 동기화 // 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => { useEffect(() => {
@ -63,6 +71,14 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
readonly: currentConfig.readonly || false, readonly: currentConfig.readonly || false,
inline: currentConfig.inline !== false, inline: currentConfig.inline !== false,
}); });
// 입력 필드 로컬 상태도 동기화
setLocalInputs({
label: currentConfig.label || "",
checkedValue: currentConfig.checkedValue || "Y",
uncheckedValue: currentConfig.uncheckedValue || "N",
groupLabel: currentConfig.groupLabel || "",
});
}, [widget.webTypeConfig]); }, [widget.webTypeConfig]);
// 설정 업데이트 핸들러 // 설정 업데이트 핸들러
@ -107,11 +123,16 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("options", newOptions); updateConfig("options", newOptions);
}; };
// 옵션 업데이트 // 옵션 업데이트 (입력 필드용 - 로컬 상태만)
const updateOption = (index: number, field: keyof CheckboxOption, value: any) => { const updateOptionLocal = (index: number, field: keyof CheckboxOption, value: any) => {
const newOptions = [...localConfig.options]; const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], [field]: value }; newOptions[index] = { ...newOptions[index], [field]: value };
updateConfig("options", newOptions); setLocalConfig({ ...localConfig, options: newOptions });
};
// 옵션 업데이트 완료 (onBlur)
const handleOptionBlur = () => {
onUpdateProperty("webTypeConfig", localConfig);
}; };
return ( return (
@ -170,8 +191,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="label" id="label"
value={localConfig.label || ""} value={localInputs.label}
onChange={(e) => updateConfig("label", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, label: e.target.value })}
onBlur={() => updateConfig("label", localInputs.label)}
placeholder="체크박스 라벨" placeholder="체크박스 라벨"
className="text-xs" className="text-xs"
/> />
@ -184,8 +206,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="checkedValue" id="checkedValue"
value={localConfig.checkedValue || ""} value={localInputs.checkedValue}
onChange={(e) => updateConfig("checkedValue", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, checkedValue: e.target.value })}
onBlur={() => updateConfig("checkedValue", localInputs.checkedValue)}
placeholder="Y" placeholder="Y"
className="text-xs" className="text-xs"
/> />
@ -196,8 +219,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="uncheckedValue" id="uncheckedValue"
value={localConfig.uncheckedValue || ""} value={localInputs.uncheckedValue}
onChange={(e) => updateConfig("uncheckedValue", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, uncheckedValue: e.target.value })}
onBlur={() => updateConfig("uncheckedValue", localInputs.uncheckedValue)}
placeholder="N" placeholder="N"
className="text-xs" className="text-xs"
/> />
@ -229,8 +253,9 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="groupLabel" id="groupLabel"
value={localConfig.groupLabel || ""} value={localInputs.groupLabel}
onChange={(e) => updateConfig("groupLabel", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
placeholder="체크박스 그룹 제목" placeholder="체크박스 그룹 제목"
className="text-xs" className="text-xs"
/> />
@ -268,26 +293,40 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Label className="text-xs"> ({localConfig.options.length})</Label> <Label className="text-xs"> ({localConfig.options.length})</Label>
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{localConfig.options.map((option, index) => ( {localConfig.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 rounded border p-2"> <div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
<Switch <Switch
checked={option.checked || false} checked={option.checked || false}
onCheckedChange={(checked) => updateOption(index, "checked", checked)} onCheckedChange={(checked) => {
const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}}
/> />
<Input <Input
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
onBlur={handleOptionBlur}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
onBlur={handleOptionBlur}
placeholder="값" placeholder="값"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)} onCheckedChange={(checked) => {
const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], disabled: !checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}}
/> />
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs"> <Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />

View File

@ -51,32 +51,29 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newFieldName, setNewFieldName] = useState(""); const [newFieldName, setNewFieldName] = useState("");
const [newFieldLabel, setNewFieldLabel] = useState(""); const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldType, setNewFieldType] = useState("string"); const [newFieldType, setNewFieldType] = useState("string");
const [isUserEditing, setIsUserEditing] = useState(false);
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만) // 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => { useEffect(() => {
if (!isUserEditing) { const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {}; setLocalConfig({
setLocalConfig({ entityType: currentConfig.entityType || "",
entityType: currentConfig.entityType || "", displayFields: currentConfig.displayFields || [],
displayFields: currentConfig.displayFields || [], searchFields: currentConfig.searchFields || [],
searchFields: currentConfig.searchFields || [], valueField: currentConfig.valueField || "id",
valueField: currentConfig.valueField || "id", labelField: currentConfig.labelField || "name",
labelField: currentConfig.labelField || "name", multiple: currentConfig.multiple || false,
multiple: currentConfig.multiple || false, searchable: currentConfig.searchable !== false,
searchable: currentConfig.searchable !== false, placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
placeholder: currentConfig.placeholder || "엔티티를 선택하세요", emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다", pageSize: currentConfig.pageSize || 20,
pageSize: currentConfig.pageSize || 20, minSearchLength: currentConfig.minSearchLength || 1,
minSearchLength: currentConfig.minSearchLength || 1, defaultValue: currentConfig.defaultValue || "",
defaultValue: currentConfig.defaultValue || "", required: currentConfig.required || false,
required: currentConfig.required || false, readonly: currentConfig.readonly || false,
readonly: currentConfig.readonly || false, apiEndpoint: currentConfig.apiEndpoint || "",
apiEndpoint: currentConfig.apiEndpoint || "", filters: currentConfig.filters || {},
filters: currentConfig.filters || {}, });
}); }, [widget.webTypeConfig]);
}
}, [widget.webTypeConfig, isUserEditing]);
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등) // 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
const updateConfig = (field: keyof EntityTypeConfig, value: any) => { const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
@ -87,13 +84,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
// 입력 필드용 업데이트 (로컬 상태만) // 입력 필드용 업데이트 (로컬 상태만)
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => { const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
setIsUserEditing(true);
setLocalConfig({ ...localConfig, [field]: value }); setLocalConfig({ ...localConfig, [field]: value });
}; };
// 입력 완료 시 부모에게 전달 // 입력 완료 시 부모에게 전달
const handleInputBlur = () => { const handleInputBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig); onUpdateProperty("webTypeConfig", localConfig);
}; };
@ -121,17 +116,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("displayFields", newFields); updateConfig("displayFields", newFields);
}; };
// 필드 업데이트 (입력 중) // 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => { const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
setIsUserEditing(true);
const newFields = [...localConfig.displayFields]; const newFields = [...localConfig.displayFields];
newFields[index] = { ...newFields[index], [field]: value }; newFields[index] = { ...newFields[index], [field]: value };
setLocalConfig({ ...localConfig, displayFields: newFields }); setLocalConfig({ ...localConfig, displayFields: newFields });
}; };
// 필드 업데이트 완료 (onBlur) // 필드 업데이트 완료 (onBlur) - 부모에게 전달
const handleFieldBlur = () => { const handleFieldBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig); onUpdateProperty("webTypeConfig", localConfig);
}; };
@ -325,12 +318,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Label className="text-xs"> ({localConfig.displayFields.length})</Label> <Label className="text-xs"> ({localConfig.displayFields.length})</Label>
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{localConfig.displayFields.map((field, index) => ( {localConfig.displayFields.map((field, index) => (
<div key={index} className="flex items-center gap-2 rounded border p-2"> <div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
<Switch <Switch
checked={field.visible} checked={field.visible}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
updateDisplayField(index, "visible", checked); const newFields = [...localConfig.displayFields];
handleFieldBlur(); newFields[index] = { ...newFields[index], visible: checked };
const newConfig = { ...localConfig, displayFields: newFields };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}} }}
/> />
<Input <Input
@ -347,7 +343,16 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}> <Select
value={field.type}
onValueChange={(value) => {
const newFields = [...localConfig.displayFields];
newFields[index] = { ...newFields[index], type: value };
const newConfig = { ...localConfig, displayFields: newFields };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}}
>
<SelectTrigger className="w-24 text-xs"> <SelectTrigger className="w-24 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>

View File

@ -43,6 +43,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newOptionLabel, setNewOptionLabel] = useState(""); const [newOptionLabel, setNewOptionLabel] = useState("");
const [newOptionValue, setNewOptionValue] = useState(""); const [newOptionValue, setNewOptionValue] = useState("");
const [bulkOptions, setBulkOptions] = useState(""); const [bulkOptions, setBulkOptions] = useState("");
// 입력 필드용 로컬 상태
const [localInputs, setLocalInputs] = useState({
groupLabel: config.groupLabel || "",
groupName: config.groupName || "",
});
// 컴포넌트 변경 시 로컬 상태 동기화 // 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => { useEffect(() => {
@ -59,6 +65,12 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
inline: currentConfig.inline !== false, inline: currentConfig.inline !== false,
groupLabel: currentConfig.groupLabel || "", groupLabel: currentConfig.groupLabel || "",
}); });
// 입력 필드 로컬 상태도 동기화
setLocalInputs({
groupLabel: currentConfig.groupLabel || "",
groupName: currentConfig.groupName || "",
});
}, [widget.webTypeConfig]); }, [widget.webTypeConfig]);
// 설정 업데이트 핸들러 // 설정 업데이트 핸들러
@ -95,17 +107,24 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
} }
}; };
// 옵션 업데이트 // 옵션 업데이트 (입력 필드용 - 로컬 상태만)
const updateOption = (index: number, field: keyof RadioOption, value: any) => { const updateOptionLocal = (index: number, field: keyof RadioOption, value: any) => {
const newOptions = [...localConfig.options]; const newOptions = [...localConfig.options];
const oldValue = newOptions[index].value; const oldValue = newOptions[index].value;
newOptions[index] = { ...newOptions[index], [field]: value }; newOptions[index] = { ...newOptions[index], [field]: value };
updateConfig("options", newOptions);
// 값이 변경되고 해당 값이 기본값이었다면 기본값도 업데이트 // 값이 변경되고 해당 값이 기본값이었다면 기본값도 업데이트
const newConfig = { ...localConfig, options: newOptions };
if (field === "value" && localConfig.defaultValue === oldValue) { if (field === "value" && localConfig.defaultValue === oldValue) {
updateConfig("defaultValue", value); newConfig.defaultValue = value;
} }
setLocalConfig(newConfig);
};
// 옵션 업데이트 완료 (onBlur)
const handleOptionBlur = () => {
onUpdateProperty("webTypeConfig", localConfig);
}; };
// 벌크 옵션 추가 // 벌크 옵션 추가
@ -185,8 +204,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="groupLabel" id="groupLabel"
value={localConfig.groupLabel || ""} value={localInputs.groupLabel}
onChange={(e) => updateConfig("groupLabel", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, groupLabel: e.target.value })}
onBlur={() => updateConfig("groupLabel", localInputs.groupLabel)}
placeholder="라디오버튼 그룹 제목" placeholder="라디오버튼 그룹 제목"
className="text-xs" className="text-xs"
/> />
@ -198,8 +218,9 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="groupName" id="groupName"
value={localConfig.groupName || ""} value={localInputs.groupName}
onChange={(e) => updateConfig("groupName", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, groupName: e.target.value })}
onBlur={() => updateConfig("groupName", localInputs.groupName)}
placeholder="자동 생성 (필드명 기반)" placeholder="자동 생성 (필드명 기반)"
className="text-xs" className="text-xs"
/> />
@ -290,22 +311,30 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Label className="text-xs"> ({localConfig.options.length})</Label> <Label className="text-xs"> ({localConfig.options.length})</Label>
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{localConfig.options.map((option, index) => ( {localConfig.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 rounded border p-2"> <div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
<Input <Input
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
onBlur={handleOptionBlur}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
onBlur={handleOptionBlur}
placeholder="값" placeholder="값"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)} onCheckedChange={(checked) => {
const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], disabled: !checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}}
/> />
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs"> <Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />

View File

@ -44,6 +44,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newOptionLabel, setNewOptionLabel] = useState(""); const [newOptionLabel, setNewOptionLabel] = useState("");
const [newOptionValue, setNewOptionValue] = useState(""); const [newOptionValue, setNewOptionValue] = useState("");
const [bulkOptions, setBulkOptions] = useState(""); const [bulkOptions, setBulkOptions] = useState("");
// 입력 필드용 로컬 상태
const [localInputs, setLocalInputs] = useState({
placeholder: config.placeholder || "",
emptyMessage: config.emptyMessage || "",
});
// 컴포넌트 변경 시 로컬 상태 동기화 // 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => { useEffect(() => {
@ -61,6 +67,12 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
readonly: currentConfig.readonly || false, readonly: currentConfig.readonly || false,
emptyMessage: currentConfig.emptyMessage || "선택 가능한 옵션이 없습니다", emptyMessage: currentConfig.emptyMessage || "선택 가능한 옵션이 없습니다",
}); });
// 입력 필드 로컬 상태도 동기화
setLocalInputs({
placeholder: currentConfig.placeholder || "",
emptyMessage: currentConfig.emptyMessage || "",
});
}, [widget.webTypeConfig]); }, [widget.webTypeConfig]);
// 설정 업데이트 핸들러 // 설정 업데이트 핸들러
@ -91,11 +103,16 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("options", newOptions); updateConfig("options", newOptions);
}; };
// 옵션 업데이트 // 옵션 업데이트 (입력 필드용 - 로컬 상태만)
const updateOption = (index: number, field: keyof SelectOption, value: any) => { const updateOptionLocal = (index: number, field: keyof SelectOption, value: any) => {
const newOptions = [...localConfig.options]; const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], [field]: value }; newOptions[index] = { ...newOptions[index], [field]: value };
updateConfig("options", newOptions); setLocalConfig({ ...localConfig, options: newOptions });
};
// 옵션 업데이트 완료 (onBlur)
const handleOptionBlur = () => {
onUpdateProperty("webTypeConfig", localConfig);
}; };
// 벌크 옵션 추가 // 벌크 옵션 추가
@ -170,8 +187,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="placeholder" id="placeholder"
value={localConfig.placeholder || ""} value={localInputs.placeholder}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, placeholder: e.target.value })}
onBlur={() => updateConfig("placeholder", localInputs.placeholder)}
placeholder="선택하세요" placeholder="선택하세요"
className="text-xs" className="text-xs"
/> />
@ -183,8 +201,9 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
</Label> </Label>
<Input <Input
id="emptyMessage" id="emptyMessage"
value={localConfig.emptyMessage || ""} value={localInputs.emptyMessage}
onChange={(e) => updateConfig("emptyMessage", e.target.value)} onChange={(e) => setLocalInputs({ ...localInputs, emptyMessage: e.target.value })}
onBlur={() => updateConfig("emptyMessage", localInputs.emptyMessage)}
placeholder="선택 가능한 옵션이 없습니다" placeholder="선택 가능한 옵션이 없습니다"
className="text-xs" className="text-xs"
/> />
@ -285,22 +304,30 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Label className="text-xs"> ({localConfig.options.length})</Label> <Label className="text-xs"> ({localConfig.options.length})</Label>
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{localConfig.options.map((option, index) => ( {localConfig.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 rounded border p-2"> <div key={`${option.value}-${index}`} className="flex items-center gap-2 rounded border p-2">
<Input <Input
value={option.label} value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)} onChange={(e) => updateOptionLocal(index, "label", e.target.value)}
onBlur={handleOptionBlur}
placeholder="라벨" placeholder="라벨"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Input <Input
value={option.value} value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)} onChange={(e) => updateOptionLocal(index, "value", e.target.value)}
onBlur={handleOptionBlur}
placeholder="값" placeholder="값"
className="flex-1 text-xs" className="flex-1 text-xs"
/> />
<Switch <Switch
checked={!option.disabled} checked={!option.disabled}
onCheckedChange={(checked) => updateOption(index, "disabled", !checked)} onCheckedChange={(checked) => {
const newOptions = [...localConfig.options];
newOptions[index] = { ...newOptions[index], disabled: !checked };
const newConfig = { ...localConfig, options: newOptions };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
}}
/> />
<Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs"> <Button size="sm" variant="destructive" onClick={() => removeOption(index)} className="p-1 text-xs">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />

View File

@ -78,3 +78,4 @@ export const numberingRuleTemplate = {

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useMemo } from "react"; import React, { useState, useMemo, useEffect } from "react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -34,6 +34,21 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
}) => { }) => {
const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []); const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []);
const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({}); const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
// 로컬 입력 상태 (각 필드의 라벨, placeholder 등)
const [localInputs, setLocalInputs] = useState<Record<number, { label: string; placeholder: string }>>({});
// 설정 입력 필드의 로컬 상태
const [localConfigInputs, setLocalConfigInputs] = useState({
addButtonText: config.addButtonText || "",
});
// config 변경 시 로컬 상태 동기화
useEffect(() => {
setLocalConfigInputs({
addButtonText: config.addButtonText || "",
});
}, [config.addButtonText]);
// 이미 사용된 컬럼명 목록 // 이미 사용된 컬럼명 목록
const usedColumnNames = useMemo(() => { const usedColumnNames = useMemo(() => {
@ -72,7 +87,32 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
handleFieldsChange(localFields.filter((_, i) => i !== index)); handleFieldsChange(localFields.filter((_, i) => i !== index));
}; };
// 필드 수정 // 필드 수정 (입력 중 - 로컬 상태만)
const updateFieldLocal = (index: number, field: 'label' | 'placeholder', value: string) => {
setLocalInputs(prev => ({
...prev,
[index]: {
...prev[index],
[field]: value
}
}));
};
// 필드 수정 완료 (onBlur - 실제 업데이트)
const handleFieldBlur = (index: number) => {
const localInput = localInputs[index];
if (localInput) {
const newFields = [...localFields];
newFields[index] = {
...newFields[index],
label: localInput.label,
placeholder: localInput.placeholder
};
handleFieldsChange(newFields);
}
};
// 필드 수정 (즉시 반영 - 드롭다운, 체크박스 등)
const updateField = (index: number, updates: Partial<RepeaterFieldDefinition>) => { const updateField = (index: number, updates: Partial<RepeaterFieldDefinition>) => {
const newFields = [...localFields]; const newFields = [...localFields];
newFields[index] = { ...newFields[index], ...updates }; newFields[index] = { ...newFields[index], ...updates };
@ -157,7 +197,7 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
<Label className="text-sm font-semibold"> </Label> <Label className="text-sm font-semibold"> </Label>
{localFields.map((field, index) => ( {localFields.map((field, index) => (
<Card key={index} className="border-2"> <Card key={`${field.name}-${index}`} className="border-2">
<CardContent className="space-y-3 pt-4"> <CardContent className="space-y-3 pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-700"> {index + 1}</span> <span className="text-sm font-semibold text-gray-700"> {index + 1}</span>
@ -200,6 +240,14 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
label: column.columnLabel || column.columnName, label: column.columnLabel || column.columnName,
type: (column.widgetType as RepeaterFieldType) || "text", type: (column.widgetType as RepeaterFieldType) || "text",
}); });
// 로컬 입력 상태도 업데이트
setLocalInputs(prev => ({
...prev,
[index]: {
label: column.columnLabel || column.columnName,
placeholder: prev[index]?.placeholder || ""
}
}));
setFieldNamePopoverOpen({ ...fieldNamePopoverOpen, [index]: false }); setFieldNamePopoverOpen({ ...fieldNamePopoverOpen, [index]: false });
}} }}
className="text-xs" className="text-xs"
@ -225,8 +273,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs"></Label> <Label className="text-xs"></Label>
<Input <Input
value={field.label} value={localInputs[index]?.label !== undefined ? localInputs[index].label : field.label}
onChange={(e) => updateField(index, { label: e.target.value })} onChange={(e) => updateFieldLocal(index, 'label', e.target.value)}
onBlur={() => handleFieldBlur(index)}
placeholder="필드 라벨" placeholder="필드 라벨"
className="h-8 w-full text-xs" className="h-8 w-full text-xs"
/> />
@ -258,10 +307,11 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs">Placeholder</Label> <Label className="text-xs">Placeholder</Label>
<Input <Input
value={field.placeholder || ""} value={localInputs[index]?.placeholder !== undefined ? localInputs[index].placeholder : (field.placeholder || "")}
onChange={(e) => updateField(index, { placeholder: e.target.value })} onChange={(e) => updateFieldLocal(index, 'placeholder', e.target.value)}
onBlur={() => handleFieldBlur(index)}
placeholder="입력 안내" placeholder="입력 안내"
className="h-8 w-full" className="h-8 w-full text-xs"
/> />
</div> </div>
</div> </div>
@ -329,8 +379,9 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
<Input <Input
id="repeater-add-button-text" id="repeater-add-button-text"
type="text" type="text"
value={config.addButtonText || ""} value={localConfigInputs.addButtonText}
onChange={(e) => handleChange("addButtonText", e.target.value)} onChange={(e) => setLocalConfigInputs({ ...localConfigInputs, addButtonText: e.target.value })}
onBlur={() => handleChange("addButtonText", localConfigInputs.addButtonText)}
placeholder="항목 추가" placeholder="항목 추가"
className="h-8" className="h-8"
/> />

View File

@ -20,6 +20,25 @@ export async function getCategoryColumns(tableName: string) {
} }
} }
/**
*
*
* @param menuObjid OBJID
* @returns
*/
export async function getCategoryColumnsByMenu(menuObjid: number) {
try {
const response = await apiClient.get<{
success: boolean;
data: CategoryColumn[];
}>(`/table-management/menu/${menuObjid}/category-columns`);
return response.data;
} catch (error: any) {
console.error("메뉴별 카테고리 컬럼 조회 실패:", error);
return { success: false, error: error.message };
}
}
/** /**
* ( ) * ( )
* *

View File

@ -15,14 +15,14 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react"; import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getSecondLevelMenus, getCategoryColumns, getCategoryValues } from "@/lib/api/tableCategoryValue"; import { getSecondLevelMenus, getCategoryColumns, getCategoryColumnsByMenu, getCategoryValues } from "@/lib/api/tableCategoryValue";
import { CalculationBuilder } from "./CalculationBuilder"; import { CalculationBuilder } from "./CalculationBuilder";
export interface SelectedItemsDetailInputConfigPanelProps { export interface SelectedItemsDetailInputConfigPanelProps {
config: SelectedItemsDetailInputConfig; config: SelectedItemsDetailInputConfig;
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void; onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼 sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string }>; // 🆕 원본 테이블 컬럼 (inputType 추가)
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼 targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string; codeCategory?: string }>; // 🆕 대상 테이블 컬럼 (inputType, codeCategory 추가)
allTables?: Array<{ tableName: string; displayName?: string }>; allTables?: Array<{ tableName: string; displayName?: string }>;
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용) screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백 onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
@ -53,9 +53,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
// 🆕 그룹 입력값을 위한 로컬 상태 (포커스 유지용) // 🆕 그룹 입력값을 위한 로컬 상태 (포커스 유지용)
const [localGroupInputs, setLocalGroupInputs] = useState<Record<string, { id?: string; title?: string; description?: string; order?: number }>>({}); const [localGroupInputs, setLocalGroupInputs] = useState<Record<string, { id?: string; title?: string; description?: string; order?: number }>>({});
// 🆕 필드 입력값을 위한 로컬 상태 (포커스 유지용)
const [localFieldInputs, setLocalFieldInputs] = useState<Record<number, { label?: string; placeholder?: string }>>({});
// 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용) // 🆕 표시 항목의 입력값을 위한 로컬 상태 (포커스 유지용)
const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState<Record<string, Record<number, { label?: string; value?: string }>>>({}); const [localDisplayItemInputs, setLocalDisplayItemInputs] = useState<Record<string, Record<number, { label?: string; value?: string }>>>({});
// 🆕 부모 데이터 매핑의 기본값 입력을 위한 로컬 상태 (포커스 유지용)
const [localMappingInputs, setLocalMappingInputs] = useState<Record<number, string>>({});
// 🆕 그룹별 펼침/접힘 상태 // 🆕 그룹별 펼침/접힘 상태
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({}); const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
@ -63,6 +69,13 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태 // 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({}); const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
// 🆕 카테고리 매핑 아코디언 펼침/접힘 상태
const [expandedCategoryMappings, setExpandedCategoryMappings] = useState<Record<string, boolean>>({
discountType: false,
roundingType: false,
roundingUnit: false,
});
// 🆕 원본 테이블 선택 상태 // 🆕 원본 테이블 선택 상태
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false); const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
const [sourceTableSearchValue, setSourceTableSearchValue] = useState(""); const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
@ -83,8 +96,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
const [autoFillTableColumns, setAutoFillTableColumns] = useState<Record<number, Array<{ columnName: string; columnLabel?: string; dataType?: string }>>>({}); const [autoFillTableColumns, setAutoFillTableColumns] = useState<Record<number, Array<{ columnName: string; columnLabel?: string; dataType?: string }>>>({});
// 🆕 원본/대상 테이블 컬럼 상태 (내부에서 로드) // 🆕 원본/대상 테이블 컬럼 상태 (내부에서 로드)
const [loadedSourceTableColumns, setLoadedSourceTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string }>>([]); const [loadedSourceTableColumns, setLoadedSourceTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string }>>([]);
const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string }>>([]); const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState<Array<{ columnName: string; columnLabel?: string; dataType?: string; inputType?: string; codeCategory?: string }>>([]);
// 🆕 원본 테이블 컬럼 로드 // 🆕 원본 테이블 컬럼 로드
useEffect(() => { useEffect(() => {
@ -105,6 +118,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
columnName: col.columnName, columnName: col.columnName,
columnLabel: col.displayName || col.columnLabel || col.columnName, columnLabel: col.displayName || col.columnLabel || col.columnName,
dataType: col.dataType, dataType: col.dataType,
inputType: col.inputType, // 🔧 inputType 추가
}))); })));
console.log("✅ 원본 테이블 컬럼 로드 성공:", columns.length); console.log("✅ 원본 테이블 컬럼 로드 성공:", columns.length);
} }
@ -135,6 +149,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
columnName: col.columnName, columnName: col.columnName,
columnLabel: col.displayName || col.columnLabel || col.columnName, columnLabel: col.displayName || col.columnLabel || col.columnName,
dataType: col.dataType, dataType: col.dataType,
inputType: col.inputType, // 🔧 inputType 추가
}))); })));
console.log("✅ 대상 테이블 컬럼 로드 성공:", columns.length); console.log("✅ 대상 테이블 컬럼 로드 성공:", columns.length);
} }
@ -165,6 +180,18 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
}); });
return newInputs; return newInputs;
}); });
// 🔧 표시 항목이 있는 그룹은 아코디언을 열린 상태로 초기화
setExpandedDisplayItems(prev => {
const newExpanded = { ...prev };
(config.fieldGroups || []).forEach(group => {
// 이미 상태가 있으면 유지, 없으면 displayItems가 있을 때만 열기
if (!(group.id in newExpanded) && group.displayItems && group.displayItems.length > 0) {
newExpanded[group.id] = true;
}
});
return newExpanded;
});
}, [config.fieldGroups]); }, [config.fieldGroups]);
// 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드 // 🆕 초기 렌더링 시 기존 필드들의 autoFillFromTable 컬럼 로드
@ -238,6 +265,36 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
} }
}; };
// 🆕 저장된 부모 데이터 매핑의 컬럼 자동 로드
useEffect(() => {
const loadSavedMappingColumns = async () => {
if (!config.parentDataMapping || config.parentDataMapping.length === 0) {
console.log("📭 [부모 데이터 매핑] 매핑이 없습니다");
return;
}
console.log("🔍 [부모 데이터 매핑] 저장된 매핑 컬럼 자동 로드 시작:", config.parentDataMapping.length);
for (let i = 0; i < config.parentDataMapping.length; i++) {
const mapping = config.parentDataMapping[i];
// 이미 로드된 컬럼이 있으면 스킵
if (mappingSourceColumns[i] && mappingSourceColumns[i].length > 0) {
console.log(`⏭️ [매핑 ${i}] 이미 로드된 컬럼이 있음`);
continue;
}
// 소스 테이블이 선택되어 있으면 컬럼 로드
if (mapping.sourceTable) {
console.log(`📡 [매핑 ${i}] 소스 테이블 컬럼 자동 로드:`, mapping.sourceTable);
await loadMappingSourceColumns(mapping.sourceTable, i);
}
}
};
loadSavedMappingColumns();
}, [config.parentDataMapping]);
// 2레벨 메뉴 목록 로드 // 2레벨 메뉴 목록 로드
useEffect(() => { useEffect(() => {
const loadMenus = async () => { const loadMenus = async () => {
@ -251,26 +308,39 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
// 메뉴 선택 시 카테고리 목록 로드 // 메뉴 선택 시 카테고리 목록 로드
const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => { const handleMenuSelect = async (menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
if (!config.targetTable) { console.log("🔍 [handleMenuSelect] 시작", { menuObjid, fieldType });
console.warn("⚠️ targetTable이 설정되지 않았습니다");
return;
}
console.log("🔍 카테고리 목록 로드 시작", { targetTable: config.targetTable, menuObjid, fieldType }); // 🔧 1단계: 아코디언 먼저 열기 (리렌더링 전에)
setExpandedCategoryMappings(prev => {
const newState = { ...prev, [fieldType]: true };
console.log("🔄 [handleMenuSelect] 아코디언 열기:", newState);
return newState;
});
const response = await getCategoryColumns(config.targetTable); // 🔧 2단계: 메뉴별 카테고리 컬럼 API 호출
const response = await getCategoryColumnsByMenu(menuObjid);
console.log("📥 getCategoryColumns 응답:", response); console.log("📥 [handleMenuSelect] API 응답:", response);
if (response.success && response.data) { if (response.success && response.data) {
console.log("✅ 카테고리 컬럼 데이터:", response.data); console.log("✅ [handleMenuSelect] 카테고리 컬럼 데이터:", {
setCategoryColumns(prev => ({ ...prev, [fieldType]: response.data })); fieldType,
columns: response.data,
count: response.data.length
});
// 카테고리 컬럼 상태 업데이트
setCategoryColumns(prev => {
const newState = { ...prev, [fieldType]: response.data };
console.log("🔄 [handleMenuSelect] categoryColumns 업데이트:", newState);
return newState;
});
} else { } else {
console.error("❌ 카테고리 컬럼 로드 실패:", response); console.error("❌ [handleMenuSelect] 카테고리 컬럼 로드 실패:", response);
} }
// valueMapping 업데이트 // 🔧 3단계: valueMapping 업데이트 (마지막에)
handleChange("autoCalculation", { const newConfig = {
...config.autoCalculation, ...config.autoCalculation,
valueMapping: { valueMapping: {
...config.autoCalculation.valueMapping, ...config.autoCalculation.valueMapping,
@ -279,20 +349,50 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
[fieldType]: menuObjid, [fieldType]: menuObjid,
}, },
}, },
}); };
console.log("🔄 [handleMenuSelect] valueMapping 업데이트:", newConfig);
handleChange("autoCalculation", newConfig);
}; };
// 카테고리 선택 시 카테고리 값 목록 로드 // 카테고리 선택 시 카테고리 값 목록 로드
const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => { const handleCategorySelect = async (columnName: string, menuObjid: number, fieldType: "discountType" | "roundingType" | "roundingUnit") => {
if (!config.targetTable) return; console.log("🔍 [handleCategorySelect] 시작", { columnName, menuObjid, fieldType, targetTable: config.targetTable });
const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid); if (!config.targetTable) {
if (response.success && response.data) { console.warn("⚠️ [handleCategorySelect] targetTable이 없습니다");
setCategoryValues(prev => ({ ...prev, [fieldType]: response.data })); return;
} }
const response = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
console.log("📥 [handleCategorySelect] API 응답:", response);
if (response.success && response.data) {
console.log("✅ [handleCategorySelect] 카테고리 값 데이터:", {
fieldType,
values: response.data,
count: response.data.length
});
setCategoryValues(prev => {
const newState = { ...prev, [fieldType]: response.data };
console.log("🔄 [handleCategorySelect] categoryValues 업데이트:", newState);
return newState;
});
} else {
console.error("❌ [handleCategorySelect] 카테고리 값 로드 실패:", response);
}
// 🔧 카테고리 선택 시 아코디언 열기 (이미 열려있을 수도 있음)
setExpandedCategoryMappings(prev => {
const newState = { ...prev, [fieldType]: true };
console.log("🔄 [handleCategorySelect] 아코디언 상태:", newState);
return newState;
});
// valueMapping 업데이트 // valueMapping 업데이트
handleChange("autoCalculation", { const newConfig = {
...config.autoCalculation, ...config.autoCalculation,
valueMapping: { valueMapping: {
...config.autoCalculation.valueMapping, ...config.autoCalculation.valueMapping,
@ -301,9 +401,99 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
[fieldType]: columnName, [fieldType]: columnName,
}, },
}, },
}); };
console.log("🔄 [handleCategorySelect] valueMapping 업데이트:", newConfig);
handleChange("autoCalculation", newConfig);
}; };
// 🆕 저장된 설정에서 카테고리 정보 복원
useEffect(() => {
const loadSavedCategories = async () => {
console.log("🔍 [loadSavedCategories] useEffect 실행", {
hasTargetTable: !!config.targetTable,
hasAutoCalc: !!config.autoCalculation,
hasValueMapping: !!config.autoCalculation?.valueMapping
});
if (!config.targetTable || !config.autoCalculation?.valueMapping) {
console.warn("⚠️ [loadSavedCategories] targetTable 또는 valueMapping이 없어 종료");
return;
}
const savedMenus = (config.autoCalculation.valueMapping as any)?._selectedMenus;
const savedCategories = (config.autoCalculation.valueMapping as any)?._selectedCategories;
console.log("🔄 [loadSavedCategories] 저장된 카테고리 설정 복원 시작:", { savedMenus, savedCategories });
// 각 필드 타입별로 저장된 카테고리 값 로드
const fieldTypes: Array<"discountType" | "roundingType" | "roundingUnit"> = ["discountType", "roundingType", "roundingUnit"];
// 🔧 복원할 아코디언 상태 준비
const newExpandedState: Record<string, boolean> = {};
for (const fieldType of fieldTypes) {
const menuObjid = savedMenus?.[fieldType];
const columnName = savedCategories?.[fieldType];
console.log(`🔍 [loadSavedCategories] ${fieldType} 처리`, { menuObjid, columnName });
// 🔧 메뉴만 선택된 경우에도 카테고리 컬럼 로드
if (menuObjid) {
console.log(`✅ [loadSavedCategories] ${fieldType} 메뉴 발견, 카테고리 컬럼 로드 시작:`, { menuObjid });
// 🔧 메뉴가 선택되어 있으면 아코디언 열기
newExpandedState[fieldType] = true;
// 🔧 메뉴별 카테고리 컬럼 로드 (카테고리 선택 여부와 무관)
console.log(`📡 [loadSavedCategories] ${fieldType} 카테고리 컬럼 API 호출`, { menuObjid });
const columnsResponse = await getCategoryColumnsByMenu(menuObjid);
console.log(`📥 [loadSavedCategories] ${fieldType} 컬럼 응답:`, columnsResponse);
if (columnsResponse.success && columnsResponse.data) {
setCategoryColumns(prev => {
const newState = { ...prev, [fieldType]: columnsResponse.data };
console.log(`🔄 [loadSavedCategories] ${fieldType} categoryColumns 업데이트:`, newState);
return newState;
});
} else {
console.error(`❌ [loadSavedCategories] ${fieldType} 컬럼 로드 실패:`, columnsResponse);
}
// 🔧 카테고리까지 선택된 경우에만 값 로드
if (columnName) {
console.log(`📡 [loadSavedCategories] ${fieldType} 카테고리 값 API 호출`, { columnName });
const valuesResponse = await getCategoryValues(config.targetTable, columnName, false, menuObjid);
console.log(`📥 [loadSavedCategories] ${fieldType} 값 응답:`, valuesResponse);
if (valuesResponse.success && valuesResponse.data) {
console.log(`✅ [loadSavedCategories] ${fieldType} 카테고리 값:`, valuesResponse.data);
setCategoryValues(prev => {
const newState = { ...prev, [fieldType]: valuesResponse.data };
console.log(`🔄 [loadSavedCategories] ${fieldType} categoryValues 업데이트:`, newState);
return newState;
});
} else {
console.error(`❌ [loadSavedCategories] ${fieldType} 값 로드 실패:`, valuesResponse);
}
}
}
}
// 🔧 저장된 설정이 있는 아코디언들 열기
if (Object.keys(newExpandedState).length > 0) {
console.log("🔓 [loadSavedCategories] 아코디언 열기:", newExpandedState);
setExpandedCategoryMappings(prev => {
const finalState = { ...prev, ...newExpandedState };
console.log("🔄 [loadSavedCategories] 최종 아코디언 상태:", finalState);
return finalState;
});
}
};
loadSavedCategories();
}, [config.targetTable, config.autoCalculation?.valueMapping]);
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정 // 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
React.useEffect(() => { React.useEffect(() => {
if (screenTableName && !config.targetTable) { if (screenTableName && !config.targetTable) {
@ -344,10 +534,37 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
// 필드 제거 // 필드 제거
const removeField = (index: number) => { const removeField = (index: number) => {
// 로컬 입력 상태에서도 제거
setLocalFieldInputs(prev => {
const newInputs = { ...prev };
delete newInputs[index];
return newInputs;
});
handleFieldsChange(localFields.filter((_, i) => i !== index)); handleFieldsChange(localFields.filter((_, i) => i !== index));
}; };
// 필드 수정 // 🆕 로컬 필드 입력 업데이트 (포커스 유지용)
const updateFieldLocal = (index: number, field: 'label' | 'placeholder', value: string) => {
setLocalFieldInputs(prev => ({
...prev,
[index]: {
...prev[index],
[field]: value
}
}));
};
// 🆕 실제 필드 데이터 업데이트 (onBlur 시 호출)
const handleFieldBlur = (index: number) => {
const localInput = localFieldInputs[index];
if (localInput) {
const newFields = [...localFields];
newFields[index] = { ...newFields[index], ...localInput };
handleFieldsChange(newFields);
}
};
// 필드 수정 (Switch 같은 즉시 업데이트가 필요한 경우에만 사용)
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => { const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
const newFields = [...localFields]; const newFields = [...localFields];
newFields[index] = { ...newFields[index], ...updates }; newFields[index] = { ...newFields[index], ...updates };
@ -386,14 +603,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId)); handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
}; };
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => { // 🆕 로컬 그룹 입력 업데이트 (포커스 유지용)
// 1. 로컬 입력 상태 즉시 업데이트 (포커스 유지) const updateGroupLocal = (groupId: string, field: 'id' | 'title' | 'description' | 'order', value: any) => {
setLocalGroupInputs(prev => ({ setLocalGroupInputs(prev => ({
...prev, ...prev,
[groupId]: { ...prev[groupId], ...updates } [groupId]: {
...prev[groupId],
[field]: value
}
})); }));
};
// 2. 실제 그룹 데이터 업데이트
// 🆕 실제 그룹 데이터 업데이트 (onBlur 시 호출)
const handleGroupBlur = (groupId: string) => {
const localInput = localGroupInputs[groupId];
if (localInput) {
const newGroups = localFieldGroups.map(g =>
g.id === groupId ? { ...g, ...localInput } : g
);
handleFieldGroupsChange(newGroups);
}
};
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
// 2. 실제 그룹 데이터 업데이트 (Switch 같은 즉시 업데이트가 필요한 경우에만 사용)
const newGroups = localFieldGroups.map(g => const newGroups = localFieldGroups.map(g =>
g.id === groupId ? { ...g, ...updates } : g g.id === groupId ? { ...g, ...updates } : g
); );
@ -467,6 +700,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
return g; return g;
}); });
// 🔧 아이템 추가 시 해당 그룹의 아코디언을 열린 상태로 유지
setExpandedDisplayItems(prev => ({
...prev,
[groupId]: true
}));
setLocalFieldGroups(updatedGroups); setLocalFieldGroups(updatedGroups);
handleChange("fieldGroups", updatedGroups); handleChange("fieldGroups", updatedGroups);
}; };
@ -796,8 +1035,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px] sm:text-xs"></Label> <Label className="text-[10px] sm:text-xs"></Label>
<Input <Input
value={field.label} value={localFieldInputs[index]?.label !== undefined ? localFieldInputs[index].label : field.label}
onChange={(e) => updateField(index, { label: e.target.value })} onChange={(e) => updateFieldLocal(index, 'label', e.target.value)}
onBlur={() => handleFieldBlur(index)}
placeholder="필드 라벨" placeholder="필드 라벨"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs" className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/> />
@ -821,8 +1061,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px] sm:text-xs">Placeholder</Label> <Label className="text-[10px] sm:text-xs">Placeholder</Label>
<Input <Input
value={field.placeholder || ""} value={localFieldInputs[index]?.placeholder !== undefined ? localFieldInputs[index].placeholder : (field.placeholder || "")}
onChange={(e) => updateField(index, { placeholder: e.target.value })} onChange={(e) => updateFieldLocal(index, 'placeholder', e.target.value)}
onBlur={() => handleFieldBlur(index)}
placeholder="입력 안내" placeholder="입력 안내"
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs" className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
/> />
@ -1078,14 +1319,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Label className="text-[10px] sm:text-xs"> ID</Label> <Label className="text-[10px] sm:text-xs"> ID</Label>
<Input <Input
value={localGroupInputs[group.id]?.id !== undefined ? localGroupInputs[group.id].id : group.id} value={localGroupInputs[group.id]?.id !== undefined ? localGroupInputs[group.id].id : group.id}
onChange={(e) => { onChange={(e) => updateGroupLocal(group.id, 'id', e.target.value)}
const newValue = e.target.value; onBlur={() => handleGroupBlur(group.id)}
setLocalGroupInputs(prev => ({
...prev,
[group.id]: { ...prev[group.id], id: newValue }
}));
updateFieldGroup(group.id, { id: newValue });
}}
className="h-7 text-xs sm:h-8 sm:text-sm" className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="group_customer" placeholder="group_customer"
/> />
@ -1096,14 +1331,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Label className="text-[10px] sm:text-xs"> </Label> <Label className="text-[10px] sm:text-xs"> </Label>
<Input <Input
value={localGroupInputs[group.id]?.title !== undefined ? localGroupInputs[group.id].title : group.title} value={localGroupInputs[group.id]?.title !== undefined ? localGroupInputs[group.id].title : group.title}
onChange={(e) => { onChange={(e) => updateGroupLocal(group.id, 'title', e.target.value)}
const newValue = e.target.value; onBlur={() => handleGroupBlur(group.id)}
setLocalGroupInputs(prev => ({
...prev,
[group.id]: { ...prev[group.id], title: newValue }
}));
updateFieldGroup(group.id, { title: newValue });
}}
className="h-7 text-xs sm:h-8 sm:text-sm" className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="거래처 정보" placeholder="거래처 정보"
/> />
@ -1114,14 +1343,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Label className="text-[10px] sm:text-xs"> ()</Label> <Label className="text-[10px] sm:text-xs"> ()</Label>
<Input <Input
value={localGroupInputs[group.id]?.description !== undefined ? localGroupInputs[group.id].description : (group.description || "")} value={localGroupInputs[group.id]?.description !== undefined ? localGroupInputs[group.id].description : (group.description || "")}
onChange={(e) => { onChange={(e) => updateGroupLocal(group.id, 'description', e.target.value)}
const newValue = e.target.value; onBlur={() => handleGroupBlur(group.id)}
setLocalGroupInputs(prev => ({
...prev,
[group.id]: { ...prev[group.id], description: newValue }
}));
updateFieldGroup(group.id, { description: newValue });
}}
className="h-7 text-xs sm:h-8 sm:text-sm" className="h-7 text-xs sm:h-8 sm:text-sm"
placeholder="거래처 관련 정보를 입력합니다" placeholder="거래처 관련 정보를 입력합니다"
/> />
@ -1133,14 +1356,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Input <Input
type="number" type="number"
value={localGroupInputs[group.id]?.order !== undefined ? localGroupInputs[group.id].order : (group.order || 0)} value={localGroupInputs[group.id]?.order !== undefined ? localGroupInputs[group.id].order : (group.order || 0)}
onChange={(e) => { onChange={(e) => updateGroupLocal(group.id, 'order', parseInt(e.target.value) || 0)}
const newValue = parseInt(e.target.value) || 0; onBlur={() => handleGroupBlur(group.id)}
setLocalGroupInputs(prev => ({
...prev,
[group.id]: { ...prev[group.id], order: newValue }
}));
updateFieldGroup(group.id, { order: newValue });
}}
className="h-7 text-xs sm:h-8 sm:text-sm" className="h-7 text-xs sm:h-8 sm:text-sm"
min="0" min="0"
/> />
@ -1236,8 +1453,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
{/* 아이콘 설정 */} {/* 아이콘 설정 */}
{item.type === "icon" && ( {item.type === "icon" && (
<Input <Input
value={item.icon || ""} value={
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { icon: e.target.value })} localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined
? localDisplayItemInputs[group.id][itemIndex].value
: item.icon || ""
}
onChange={(e) => {
const newValue = e.target.value;
setLocalDisplayItemInputs(prev => ({
...prev,
[group.id]: {
...prev[group.id],
[itemIndex]: {
...prev[group.id]?.[itemIndex],
value: newValue
}
}
}));
}}
onBlur={() => {
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
if (localValue !== undefined) {
updateDisplayItemInGroup(group.id, itemIndex, { icon: localValue });
}
}}
placeholder="Building" placeholder="Building"
className="h-6 text-[9px] sm:text-[10px]" className="h-6 text-[9px] sm:text-[10px]"
/> />
@ -1264,8 +1503,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
} }
} }
})); }));
// 실제 상태 업데이트 }}
updateDisplayItemInGroup(group.id, itemIndex, { value: newValue }); onBlur={() => {
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
if (localValue !== undefined) {
updateDisplayItemInGroup(group.id, itemIndex, { value: localValue });
}
}} }}
placeholder="| , / , -" placeholder="| , / , -"
className="h-6 text-[9px] sm:text-[10px]" className="h-6 text-[9px] sm:text-[10px]"
@ -1312,8 +1555,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
} }
} }
})); }));
// 실제 상태 업데이트 }}
updateDisplayItemInGroup(group.id, itemIndex, { label: newValue }); onBlur={() => {
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.label;
if (localValue !== undefined) {
updateDisplayItemInGroup(group.id, itemIndex, { label: localValue });
}
}} }}
placeholder="라벨 (예: 거래처:)" placeholder="라벨 (예: 거래처:)"
className="h-6 w-full text-[9px] sm:text-[10px]" className="h-6 w-full text-[9px] sm:text-[10px]"
@ -1354,8 +1601,30 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
{/* 기본값 */} {/* 기본값 */}
{item.emptyBehavior === "default" && ( {item.emptyBehavior === "default" && (
<Input <Input
value={item.defaultValue || ""} value={
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: e.target.value })} localDisplayItemInputs[group.id]?.[itemIndex]?.value !== undefined
? localDisplayItemInputs[group.id][itemIndex].value
: item.defaultValue || ""
}
onChange={(e) => {
const newValue = e.target.value;
setLocalDisplayItemInputs(prev => ({
...prev,
[group.id]: {
...prev[group.id],
[itemIndex]: {
...prev[group.id]?.[itemIndex],
value: newValue
}
}
}));
}}
onBlur={() => {
const localValue = localDisplayItemInputs[group.id]?.[itemIndex]?.value;
if (localValue !== undefined) {
updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: localValue });
}
}}
placeholder="미입력" placeholder="미입력"
className="h-6 w-full text-[9px] sm:text-[10px]" className="h-6 w-full text-[9px] sm:text-[10px]"
/> />
@ -1670,14 +1939,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Label className="text-[10px] font-semibold sm:text-xs"> </Label> <Label className="text-[10px] font-semibold sm:text-xs"> </Label>
{/* 할인 방식 매핑 */} {/* 할인 방식 매핑 */}
<Collapsible> <Collapsible
open={expandedCategoryMappings.discountType}
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, discountType: open }))}
>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted" className="flex w-full items-center justify-between p-2 hover:bg-muted"
> >
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" /> {expandedCategoryMappings.discountType ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <CollapsibleContent className="space-y-2 pt-2">
@ -1702,30 +1978,40 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
</div> </div>
{/* 2단계: 카테고리 선택 */} {/* 2단계: 카테고리 선택 */}
{(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType && ( {(() => {
<div className="space-y-1"> const hasSelectedMenu = !!(config.autoCalculation.valueMapping as any)?._selectedMenus?.discountType;
<Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 </Label> const columns = categoryColumns.discountType || [];
<Select console.log("🎨 [렌더링] 2단계 카테고리 선택", {
value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType || ""} hasSelectedMenu,
onValueChange={(value) => handleCategorySelect( columns,
value, columnsCount: columns.length,
(config.autoCalculation.valueMapping as any)._selectedMenus.discountType, categoryColumnsState: categoryColumns
"discountType" });
)} return hasSelectedMenu ? (
> <div className="space-y-1">
<SelectTrigger className="h-7 text-xs"> <Label className="text-[9px] sm:text-[10px]">2단계: 카테고리 </Label>
<SelectValue placeholder="카테고리 선택" /> <Select
</SelectTrigger> value={(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType || ""}
<SelectContent> onValueChange={(value) => handleCategorySelect(
{(categoryColumns.discountType || []).map((col: any) => ( value,
<SelectItem key={col.columnName} value={col.columnName}> (config.autoCalculation.valueMapping as any)._selectedMenus.discountType,
{col.columnLabel || col.columnName} "discountType"
</SelectItem> )}
))} >
</SelectContent> <SelectTrigger className="h-7 text-xs">
</Select> <SelectValue placeholder="카테고리 선택" />
</div> </SelectTrigger>
)} <SelectContent>
{columns.map((col: any) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null;
})()}
{/* 3단계: 값 매핑 */} {/* 3단계: 값 매핑 */}
{(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && ( {(config.autoCalculation.valueMapping as any)?._selectedCategories?.discountType && (
@ -1780,14 +2066,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
</Collapsible> </Collapsible>
{/* 반올림 방식 매핑 */} {/* 반올림 방식 매핑 */}
<Collapsible> <Collapsible
open={expandedCategoryMappings.roundingType}
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, roundingType: open }))}
>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted" className="flex w-full items-center justify-between p-2 hover:bg-muted"
> >
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" /> {expandedCategoryMappings.roundingType ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <CollapsibleContent className="space-y-2 pt-2">
@ -1890,14 +2183,21 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
</Collapsible> </Collapsible>
{/* 반올림 단위 매핑 */} {/* 반올림 단위 매핑 */}
<Collapsible> <Collapsible
open={expandedCategoryMappings.roundingUnit}
onOpenChange={(open) => setExpandedCategoryMappings(prev => ({ ...prev, roundingUnit: open }))}
>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
className="flex w-full items-center justify-between p-2 hover:bg-muted" className="flex w-full items-center justify-between p-2 hover:bg-muted"
> >
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
<ChevronDown className="h-4 w-4" /> {expandedCategoryMappings.roundingUnit ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <CollapsibleContent className="space-y-2 pt-2">
@ -2235,10 +2535,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
variant="outline" variant="outline"
role="combobox" role="combobox"
className="h-7 w-full justify-between text-xs font-normal" className="h-7 w-full justify-between text-xs font-normal"
disabled={targetTableColumns.length === 0} disabled={!config.targetTable || loadedTargetTableColumns.length === 0}
> >
{mapping.targetField {mapping.targetField
? targetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel || ? loadedTargetTableColumns.find((c) => c.columnName === mapping.targetField)?.columnLabel ||
mapping.targetField mapping.targetField
: "저장 테이블 컬럼 선택"} : "저장 테이블 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
@ -2248,13 +2548,15 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<Command> <Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" /> <CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList> <CommandList>
{targetTableColumns.length === 0 ? ( {!config.targetTable ? (
<CommandEmpty className="text-xs"> </CommandEmpty> <CommandEmpty className="text-xs"> </CommandEmpty>
) : loadedTargetTableColumns.length === 0 ? (
<CommandEmpty className="text-xs"> ...</CommandEmpty>
) : ( ) : (
<> <>
<CommandEmpty className="text-xs"> .</CommandEmpty> <CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup> <CommandGroup>
{targetTableColumns.map((col) => { {loadedTargetTableColumns.map((col) => {
const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase(); const searchValue = `${col.columnLabel || col.columnName} ${col.columnName} ${col.dataType || ""}`.toLowerCase();
return ( return (
<CommandItem <CommandItem
@ -2276,7 +2578,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
<div className="flex flex-col"> <div className="flex flex-col">
<span>{col.columnLabel || col.columnName}</span> <span>{col.columnLabel || col.columnName}</span>
{col.dataType && ( {col.dataType && (
<span className="text-[10px] text-muted-foreground">{col.dataType}</span> <span className="text-[10px] text-muted-foreground">
{col.dataType}
</span>
)} )}
</div> </div>
</CommandItem> </CommandItem>
@ -2289,17 +2593,27 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<p className="text-[8px] text-muted-foreground">
({config.targetTable || "미선택"})
</p>
</div> </div>
{/* 기본값 (선택사항) */} {/* 기본값 (선택사항) */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[9px] sm:text-[10px]"> ()</Label> <Label className="text-[9px] sm:text-[10px]"> ()</Label>
<Input <Input
value={mapping.defaultValue || ""} value={localMappingInputs[index] !== undefined ? localMappingInputs[index] : mapping.defaultValue || ""}
onChange={(e) => { onChange={(e) => {
const updated = [...(config.parentDataMapping || [])]; const newValue = e.target.value;
updated[index] = { ...updated[index], defaultValue: e.target.value }; setLocalMappingInputs(prev => ({ ...prev, [index]: newValue }));
handleChange("parentDataMapping", updated); }}
onBlur={() => {
const currentValue = localMappingInputs[index];
if (currentValue !== undefined) {
const updated = [...(config.parentDataMapping || [])];
updated[index] = { ...updated[index], defaultValue: currentValue || undefined };
handleChange("parentDataMapping", updated);
}
}} }}
placeholder="값이 없을 때 사용할 기본값" placeholder="값이 없을 때 사용할 기본값"
className="h-7 text-xs" className="h-7 text-xs"
@ -2307,46 +2621,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
</div> </div>
{/* 삭제 버튼 */} {/* 삭제 버튼 */}
<Button <div className="flex justify-end pt-2">
size="sm" <Button
variant="ghost" size="sm"
className="h-6 w-full text-xs text-destructive hover:text-destructive" variant="ghost"
onClick={() => { className="h-7 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive"
const updated = (config.parentDataMapping || []).filter((_, i) => i !== index); onClick={() => {
handleChange("parentDataMapping", updated); const updated = (config.parentDataMapping || []).filter((_, i) => i !== index);
}} handleChange("parentDataMapping", updated);
> }}
<X className="mr-1 h-3 w-3" /> >
<X className="mr-1 h-3 w-3" />
</Button>
</Button>
</div>
</div> </div>
</Card> </Card>
))} ))}
</div> </div>
{(config.parentDataMapping || []).length === 0 && (
<p className="text-center text-[10px] text-muted-foreground py-4">
. "추가" .
</p>
)}
{/* 예시 */}
<div className="rounded-lg bg-green-50 p-2 text-xs">
<p className="mb-1 text-[10px] font-medium text-green-900">💡 </p>
<div className="space-y-1 text-[9px] text-green-700">
<p><strong> 1: 거래처 ID</strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">customer_mng</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">id</code> : <code className="bg-green-100 px-1">customer_id</code></p>
<p className="mt-1"><strong> 2: 품목 ID</strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">item_info</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">id</code> : <code className="bg-green-100 px-1">item_id</code></p>
<p className="mt-1"><strong> 3: 품목 </strong></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">item_info</code></p>
<p className="ml-2"> : <code className="bg-green-100 px-1">standard_price</code> : <code className="bg-green-100 px-1">base_price</code></p>
</div>
</div>
</div> </div>
{/* 사용 예시 */} {/* 사용 예시 */}
@ -2363,3 +2655,5 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
}; };
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel"; SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";
export default SelectedItemsDetailInputConfigPanel;

View File

@ -378,3 +378,4 @@ interface TablePermission {