"use client"; import { useState, useCallback, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Search, ChevronRight } from "lucide-react"; import { usePopEvent } from "@/hooks/pop"; import type { PopSearchConfig, DatePresetOption, } from "./types"; import { DATE_PRESET_LABELS, computeDateRange, DEFAULT_SEARCH_CONFIG } from "./types"; // ======================================== // 메인 컴포넌트 // ======================================== interface PopSearchComponentProps { config: PopSearchConfig; label?: string; screenId?: string; componentId?: string; } const DEFAULT_CONFIG = DEFAULT_SEARCH_CONFIG; export function PopSearchComponent({ config: rawConfig, label, screenId, componentId, }: PopSearchComponentProps) { const config = { ...DEFAULT_CONFIG, ...(rawConfig || {}) }; const { publish, subscribe, setSharedData } = usePopEvent(screenId || ""); const [value, setValue] = useState(config.defaultValue ?? ""); const fieldKey = config.fieldName || componentId || "search"; const emitFilterChanged = useCallback( (newValue: unknown) => { setValue(newValue); setSharedData(`search_${fieldKey}`, newValue); // 표준 출력 이벤트 (연결 시스템용) if (componentId) { publish(`__comp_output__${componentId}__filter_value`, { fieldName: fieldKey, value: newValue, }); } // 레거시 호환 publish("filter_changed", { [fieldKey]: newValue }); }, [fieldKey, publish, setSharedData, componentId] ); // 외부 값 수신 (스캔 결과, 모달 선택 등) useEffect(() => { if (!componentId) return; const unsub = subscribe( `__comp_input__${componentId}__set_value`, (payload: unknown) => { const data = payload as { value?: unknown } | unknown; const incoming = typeof data === "object" && data && "value" in data ? (data as { value: unknown }).value : data; emitFilterChanged(incoming); } ); return unsub; }, [componentId, subscribe, emitFilterChanged]); const showLabel = config.labelVisible !== false && !!config.labelText; return (
{showLabel && ( {config.labelText} )}
); } // ======================================== // 서브타입 분기 렌더러 // ======================================== interface InputRendererProps { config: PopSearchConfig; value: unknown; onChange: (v: unknown) => void; } function SearchInputRenderer({ config, value, onChange }: InputRendererProps) { switch (config.inputType) { case "text": case "number": return ( ); case "select": return ( ); case "date-preset": return ; case "toggle": return ( ); case "modal-table": case "modal-card": case "modal-icon-grid": return ( ); default: return ; } } // ======================================== // text 서브타입: 디바운스 + Enter // ======================================== interface TextInputProps { config: PopSearchConfig; value: string; onChange: (v: unknown) => void; } function TextSearchInput({ config, value, onChange }: TextInputProps) { const [inputValue, setInputValue] = useState(value); const debounceRef = useRef | null>(null); useEffect(() => { setInputValue(value); }, [value]); useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); const handleChange = (e: React.ChangeEvent) => { const v = e.target.value; setInputValue(v); if (debounceRef.current) clearTimeout(debounceRef.current); const ms = config.debounceMs ?? 500; if (ms > 0) { debounceRef.current = setTimeout(() => onChange(v), ms); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && config.triggerOnEnter !== false) { if (debounceRef.current) clearTimeout(debounceRef.current); onChange(inputValue); } }; const isNumber = config.inputType === "number"; return (
); } // ======================================== // select 서브타입: 즉시 발행 // ======================================== interface SelectInputProps { config: PopSearchConfig; value: string; onChange: (v: unknown) => void; } function SelectSearchInput({ config, value, onChange }: SelectInputProps) { return ( ); } // ======================================== // date-preset 서브타입: 탭 버튼 + 즉시 발행 // ======================================== interface DatePresetInputProps { config: PopSearchConfig; value: unknown; onChange: (v: unknown) => void; } function DatePresetSearchInput({ config, value, onChange }: DatePresetInputProps) { const presets: DatePresetOption[] = config.datePresets || ["today", "this-week", "this-month"]; const currentPreset = value && typeof value === "object" && "preset" in (value as Record) ? (value as Record).preset : value; const handleSelect = (preset: DatePresetOption) => { if (preset === "custom") { onChange({ preset: "custom", from: "", to: "" }); return; } const range = computeDateRange(preset); if (range) onChange(range); }; return (
{presets.map((preset) => ( ))}
); } // ======================================== // toggle 서브타입: Switch + 즉시 발행 // ======================================== interface ToggleInputProps { value: boolean; onChange: (v: unknown) => void; } function ToggleSearchInput({ value, onChange }: ToggleInputProps) { return (
onChange(checked)} /> {value ? "ON" : "OFF"}
); } // ======================================== // modal-* 서브타입: readonly 입력 + 아이콘 (MVP: UI만) // ======================================== interface ModalInputProps { config: PopSearchConfig; value: string; } function ModalSearchInput({ config, value }: ModalInputProps) { return (
{value || config.placeholder || "선택..."}
); } // ======================================== // 미구현 서브타입 플레이스홀더 // ======================================== function PlaceholderInput({ inputType }: { inputType: string }) { return (
{inputType} (후속 구현 예정)
); }