fix: PopFieldConfig JsonKeySelect - 데이터 없을 때도 Combobox UI 유지

테이블에 데이터가 0건일 때 JsonKeySelect가 plain Input으로 폴백되어
설계 단계에서 Select 박스가 표시되지 않는 문제를 수정한다.
[JsonKeySelect 개선]
- 항상 Combobox(Popover + Command) UI로 렌더링
- keys 있을 때: 기존과 동일한 자동완성 목록 + 검색
- keys 없을 때: "테이블에 데이터가 없습니다" 안내 + Enter로 직접 입력 확정
- 검색 결과 없을 때도 Enter로 자유 입력 가능
[updateSaveMapping 경합 조건 수정]
- onUpdateConfig 두 번 연속 호출 시 React batching으로 첫 번째 호출이
  덮어쓰여지는 문제 수정
- syncAndUpdateSaveMappings에 extraPartial 파라미터 추가하여
  한 번의 onUpdateConfig 호출로 병합
This commit is contained in:
SeongHyun Kim 2026-03-16 16:24:27 +09:00
parent 8ee10e411e
commit 230d35b03a
1 changed files with 78 additions and 50 deletions

View File

@ -356,7 +356,10 @@ function SaveTabContent({
};
const syncAndUpdateSaveMappings = useCallback(
(updater?: (prev: PopFieldSaveMapping[]) => PopFieldSaveMapping[]) => {
(
updater?: (prev: PopFieldSaveMapping[]) => PopFieldSaveMapping[],
extraPartial?: Partial<PopFieldConfig>,
) => {
const fieldIds = new Set(allFields.map(({ field }) => field.id));
const prev = saveMappings.filter((m) => fieldIds.has(m.fieldId));
const next = updater ? updater(prev) : prev;
@ -381,6 +384,7 @@ function SaveTabContent({
tableName: saveTableName,
fieldMappings: merged,
},
...extraPartial,
});
}
},
@ -395,22 +399,27 @@ function SaveTabContent({
const updateSaveMapping = useCallback(
(fieldId: string, partial: Partial<PopFieldSaveMapping>) => {
syncAndUpdateSaveMappings((prev) =>
prev.map((m) => (m.fieldId === fieldId ? { ...m, ...partial } : m))
);
let extraPartial: Partial<PopFieldConfig> | undefined;
if (partial.targetColumn !== undefined) {
const newFieldName = partial.targetColumn || "";
const sections = cfg.sections.map((s) => ({
...s,
fields: (s.fields ?? []).map((f) =>
f.id === fieldId ? { ...f, fieldName: newFieldName } : f
),
}));
onUpdateConfig({ sections });
extraPartial = {
sections: cfg.sections.map((s) => ({
...s,
fields: (s.fields ?? []).map((f) =>
f.id === fieldId ? { ...f, fieldName: newFieldName } : f
),
})),
};
}
syncAndUpdateSaveMappings(
(prev) =>
prev.map((m) => (m.fieldId === fieldId ? { ...m, ...partial } : m)),
extraPartial,
);
},
[syncAndUpdateSaveMappings, cfg, onUpdateConfig]
[syncAndUpdateSaveMappings, cfg.sections]
);
// --- 숨은 필드 매핑 로직 ---
@ -2086,23 +2095,24 @@ function JsonKeySelect({
onOpen?: () => void;
}) {
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen);
if (nextOpen) onOpen?.();
if (nextOpen) {
onOpen?.();
setInputValue("");
}
};
if (keys.length === 0 && !value) {
return (
<Input
placeholder="키"
value={value}
onChange={(e) => onValueChange(e.target.value)}
onFocus={() => onOpen?.()}
className="h-7 w-24 text-xs"
/>
);
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && inputValue.trim()) {
e.preventDefault();
onValueChange(inputValue.trim());
setInputValue("");
setOpen(false);
}
};
return (
<Popover open={open} onOpenChange={handleOpenChange}>
@ -2117,33 +2127,51 @@ function JsonKeySelect({
</Button>
</PopoverTrigger>
<PopoverContent className="w-48 p-0" align="start">
<Command>
<CommandInput placeholder="키 검색..." className="text-xs" />
<Command shouldFilter={keys.length > 0}>
<CommandInput
placeholder={keys.length > 0 ? "키 검색..." : "키 직접 입력..."}
className="text-xs"
value={inputValue}
onValueChange={setInputValue}
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandEmpty className="py-2 text-center text-xs">
{keys.length === 0 ? "데이터를 불러오는 중..." : "일치하는 키가 없습니다."}
</CommandEmpty>
<CommandGroup>
{keys.map((k) => (
<CommandItem
key={k}
value={k}
onSelect={(v) => {
onValueChange(v === value ? "" : v);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
value === k ? "opacity-100" : "opacity-0"
)}
/>
{k}
</CommandItem>
))}
</CommandGroup>
{keys.length === 0 ? (
<div className="px-3 py-2 text-center text-xs text-muted-foreground">
{inputValue.trim()
? "Enter로 입력 확정"
: "테이블에 데이터가 없습니다. 키를 직접 입력하세요."}
</div>
) : (
<>
<CommandEmpty className="py-2 text-center text-xs">
{inputValue.trim()
? "Enter로 직접 입력 확정"
: "일치하는 키가 없습니다."}
</CommandEmpty>
<CommandGroup>
{keys.map((k) => (
<CommandItem
key={k}
value={k}
onSelect={(v) => {
onValueChange(v === value ? "" : v);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
value === k ? "opacity-100" : "opacity-0"
)}
/>
{k}
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>