출발지 목적지 선택

This commit is contained in:
leeheejin 2025-12-01 11:07:16 +09:00
parent c657d6f7a0
commit d7ee63a857
6 changed files with 284 additions and 65 deletions

View File

@ -53,6 +53,7 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
}; };
// DOM에 전달하면 안 되는 React-specific props 필터링 // DOM에 전달하면 안 되는 React-specific props 필터링
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { const {
selectedScreen, selectedScreen,
onZoneComponentDrop, onZoneComponentDrop,
@ -70,8 +71,40 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
tableName: _tableName, tableName: _tableName,
onRefresh: _onRefresh, onRefresh: _onRefresh,
onClose: _onClose, onClose: _onClose,
// 추가된 props 필터링
webType: _webType,
autoGeneration: _autoGeneration,
isInteractive: _isInteractive,
formData: _formData,
onFormDataChange: _onFormDataChange,
menuId: _menuId,
menuObjid: _menuObjid,
onSave: _onSave,
userId: _userId,
userName: _userName,
companyCode: _companyCode,
isInModal: _isInModal,
readonly: _readonly,
originalData: _originalData,
allComponents: _allComponents,
onUpdateLayout: _onUpdateLayout,
selectedRows: _selectedRows,
selectedRowsData: _selectedRowsData,
onSelectedRowsChange: _onSelectedRowsChange,
sortBy: _sortBy,
sortOrder: _sortOrder,
tableDisplayData: _tableDisplayData,
flowSelectedData: _flowSelectedData,
flowSelectedStepId: _flowSelectedStepId,
onFlowSelectedDataChange: _onFlowSelectedDataChange,
onConfigChange: _onConfigChange,
refreshKey: _refreshKey,
flowRefreshKey: _flowRefreshKey,
onFlowRefresh: _onFlowRefresh,
isPreview: _isPreview,
groupedData: _groupedData,
...domProps ...domProps
} = props; } = props as any;
return ( return (
<div style={componentStyle} className={className} {...domProps}> <div style={componentStyle} className={className} {...domProps}>

View File

@ -103,11 +103,36 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
const departureValue = formData[departureField] || ""; const departureValue = formData[departureField] || "";
const destinationValue = formData[destinationField] || ""; const destinationValue = formData[destinationField] || "";
// 기본 옵션 (포항/광양)
const DEFAULT_OPTIONS: LocationOption[] = [
{ value: "pohang", label: "포항" },
{ value: "gwangyang", label: "광양" },
];
// 옵션 로드 // 옵션 로드
useEffect(() => { useEffect(() => {
const loadOptions = async () => { const loadOptions = async () => {
if (dataSource.type === "static") { console.log("[LocationSwapSelector] 옵션 로드 시작:", { dataSource, isDesignMode });
setOptions(dataSource.staticOptions || []);
// 정적 옵션 처리 (기본값)
// type이 없거나 static이거나, table인데 tableName이 없는 경우
const shouldUseStatic =
!dataSource.type ||
dataSource.type === "static" ||
(dataSource.type === "table" && !dataSource.tableName) ||
(dataSource.type === "code" && !dataSource.codeCategory);
if (shouldUseStatic) {
const staticOpts = dataSource.staticOptions || [];
// 정적 옵션이 설정되어 있으면 사용
if (staticOpts.length > 0 && staticOpts[0]?.value) {
console.log("[LocationSwapSelector] 정적 옵션 사용:", staticOpts);
setOptions(staticOpts);
} else {
// 기본값 (포항/광양)
console.log("[LocationSwapSelector] 기본 옵션 사용:", DEFAULT_OPTIONS);
setOptions(DEFAULT_OPTIONS);
}
return; return;
} }
@ -159,17 +184,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
} }
}; };
if (!isDesignMode) { loadOptions();
loadOptions();
} else {
// 디자인 모드에서는 샘플 데이터
setOptions([
{ value: "seoul", label: "서울" },
{ value: "busan", label: "부산" },
{ value: "pohang", label: "포항" },
{ value: "gwangyang", label: "광양" },
]);
}
}, [dataSource, isDesignMode]); }, [dataSource, isDesignMode]);
// 출발지 변경 // 출발지 변경
@ -250,7 +265,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={departureValue} value={departureValue}
onValueChange={handleDepartureChange} onValueChange={handleDepartureChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-auto w-full max-w-[120px] border-0 bg-transparent p-0 text-center text-lg font-bold shadow-none focus:ring-0"> <SelectTrigger className="h-auto w-full max-w-[120px] border-0 bg-transparent p-0 text-center text-lg font-bold shadow-none focus:ring-0">
<SelectValue placeholder="선택"> <SelectValue placeholder="선택">
@ -259,12 +274,16 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
</span> </span>
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -276,7 +295,6 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={handleSwap} onClick={handleSwap}
disabled={isDesignMode || !departureValue || !destinationValue}
className={cn( className={cn(
"mx-2 h-10 w-10 rounded-full border bg-background transition-transform hover:bg-muted", "mx-2 h-10 w-10 rounded-full border bg-background transition-transform hover:bg-muted",
isSwapping && "rotate-180" isSwapping && "rotate-180"
@ -292,7 +310,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={destinationValue} value={destinationValue}
onValueChange={handleDestinationChange} onValueChange={handleDestinationChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-auto w-full max-w-[120px] border-0 bg-transparent p-0 text-center text-lg font-bold shadow-none focus:ring-0"> <SelectTrigger className="h-auto w-full max-w-[120px] border-0 bg-transparent p-0 text-center text-lg font-bold shadow-none focus:ring-0">
<SelectValue placeholder="선택"> <SelectValue placeholder="선택">
@ -301,12 +319,16 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
</span> </span>
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -328,17 +350,21 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={departureValue} value={departureValue}
onValueChange={handleDepartureChange} onValueChange={handleDepartureChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-10"> <SelectTrigger className="h-10">
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -349,7 +375,6 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
variant="outline" variant="outline"
size="icon" size="icon"
onClick={handleSwap} onClick={handleSwap}
disabled={isDesignMode}
className="mt-5 h-10 w-10" className="mt-5 h-10 w-10"
> >
<ArrowLeftRight className="h-4 w-4" /> <ArrowLeftRight className="h-4 w-4" />
@ -361,17 +386,21 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={destinationValue} value={destinationValue}
onValueChange={handleDestinationChange} onValueChange={handleDestinationChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-10"> <SelectTrigger className="h-10">
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -389,17 +418,21 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={departureValue} value={departureValue}
onValueChange={handleDepartureChange} onValueChange={handleDepartureChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-8 flex-1 text-sm"> <SelectTrigger className="h-8 flex-1 text-sm">
<SelectValue placeholder={departureLabel} /> <SelectValue placeholder={departureLabel} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
@ -409,7 +442,6 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={handleSwap} onClick={handleSwap}
disabled={isDesignMode}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
<ArrowLeftRight className="h-4 w-4" /> <ArrowLeftRight className="h-4 w-4" />
@ -419,17 +451,21 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
<Select <Select
value={destinationValue} value={destinationValue}
onValueChange={handleDestinationChange} onValueChange={handleDestinationChange}
disabled={loading || isDesignMode} disabled={loading}
> >
<SelectTrigger className="h-8 flex-1 text-sm"> <SelectTrigger className="h-8 flex-1 text-sm">
<SelectValue placeholder={destinationLabel} /> <SelectValue placeholder={destinationLabel} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" sideOffset={4}>
{options.map((option) => ( {options.length > 0 ? (
<SelectItem key={option.value} value={option.value}> options.map((option) => (
{option.label} <SelectItem key={option.value} value={option.value}>
</SelectItem> {option.label}
))} </SelectItem>
))
) : (
<div className="px-2 py-1.5 text-sm text-muted-foreground"> </div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -139,13 +139,83 @@ export function LocationSwapSelectorConfigPanel({
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="static"> ()</SelectItem> <SelectItem value="static"> (/ )</SelectItem>
<SelectItem value="table"></SelectItem> <SelectItem value="table"> </SelectItem>
<SelectItem value="code"> </SelectItem> <SelectItem value="code"> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* 고정 옵션 설정 (type이 static일 때) */}
{(!config?.dataSource?.type || config?.dataSource?.type === "static") && (
<div className="space-y-3 rounded-md bg-amber-50 p-3 dark:bg-amber-950">
<h4 className="text-sm font-medium"> </h4>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label> 1 ()</Label>
<Input
value={config?.dataSource?.staticOptions?.[0]?.value || ""}
onChange={(e) => {
const options = config?.dataSource?.staticOptions || [];
const newOptions = [...options];
newOptions[0] = { ...newOptions[0], value: e.target.value };
handleChange("dataSource.staticOptions", newOptions);
}}
placeholder="예: pohang"
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<Label> 1 ()</Label>
<Input
value={config?.dataSource?.staticOptions?.[0]?.label || ""}
onChange={(e) => {
const options = config?.dataSource?.staticOptions || [];
const newOptions = [...options];
newOptions[0] = { ...newOptions[0], label: e.target.value };
handleChange("dataSource.staticOptions", newOptions);
}}
placeholder="예: 포항"
className="h-8 text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label> 2 ()</Label>
<Input
value={config?.dataSource?.staticOptions?.[1]?.value || ""}
onChange={(e) => {
const options = config?.dataSource?.staticOptions || [];
const newOptions = [...options];
newOptions[1] = { ...newOptions[1], value: e.target.value };
handleChange("dataSource.staticOptions", newOptions);
}}
placeholder="예: gwangyang"
className="h-8 text-xs"
/>
</div>
<div className="space-y-2">
<Label> 2 ()</Label>
<Input
value={config?.dataSource?.staticOptions?.[1]?.label || ""}
onChange={(e) => {
const options = config?.dataSource?.staticOptions || [];
const newOptions = [...options];
newOptions[1] = { ...newOptions[1], label: e.target.value };
handleChange("dataSource.staticOptions", newOptions);
}}
placeholder="예: 광양"
className="h-8 text-xs"
/>
</div>
</div>
<p className="text-xs text-amber-700 dark:text-amber-300">
2 . (: 포항 )
</p>
</div>
)}
{/* 테이블 선택 (type이 table일 때) */} {/* 테이블 선택 (type이 table일 때) */}
{config?.dataSource?.type === "table" && ( {config?.dataSource?.type === "table" && (
<> <>

View File

@ -12,7 +12,28 @@ export class LocationSwapSelectorRenderer extends AutoRegisteringComponentRender
static componentDefinition = LocationSwapSelectorDefinition; static componentDefinition = LocationSwapSelectorDefinition;
render(): React.ReactElement { render(): React.ReactElement {
return <LocationSwapSelectorComponent {...this.props} />; const { component, formData, onFormDataChange, isDesignMode, style, ...restProps } = this.props;
// component.componentConfig에서 설정 가져오기
const componentConfig = component?.componentConfig || {};
console.log("[LocationSwapSelectorRenderer] render:", {
componentConfig,
formData,
isDesignMode
});
return (
<LocationSwapSelectorComponent
id={component?.id}
style={style}
isDesignMode={isDesignMode}
formData={formData}
onFormDataChange={onFormDataChange}
componentConfig={componentConfig}
{...restProps}
/>
);
} }
} }

View File

@ -20,12 +20,15 @@ export const LocationSwapSelectorDefinition = createComponentDefinition({
defaultConfig: { defaultConfig: {
// 데이터 소스 설정 // 데이터 소스 설정
dataSource: { dataSource: {
type: "table", // "table" | "code" | "static" type: "static", // "table" | "code" | "static"
tableName: "", // 장소 테이블명 tableName: "", // 장소 테이블명
valueField: "location_code", // 값 필드 valueField: "location_code", // 값 필드
labelField: "location_name", // 표시 필드 labelField: "location_name", // 표시 필드
codeCategory: "", // 코드 관리 카테고리 (type이 "code"일 때) codeCategory: "", // 코드 관리 카테고리 (type이 "code"일 때)
staticOptions: [], // 정적 옵션 (type이 "static"일 때) staticOptions: [
{ value: "pohang", label: "포항" },
{ value: "gwangyang", label: "광양" },
], // 정적 옵션 (type이 "static"일 때)
}, },
// 필드 매핑 // 필드 매핑
departureField: "departure", // 출발지 저장 필드 departureField: "departure", // 출발지 저장 필드

View File

@ -271,6 +271,9 @@ export class ButtonActionExecutor {
case "geolocation": case "geolocation":
return await this.handleGeolocation(config, context); return await this.handleGeolocation(config, context);
case "swap_fields":
return await this.handleSwapFields(config, context);
case "update_field": case "update_field":
return await this.handleUpdateField(config, context); return await this.handleUpdateField(config, context);
@ -3412,6 +3415,59 @@ export class ButtonActionExecutor {
} }
} }
/**
* (: 출발지 )
*/
private static async handleSwapFields(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("🔄 필드 값 교환 액션 실행:", { config, context });
const { formData, onFormDataChange } = context;
// 교환할 필드 확인
const fieldA = config.swapFieldA;
const fieldB = config.swapFieldB;
if (!fieldA || !fieldB) {
toast.error("교환할 필드가 설정되지 않았습니다.");
return false;
}
// 현재 값 가져오기
const valueA = formData?.[fieldA];
const valueB = formData?.[fieldB];
console.log("🔄 교환 전:", { [fieldA]: valueA, [fieldB]: valueB });
// 값 교환
if (onFormDataChange) {
onFormDataChange(fieldA, valueB);
onFormDataChange(fieldB, valueA);
}
// 관련 필드도 함께 교환 (예: 위도/경도)
if (config.swapRelatedFields && config.swapRelatedFields.length > 0) {
for (const related of config.swapRelatedFields) {
const relatedValueA = formData?.[related.fieldA];
const relatedValueB = formData?.[related.fieldB];
if (onFormDataChange) {
onFormDataChange(related.fieldA, relatedValueB);
onFormDataChange(related.fieldB, relatedValueA);
}
}
}
console.log("🔄 교환 후:", { [fieldA]: valueB, [fieldB]: valueA });
toast.success(config.successMessage || "값이 교환되었습니다.");
return true;
} catch (error) {
console.error("❌ 필드 값 교환 오류:", error);
toast.error(config.errorMessage || "값 교환 중 오류가 발생했습니다.");
return false;
}
}
/** /**
* (: status를 active로 ) * (: status를 active로 )
*/ */