diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index f1dab8f4..c810762a 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -775,10 +775,13 @@ export function QuoteRegistration({ options={clients.map((c) => ({ value: c.id, label: c.vendorName }))} value={formData.clientId} onChange={(value, option) => { + const selectedClient = clients.find((c) => c.id === value); setFormData((prev) => ({ ...prev, clientId: value, clientName: option.label, + manager: selectedClient?.managerName || selectedClient?.representativeName || prev.manager, + contact: selectedClient?.managerPhone || selectedClient?.mobile || selectedClient?.phone || prev.contact, })); }} placeholder={isLoadingClients ? "로딩 중..." : "수주처를 선택하세요"} @@ -794,10 +797,12 @@ export function QuoteRegistration({ options={siteNames.map((name) => ({ value: name, label: name }))} value={formData.siteName} onChange={(value) => handleFieldChange("siteName", value)} - placeholder="현장명을 선택하세요" - searchPlaceholder="현장명 검색..." + placeholder="현장명을 선택 또는 입력하세요" + searchPlaceholder="현장명 검색 또는 입력..." emptyText="현장명이 없습니다" disabled={isViewMode} + creatable + creatableText="직접 입력" /> diff --git a/src/components/ui/searchable-select.tsx b/src/components/ui/searchable-select.tsx index 8476b25b..96d55d7f 100644 --- a/src/components/ui/searchable-select.tsx +++ b/src/components/ui/searchable-select.tsx @@ -11,7 +11,7 @@ */ import * as React from 'react'; -import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'; +import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react'; import { cn } from './utils'; import { Button } from './button'; import { Popover, PopoverContent, PopoverTrigger } from './popover'; @@ -45,6 +45,10 @@ interface SearchableSelectProps { isLoading?: boolean; /** 최소 입력 안내 메시지 */ minInputText?: string; + /** 직접 입력 허용 (목록에 없는 값 생성 가능) */ + creatable?: boolean; + /** 직접 입력 시 안내 텍스트 */ + creatableText?: string; } /** @@ -78,12 +82,15 @@ export function SearchableSelect({ onSearch, isLoading = false, minInputText = '한글 1자 또는 영문 2자 이상 입력하세요', + creatable = false, + creatableText = '직접 입력', }: SearchableSelectProps) { const [open, setOpen] = React.useState(false); const [searchQuery, setSearchQuery] = React.useState(''); const debounceRef = React.useRef | null>(null); - const selectedOption = options.find((opt) => opt.value === value); + const selectedOption = options.find((opt) => opt.value === value) + || (creatable && value ? { value, label: value } : undefined); const isServerSearch = !!onSearch; const queryValid = isValidSearchQuery(searchQuery); @@ -147,6 +154,20 @@ export function SearchableSelect({ // 서버 검색 모드에서 검색어 미입력/미충족 시 옵션 숨김 const displayOptions = isServerSearch && !queryValid ? [] : options; + // creatable: 검색어가 기존 옵션에 정확히 매칭되지 않으면 생성 옵션 표시 + const showCreateOption = + creatable && + searchQuery.trim() && + !options.some((opt) => opt.label === searchQuery.trim()); + + const handleCreate = () => { + const trimmed = searchQuery.trim(); + const newOption: SearchableSelectOption = { value: trimmed, label: trimmed }; + onChange(trimmed, newOption); + setOpen(false); + setSearchQuery(''); + }; + return ( @@ -187,7 +208,7 @@ export function SearchableSelect({ ) : ( <> - {getEmptyMessage()} + {!showCreateOption && {getEmptyMessage()}} {displayOptions.map((option) => ( ))} + {showCreateOption && ( + + + + {creatableText}: {searchQuery.trim()} + + + )} )}