From e2b22bd8e92b7e90e97ffe39886e707f6c2741b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Tue, 14 Jul 2026 09:22:33 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=99=EC=97=AC=EB=84=A3=EA=B8=B0=20?= =?UTF-8?q?=EC=8B=9C=20url=EB=A7=8C=20=EC=B6=94=EC=B6=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchInputForm.tsx | 75 ++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/src/components/SearchInputForm.tsx b/src/components/SearchInputForm.tsx index 9d58ca6..0b36ccd 100644 --- a/src/components/SearchInputForm.tsx +++ b/src/components/SearchInputForm.tsx @@ -18,6 +18,23 @@ const isValidUrl = (string: string): boolean => { } }; +// URL 형태로 판단되는 입력: 프로토콜/www. 시작 또는 도메인 형태(naver.me/xxx 등) +const isUrlLike = (value: string): boolean => { + const trimmed = value.trim(); + return /^(https?:\/\/|www\.)/i.test(trimmed) + || /^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]|$)/i.test(trimmed); +}; + +const sanitizeUrlInput = (value: string): string => { + return value.replace(/[^\x00-\x7F]/g, ''); +}; + +// 텍스트 안에 포함된 첫 번째 URL을 추출 (없으면 null) +const extractUrl = (text: string): string | null => { + const match = text.match(/(https?:\/\/|www\.)[^\s]+/i); + return match ? match[0] : null; +}; + interface SearchInputFormProps { onAnalyze?: (value: string, type: SearchType) => void; onAutocomplete?: (data: AutocompleteRequest) => void; @@ -142,6 +159,47 @@ const SearchInputForm: React.FC = ({ } }; + const applyInputChange = (value: string) => { + setInputValue(value); + setSelectedItem(null); + setHighlightedIndex(-1); + hideOnInput(value); + if (localError) setLocalError(''); + if (debounceRef.current) clearTimeout(debounceRef.current); + if (isValidUrl(value)) { + setAutocompleteResults([]); + setShowAutocomplete(false); + } else { + debounceRef.current = setTimeout(() => handleAutocompleteSearch(value), 300); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + const pastedText = e.clipboardData.getData('text'); + + // 붙여넣은 텍스트에 URL이 포함되어 있고 URL 외 다른 텍스트도 있으면 URL만 추출해서 입력창에 넣기 + const extracted = extractUrl(pastedText); + if (extracted && extracted !== pastedText.trim()) { + e.preventDefault(); + applyInputChange(sanitizeUrlInput(extracted)); + return; + } + + const input = e.currentTarget; + const start = input.selectionStart ?? inputValue.length; + const end = input.selectionEnd ?? inputValue.length; + const merged = inputValue.slice(0, start) + pastedText + inputValue.slice(end); + + // 붙여넣은 후 완성될 전체 값이 URL 형태일 때만 비영문 문자 제거 + if (!isUrlLike(merged)) return; + + const sanitized = sanitizeUrlInput(merged); + if (sanitized === merged) return; + + e.preventDefault(); + applyInputChange(sanitized); + }; + const handleManualButtonClick = () => { if (onManualButtonClick) { onManualButtonClick(); @@ -169,21 +227,8 @@ const SearchInputForm: React.FC = ({ { - const value = e.target.value; - setInputValue(value); - setSelectedItem(null); - setHighlightedIndex(-1); - hideOnInput(value); - if (localError) setLocalError(''); - if (debounceRef.current) clearTimeout(debounceRef.current); - if (isValidUrl(value)) { - setAutocompleteResults([]); - setShowAutocomplete(false); - } else { - debounceRef.current = setTimeout(() => handleAutocompleteSearch(value), 300); - } - }} + onChange={(e) => applyInputChange(e.target.value)} + onPaste={handlePaste} onFocus={() => { setIsFocused(true); if (autocompleteResults.length > 0) setShowAutocomplete(true);