"use client"; import React, { useState, useEffect } from "react"; import { ArrowLeftRight, ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; interface LocationOption { value: string; label: string; } interface DataSourceConfig { type: "table" | "code" | "static"; tableName?: string; valueField?: string; labelField?: string; codeCategory?: string; staticOptions?: LocationOption[]; } export interface LocationSwapSelectorProps { // 기본 props id?: string; style?: React.CSSProperties; isDesignMode?: boolean; // 데이터 소스 설정 dataSource?: DataSourceConfig; // 필드 매핑 departureField?: string; destinationField?: string; departureLabelField?: string; destinationLabelField?: string; // UI 설정 departureLabel?: string; destinationLabel?: string; showSwapButton?: boolean; swapButtonPosition?: "center" | "right"; variant?: "card" | "inline" | "minimal"; // 폼 데이터 formData?: Record; onFormDataChange?: (field: string, value: any) => void; // 🆕 사용자 정보 (DB에서 초기값 로드용) userId?: string; // componentConfig (화면 디자이너에서 전달) componentConfig?: { dataSource?: DataSourceConfig; departureField?: string; destinationField?: string; departureLabelField?: string; destinationLabelField?: string; departureLabel?: string; destinationLabel?: string; showSwapButton?: boolean; swapButtonPosition?: "center" | "right"; variant?: "card" | "inline" | "minimal"; // 🆕 DB 초기값 로드 설정 loadFromDb?: boolean; // DB에서 초기값 로드 여부 dbTableName?: string; // 조회할 테이블명 (기본: vehicles) dbKeyField?: string; // 키 필드 (기본: user_id) }; } /** * LocationSwapSelector 컴포넌트 * 출발지/도착지 선택 및 교환 기능 */ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) { const { id, style, isDesignMode = false, formData = {}, onFormDataChange, componentConfig, userId, } = props; // componentConfig에서 설정 가져오기 (우선순위: componentConfig > props) const config = componentConfig || {}; const dataSource = config.dataSource || props.dataSource || { type: "static", staticOptions: [] }; const departureField = config.departureField || props.departureField || "departure"; const destinationField = config.destinationField || props.destinationField || "destination"; const departureLabelField = config.departureLabelField || props.departureLabelField; const destinationLabelField = config.destinationLabelField || props.destinationLabelField; const departureLabel = config.departureLabel || props.departureLabel || "출발지"; const destinationLabel = config.destinationLabel || props.destinationLabel || "도착지"; const showSwapButton = config.showSwapButton !== false && props.showSwapButton !== false; const variant = config.variant || props.variant || "card"; // 🆕 DB 초기값 로드 설정 const loadFromDb = config.loadFromDb !== false; // 기본값 true const dbTableName = config.dbTableName || "vehicles"; const dbKeyField = config.dbKeyField || "user_id"; // 기본 옵션 (포항/광양) - 한글로 저장 const DEFAULT_OPTIONS: LocationOption[] = [ { value: "포항", label: "포항" }, { value: "광양", label: "광양" }, ]; // 상태 const [options, setOptions] = useState(DEFAULT_OPTIONS); const [loading, setLoading] = useState(false); const [isSwapping, setIsSwapping] = useState(false); const [dbLoaded, setDbLoaded] = useState(false); // DB 로드 완료 여부 // 로컬 선택 상태 (Select 컴포넌트용) const [localDeparture, setLocalDeparture] = useState(""); const [localDestination, setLocalDestination] = useState(""); // 옵션 로드 useEffect(() => { const loadOptions = async () => { console.log("[LocationSwapSelector] 옵션 로드 시작:", { dataSource, isDesignMode }); // 정적 옵션 처리 (기본값) // 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 || []; // 정적 옵션이 설정되어 있고, value가 유효한 경우 사용 // (value가 필드명과 같으면 잘못 설정된 것이므로 기본값 사용) const isValidOptions = staticOpts.length > 0 && staticOpts[0]?.value && staticOpts[0].value !== departureField && staticOpts[0].value !== destinationField; if (isValidOptions) { console.log("[LocationSwapSelector] 정적 옵션 사용:", staticOpts); setOptions(staticOpts); } else { // 기본값 (포항/광양) console.log("[LocationSwapSelector] 기본 옵션 사용 (잘못된 설정 감지):", { staticOpts, DEFAULT_OPTIONS }); setOptions(DEFAULT_OPTIONS); } return; } if (dataSource.type === "code" && dataSource.codeCategory) { // 코드 관리에서 가져오기 setLoading(true); try { const response = await apiClient.get(`/code-management/codes`, { params: { categoryCode: dataSource.codeCategory }, }); if (response.data.success && response.data.data) { const codeOptions = response.data.data.map((code: any) => ({ value: code.code_value || code.codeValue || code.code, label: code.code_name || code.codeName || code.name, })); setOptions(codeOptions); } } catch (error) { console.error("코드 로드 실패:", error); } finally { setLoading(false); } return; } if (dataSource.type === "table" && dataSource.tableName) { // 테이블에서 가져오기 setLoading(true); try { const response = await apiClient.get(`/dynamic-form/list/${dataSource.tableName}`, { params: { page: 1, pageSize: 1000 }, }); if (response.data.success && response.data.data) { // data가 배열인지 또는 data.rows인지 확인 const rows = Array.isArray(response.data.data) ? response.data.data : response.data.data.rows || []; const tableOptions = rows.map((row: any) => ({ value: String(row[dataSource.valueField || "id"] || ""), label: String(row[dataSource.labelField || "name"] || ""), })); setOptions(tableOptions); } } catch (error) { console.error("테이블 데이터 로드 실패:", error); } finally { setLoading(false); } } }; loadOptions(); }, [dataSource, isDesignMode]); // 🆕 DB에서 초기값 로드 (새로고침 시에도 출발지/목적지 유지) useEffect(() => { const loadFromDatabase = async () => { // 디자인 모드이거나, DB 로드 비활성화이거나, userId가 없으면 스킵 if (isDesignMode || !loadFromDb || !userId) { console.log("[LocationSwapSelector] DB 로드 스킵:", { isDesignMode, loadFromDb, userId }); return; } // 이미 로드했으면 스킵 if (dbLoaded) { return; } try { console.log("[LocationSwapSelector] DB에서 출발지/목적지 로드 시작:", { dbTableName, dbKeyField, userId }); const response = await apiClient.post( `/table-management/tables/${dbTableName}/data`, { page: 1, size: 1, search: { [dbKeyField]: userId }, autoFilter: true, } ); const vehicleData = response.data?.data?.data?.[0] || response.data?.data?.rows?.[0]; if (vehicleData) { const dbDeparture = vehicleData[departureField] || vehicleData.departure; const dbDestination = vehicleData[destinationField] || vehicleData.arrival || vehicleData.destination; console.log("[LocationSwapSelector] DB에서 로드된 값:", { dbDeparture, dbDestination }); // DB에 값이 있으면 로컬 상태 및 formData 업데이트 if (dbDeparture && options.some(o => o.value === dbDeparture)) { setLocalDeparture(dbDeparture); onFormDataChange?.(departureField, dbDeparture); // 라벨도 업데이트 if (departureLabelField) { const opt = options.find(o => o.value === dbDeparture); if (opt) { onFormDataChange?.(departureLabelField, opt.label); } } } if (dbDestination && options.some(o => o.value === dbDestination)) { setLocalDestination(dbDestination); onFormDataChange?.(destinationField, dbDestination); // 라벨도 업데이트 if (destinationLabelField) { const opt = options.find(o => o.value === dbDestination); if (opt) { onFormDataChange?.(destinationLabelField, opt.label); } } } } setDbLoaded(true); } catch (error) { console.error("[LocationSwapSelector] DB 로드 실패:", error); setDbLoaded(true); // 실패해도 다시 시도하지 않음 } }; // 옵션이 로드된 후에 DB 로드 실행 if (options.length > 0) { loadFromDatabase(); } }, [userId, loadFromDb, dbTableName, dbKeyField, departureField, destinationField, options, isDesignMode, dbLoaded, onFormDataChange, departureLabelField, destinationLabelField]); // formData에서 초기값 동기화 (DB 로드 후에도 formData 변경 시 반영) useEffect(() => { // DB 로드가 완료되지 않았으면 스킵 (DB 값 우선) if (loadFromDb && userId && !dbLoaded) { return; } const depVal = formData[departureField]; const destVal = formData[destinationField]; if (depVal && options.some(o => o.value === depVal)) { setLocalDeparture(depVal); } if (destVal && options.some(o => o.value === destVal)) { setLocalDestination(destVal); } }, [formData, departureField, destinationField, options, loadFromDb, userId, dbLoaded]); // 출발지 변경 const handleDepartureChange = (selectedValue: string) => { console.log("[LocationSwapSelector] 출발지 변경:", { selectedValue, departureField, hasOnFormDataChange: !!onFormDataChange, options }); // 로컬 상태 업데이트 setLocalDeparture(selectedValue); // 부모에게 전달 if (onFormDataChange) { console.log(`[LocationSwapSelector] onFormDataChange 호출: ${departureField} = ${selectedValue}`); onFormDataChange(departureField, selectedValue); // 라벨 필드도 업데이트 if (departureLabelField) { const selectedOption = options.find((opt) => opt.value === selectedValue); if (selectedOption) { console.log(`[LocationSwapSelector] onFormDataChange 호출: ${departureLabelField} = ${selectedOption.label}`); onFormDataChange(departureLabelField, selectedOption.label); } } } else { console.warn("[LocationSwapSelector] ⚠️ onFormDataChange가 없습니다!"); } }; // 도착지 변경 const handleDestinationChange = (selectedValue: string) => { console.log("[LocationSwapSelector] 도착지 변경:", { selectedValue, destinationField, hasOnFormDataChange: !!onFormDataChange, options }); // 로컬 상태 업데이트 setLocalDestination(selectedValue); // 부모에게 전달 if (onFormDataChange) { console.log(`[LocationSwapSelector] onFormDataChange 호출: ${destinationField} = ${selectedValue}`); onFormDataChange(destinationField, selectedValue); // 라벨 필드도 업데이트 if (destinationLabelField) { const selectedOption = options.find((opt) => opt.value === selectedValue); if (selectedOption) { console.log(`[LocationSwapSelector] onFormDataChange 호출: ${destinationLabelField} = ${selectedOption.label}`); onFormDataChange(destinationLabelField, selectedOption.label); } } } else { console.warn("[LocationSwapSelector] ⚠️ onFormDataChange가 없습니다!"); } }; // 출발지/도착지 교환 const handleSwap = () => { setIsSwapping(true); // 로컬 상태 교환 const tempDeparture = localDeparture; const tempDestination = localDestination; setLocalDeparture(tempDestination); setLocalDestination(tempDeparture); // 부모에게 전달 if (onFormDataChange) { onFormDataChange(departureField, tempDestination); onFormDataChange(destinationField, tempDeparture); // 라벨도 교환 if (departureLabelField && destinationLabelField) { const depOption = options.find(o => o.value === tempDestination); const destOption = options.find(o => o.value === tempDeparture); onFormDataChange(departureLabelField, depOption?.label || ""); onFormDataChange(destinationLabelField, destOption?.label || ""); } } // 애니메이션 효과 setTimeout(() => setIsSwapping(false), 300); }; // 스타일에서 width, height 추출 const { width, height, ...restStyle } = style || {}; // 선택된 라벨 가져오기 const getDepartureLabel = () => { const opt = options.find(o => o.value === localDeparture); return opt?.label || ""; }; const getDestinationLabel = () => { const opt = options.find(o => o.value === localDestination); return opt?.label || ""; }; // 디버그 로그 console.log("[LocationSwapSelector] 렌더:", { localDeparture, localDestination, options: options.map(o => `${o.value}:${o.label}`), }); // Card 스타일 (이미지 참고) if (variant === "card") { return (
{/* 출발지 */}
{departureLabel}
{/* 교환 버튼 */} {showSwapButton && ( )} {/* 도착지 */}
{destinationLabel}
); } // Inline 스타일 if (variant === "inline") { return (
{showSwapButton && ( )}
); } // Minimal 스타일 return (
{showSwapButton && ( )}
); }