feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
"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";
|
2026-02-23 18:45:21 +09:00
|
|
|
import { DATE_PRESET_LABELS, computeDateRange, DEFAULT_SEARCH_CONFIG } from "./types";
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 메인 컴포넌트
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface PopSearchComponentProps {
|
|
|
|
|
config: PopSearchConfig;
|
|
|
|
|
label?: string;
|
|
|
|
|
screenId?: string;
|
2026-02-23 18:45:21 +09:00
|
|
|
componentId?: string;
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
const DEFAULT_CONFIG = DEFAULT_SEARCH_CONFIG;
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
|
|
|
|
|
export function PopSearchComponent({
|
|
|
|
|
config: rawConfig,
|
|
|
|
|
label,
|
|
|
|
|
screenId,
|
2026-02-23 18:45:21 +09:00
|
|
|
componentId,
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
}: PopSearchComponentProps) {
|
|
|
|
|
const config = { ...DEFAULT_CONFIG, ...(rawConfig || {}) };
|
2026-02-23 18:45:21 +09:00
|
|
|
const { publish, subscribe, setSharedData } = usePopEvent(screenId || "");
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
const [value, setValue] = useState<unknown>(config.defaultValue ?? "");
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
const fieldKey = config.fieldName || componentId || "search";
|
|
|
|
|
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
const emitFilterChanged = useCallback(
|
|
|
|
|
(newValue: unknown) => {
|
|
|
|
|
setValue(newValue);
|
2026-02-23 18:45:21 +09:00
|
|
|
setSharedData(`search_${fieldKey}`, newValue);
|
|
|
|
|
|
|
|
|
|
// 표준 출력 이벤트 (연결 시스템용)
|
|
|
|
|
if (componentId) {
|
|
|
|
|
publish(`__comp_output__${componentId}__filter_value`, {
|
|
|
|
|
fieldName: fieldKey,
|
|
|
|
|
value: newValue,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 레거시 호환
|
|
|
|
|
publish("filter_changed", { [fieldKey]: newValue });
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
},
|
2026-02-23 18:45:21 +09:00
|
|
|
[fieldKey, publish, setSharedData, componentId]
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
);
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
// 외부 값 수신 (스캔 결과, 모달 선택 등)
|
|
|
|
|
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]);
|
|
|
|
|
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
const showLabel = config.labelVisible !== false && !!config.labelText;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex h-full w-full overflow-hidden",
|
|
|
|
|
showLabel && config.labelPosition === "left"
|
|
|
|
|
? "flex-row items-center gap-2 p-1.5"
|
|
|
|
|
: "flex-col justify-center gap-0.5 p-1.5"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{showLabel && (
|
|
|
|
|
<span className="shrink-0 truncate text-[10px] font-medium text-muted-foreground">
|
|
|
|
|
{config.labelText}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<SearchInputRenderer
|
|
|
|
|
config={config}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={emitFilterChanged}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 서브타입 분기 렌더러
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface InputRendererProps {
|
|
|
|
|
config: PopSearchConfig;
|
|
|
|
|
value: unknown;
|
|
|
|
|
onChange: (v: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SearchInputRenderer({ config, value, onChange }: InputRendererProps) {
|
|
|
|
|
switch (config.inputType) {
|
|
|
|
|
case "text":
|
2026-02-23 18:45:21 +09:00
|
|
|
case "number":
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
return (
|
|
|
|
|
<TextSearchInput
|
|
|
|
|
config={config}
|
|
|
|
|
value={String(value ?? "")}
|
|
|
|
|
onChange={onChange}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
case "select":
|
|
|
|
|
return (
|
|
|
|
|
<SelectSearchInput
|
|
|
|
|
config={config}
|
|
|
|
|
value={String(value ?? "")}
|
|
|
|
|
onChange={onChange}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
case "date-preset":
|
|
|
|
|
return <DatePresetSearchInput config={config} value={value} onChange={onChange} />;
|
|
|
|
|
case "toggle":
|
|
|
|
|
return (
|
|
|
|
|
<ToggleSearchInput value={Boolean(value)} onChange={onChange} />
|
|
|
|
|
);
|
|
|
|
|
case "modal-table":
|
|
|
|
|
case "modal-card":
|
|
|
|
|
case "modal-icon-grid":
|
|
|
|
|
return (
|
|
|
|
|
<ModalSearchInput
|
|
|
|
|
config={config}
|
|
|
|
|
value={String(value ?? "")}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
return <PlaceholderInput inputType={config.inputType} />;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 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<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setInputValue(value);
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
const isNumber = config.inputType === "number";
|
|
|
|
|
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
return (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
|
|
|
<Input
|
2026-02-23 18:45:21 +09:00
|
|
|
type={isNumber ? "number" : "text"}
|
|
|
|
|
inputMode={isNumber ? "numeric" : undefined}
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
value={inputValue}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
2026-02-23 18:45:21 +09:00
|
|
|
placeholder={config.placeholder || (isNumber ? "숫자 입력" : "검색어 입력")}
|
feat(pop-search): 검색 컴포넌트 MVP 구현
- pop-search 컴포넌트 신규 추가 (Component, Config, types, index)
- 입력 타입: text, number, date, date-preset, select, multi-select, combo, modal-table, modal-card, modal-icon-grid, toggle
- 디자이너 팔레트, 레지스트리, 타입, 렌더러 라벨 등록
- 기본 그리드 크기 4x2, labelText/labelVisible 설정 지원
- filter_changed 이벤트 발행 (연결 시스템 미적용, 추후 dataFlow 기반으로 전환 예정)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 17:16:38 +09:00
|
|
|
className="h-8 pl-7 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// select 서브타입: 즉시 발행
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface SelectInputProps {
|
|
|
|
|
config: PopSearchConfig;
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (v: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SelectSearchInput({ config, value, onChange }: SelectInputProps) {
|
|
|
|
|
return (
|
|
|
|
|
<Select
|
|
|
|
|
value={value || undefined}
|
|
|
|
|
onValueChange={(v) => onChange(v)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
<SelectValue placeholder={config.placeholder || "선택"} />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{(config.options || []).map((opt) => (
|
|
|
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
|
|
|
{opt.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 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<string, unknown>)
|
|
|
|
|
? (value as Record<string, unknown>).preset
|
|
|
|
|
: value;
|
|
|
|
|
|
|
|
|
|
const handleSelect = (preset: DatePresetOption) => {
|
|
|
|
|
if (preset === "custom") {
|
|
|
|
|
onChange({ preset: "custom", from: "", to: "" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const range = computeDateRange(preset);
|
|
|
|
|
if (range) onChange(range);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{presets.map((preset) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={preset}
|
|
|
|
|
variant={currentPreset === preset ? "default" : "outline"}
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 px-2 text-[10px]"
|
|
|
|
|
onClick={() => handleSelect(preset)}
|
|
|
|
|
>
|
|
|
|
|
{DATE_PRESET_LABELS[preset]}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// toggle 서브타입: Switch + 즉시 발행
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface ToggleInputProps {
|
|
|
|
|
value: boolean;
|
|
|
|
|
onChange: (v: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ToggleSearchInput({ value, onChange }: ToggleInputProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
checked={value}
|
|
|
|
|
onCheckedChange={(checked) => onChange(checked)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{value ? "ON" : "OFF"}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// modal-* 서브타입: readonly 입력 + 아이콘 (MVP: UI만)
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface ModalInputProps {
|
|
|
|
|
config: PopSearchConfig;
|
|
|
|
|
value: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ModalSearchInput({ config, value }: ModalInputProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="flex h-8 cursor-pointer items-center rounded-md border border-input bg-background px-3 transition-colors hover:bg-accent"
|
|
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
>
|
|
|
|
|
<span className="flex-1 truncate text-xs">
|
|
|
|
|
{value || config.placeholder || "선택..."}
|
|
|
|
|
</span>
|
|
|
|
|
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 미구현 서브타입 플레이스홀더
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
function PlaceholderInput({ inputType }: { inputType: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-8 items-center rounded-md border border-dashed border-muted-foreground/30 px-3">
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">
|
|
|
|
|
{inputType} (후속 구현 예정)
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|