From ecfde6c27677d94a42d6d03278b786a8aad91003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=83=9C=EC=9C=A4?= Date: Mon, 28 Oct 2024 00:05:19 +0900 Subject: [PATCH 01/11] add AdressSearch --- src/components/AdressSearch/index.tsx | 184 ++++++++++++++++++++++++++ src/components/AdressSearch/style.ts | 44 ++++++ src/components/Input/index.tsx | 24 +++- src/pages/write/index.tsx | 24 ++-- src/types/input.ts | 3 + 5 files changed, 264 insertions(+), 15 deletions(-) create mode 100644 src/components/AdressSearch/index.tsx create mode 100644 src/components/AdressSearch/style.ts diff --git a/src/components/AdressSearch/index.tsx b/src/components/AdressSearch/index.tsx new file mode 100644 index 0000000..428cdd6 --- /dev/null +++ b/src/components/AdressSearch/index.tsx @@ -0,0 +1,184 @@ +import React, { + useState, + useEffect, +} from 'react'; +import * as S from './style'; +import Input from 'components/Input'; +import { + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from 'react-hook-form'; + +interface AddressSearchProps { + register: UseFormRegister; + setValue: UseFormSetValue; + watch: UseFormWatch; +} + +const AddressSearch = ({ + register, + setValue, + watch, +}: AddressSearchProps) => { + const [results, setResults] = useState< + Array<[string, string, string]> + >([]); + const [errorMessage, setErrorMessage] = + useState(''); + const [pickResult, setPickResult] = + useState(false); + const [showResults, setShowResults] = + useState(false); + const [resultClicked, setResultClicked] = + useState(false); + + const promiseContent = watch('promise'); + + useEffect(() => { + if (!pickResult) { + if (promiseContent) { + searchJuso(promiseContent); + setShowResults(true); + } else { + setResults([]); + setErrorMessage(''); + setShowResults(false); + } + } + }, [promiseContent]); + + const handleInputChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value || ''; + setValue('promise', value); + }; + + const handleInputClick = ( + e: React.MouseEvent, + ) => { + setPickResult(false); + setShowResults(true); + setResultClicked(false); + }; + + const handleInputBlur = () => { + if (!resultClicked) { + setShowResults(false); + } + }; + + const searchJuso = (query: string) => { + const confmKey = + 'devU01TX0FVVEgyMDI0MTAwNjAyMzMwNDExNTEzMTg='; + const apiUrl = createApiUrl(confmKey, query); + + (window as any).callbackFunc = (data: any) => + handleApiResponse(data); + loadApiScript(apiUrl); + }; + + const createApiUrl = ( + confmKey: string, + query: string, + ) => { + return `https://business.juso.go.kr/addrlink/addrLinkApiJsonp.do?confmKey=${confmKey}&keyword=${encodeURIComponent(query)}&resultType=json&callback=callbackFunc`; + }; + + const handleApiResponse = (data: any) => { + if (data.results.common.errorCode === '0') { + const addresses = extractAddresses( + data.results.juso, + ); + setResults(addresses); + setErrorMessage(''); + } else { + setResults([]); + setErrorMessage( + data.results.common.errorMessage, + ); + } + }; + + const extractAddresses = ( + jusoList: any[], + ): [string, string, string][] => { + return jusoList.map( + (item) => + [ + item.emdNm, + item.roadAddrPart1, + item.bdNm, + ] as [string, string, string], + ); + }; + + const loadApiScript = (url: string) => { + const script = + document.createElement('script'); + script.src = url; + document.body.appendChild(script); + }; + + const handleResultMouseDown = ( + e: React.MouseEvent, + result: [string, string, string], + ) => { + e.stopPropagation(); + const selectedAddress = result.join(' '); + setValue('promise', selectedAddress); + setPickResult(true); + setShowResults(false); + setResultClicked(true); + }; + + return ( + + { + handleInputChange(e); + register('promise', { + required: true, + }).onChange(e); + }} + onClick={handleInputClick} + onBlur={(e) => { + handleInputBlur(); + register('promise', { + required: true, + }).onBlur(e); + }} + /> + {promiseContent && showResults && ( + + {results.length > 0 + ? results.map((result, index) => ( + + handleResultMouseDown( + e, + result, + ) + }> + {result[0]}   + {result[1]}   {result[2]} + + )) + : errorMessage && ( +
{errorMessage}
+ )} + +
+ )} +
+ ); +}; + +export default AddressSearch; diff --git a/src/components/AdressSearch/style.ts b/src/components/AdressSearch/style.ts new file mode 100644 index 0000000..07a2068 --- /dev/null +++ b/src/components/AdressSearch/style.ts @@ -0,0 +1,44 @@ +import styled from '@emotion/styled'; + +export const SearchBox = styled.div` + position: relative; +`; + +export const SearchResultsContainer = styled.div` + position: absolute; + top: 79.5px; + width: 100%; + border: 1px solid #eff0f2; + border-radius: 0.75rem; + padding: 28px 24px 0 24px; + box-sizing: border-box; + max-height: 244px; + overflow-y: scroll; + z-index: 1; + background-color: #fff; +`; +export const SearchResultItem = styled.div` + padding: 16px; + border-bottom: 1px solid #eff0f2; + cursor: pointer; + + &:hover { + background-color: #f0f0f0; + } + + color: #b4b5b7; + span { + color: black; + } +`; +export const BottomBlur = styled.div` + position: sticky; + bottom: 0; + left: 0; + right: 0; + height: 50px; + background: rgba(255, 255, 255, 0.8); + filter: blur(4px); + pointer-events: none; + z-index: 2; +`; diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 4dce47b..4eb210d 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -16,6 +16,8 @@ const Input = forwardRef< placeholder, register, onChange, + onClick, + onBlur, defaultValue, icon = false, required = false, @@ -26,6 +28,17 @@ const Input = forwardRef< ref, ) => { const [charCount, setCharCount] = useState(0); + + const handleChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value; + setCharCount(value.length); + if (onChange) { + onChange(e); + } + }; + return (
@@ -45,12 +58,17 @@ const Input = forwardRef< type="text" placeholder={placeholder} defaultValue={defaultValue} + maxLength={ + maxlength > 0 + ? maxlength + : undefined + } {...register} {...props} ref={ref} - onChange={(e) => - setCharCount(e.target.value.length) - } + onChange={handleChange} + onClick={onClick} + onBlur={onBlur} /> {icon && ( diff --git a/src/pages/write/index.tsx b/src/pages/write/index.tsx index 2cf52ad..76e52e3 100644 --- a/src/pages/write/index.tsx +++ b/src/pages/write/index.tsx @@ -12,6 +12,7 @@ import ChevronRight from 'svg/ChevronRight'; import ChoiceList from 'components/Filter/ChoiceList'; import Textarea from 'components/textarea'; import CheckboxList from 'components/CheckboxList'; +import AddressSearch from 'components/AdressSearch'; interface FormValues { title: string; @@ -23,10 +24,13 @@ interface FormValues { gender: string[]; contact: string[]; } - const Write = () => { - const { register, handleSubmit, setValue } = - useForm(); + const { + register, + handleSubmit, + setValue, + watch, + } = useForm(); const [selectedTags, setSelectedTags] = useState([]); const [selectedGrades, setSelectedGrades] = @@ -100,14 +104,10 @@ const Write = () => { required: true, })} /> -