Skip to content

Commit

Permalink
feat/#564 poi ์ ์šฉ (#566)
Browse files Browse the repository at this point in the history
* feat: ์ž๋™์™„์„ฑ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

* feat: getPoi ๊ตฌํ˜„

* feat: ์ž๋™์™„์„ฑ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

* feat: ํ•€ ์ถ”๊ฐ€ ํŽ˜์ด์ง€์— ์ž๋™์™„์„ฑ ์ž…๋ ฅ์ฐฝ ์ถ”๊ฐ€

* refactor: ์คŒ ๋ ˆ๋ฒจ ์„ค์ • ์กฐ์ • ๋ฐ ํƒ€์ž… ์ถ”๊ฐ€

* refactor: tmap api key ์ถ”๊ฐ€

* feat: Poi ํƒ€์ž… ์„ค์ •

* feat: ์ž๋™์™„์„ฑ debounce ์ ์šฉ

* refactor: ๋ฆฌ๋ทฐ ๋ฐ˜์˜

* refactor: 2์ฐจ ๋ฆฌ๋ทฐ ๋ฐ˜์˜
  • Loading branch information
jiwonh423 authored Oct 12, 2023
1 parent df6ba08 commit d21a556
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 50 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/fe-merge-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: 'frontend'
cache: "npm"
cache-dependency-path: "frontend"

- name: Install npm
run: npm install
Expand All @@ -35,7 +35,8 @@ jobs:
working-directory: frontend
env:
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS }}
APP_URL: 'https://mapbefine.kro.kr/api'
APP_URL: "https://mapbefine.kro.kr/api"
TMAP_API_KEY: ${{ secrets.TMAP_API_KEY }}

- name: upload to artifact
uses: actions/upload-artifact@v3
Expand Down Expand Up @@ -66,7 +67,7 @@ jobs:

uses: 8398a7/action-slack@v3
with:
mention: 'here'
mention: "here"
if_mention: always
status: ${{ job.status }}
fields: workflow,job,commit,message,ref,author,took
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/apis/getPoiApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PoiApiResponse } from '../types/Poi';

export const getPoiApi = async (query: string): Promise<PoiApiResponse> => {
const response = await fetch(
`https://apis.openapi.sk.com/tmap/pois?version=1&format=json&callback=result&searchKeyword=${query}&resCoordType=WGS84GEO&reqCoordType=WGS84GEO&count=10`,
{
method: 'GET',
headers: { appKey: process.env.TMAP_API_KEY || '' },
},
);

if (response.status >= 400) {
throw new Error('[POI] GET ์š”์ฒญ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}

const responseData = await response.json();

return responseData;
};
133 changes: 133 additions & 0 deletions frontend/src/components/common/Input/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable react/function-component-definition */
import { memo, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';

import { getPoiApi } from '../../../apis/getPoiApi';
import { Poi } from '../../../types/Poi';
import Input from '.';
import Text from '../Text';

interface AutocompleteProps {
defaultValue?: string;
onSuggestionSelected: (suggestion: Poi) => void;
}

const Autocomplete = ({
defaultValue = '',
onSuggestionSelected,
}: AutocompleteProps) => {
const [inputValue, setInputValue] = useState<string>(defaultValue);
const [suggestions, setSuggestions] = useState<Poi[]>([]);
const [selectedSuggestion, setSelectedSuggestion] = useState<
Poi['name'] | null
>(null);

const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const fetchData = async (query: string) => {
try {
const fetchedSuggestions = await getPoiApi(query);

if (!fetchedSuggestions)
throw new Error('์ถ”์ฒœ ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.');

setSuggestions(fetchedSuggestions.searchPoiInfo.pois.poi);
} catch (error) {
setSuggestions([]);
console.error(error);
}
};

const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.trim() === '') {
setSuggestions([]);
setInputValue('');
return;
}

setInputValue(e.target.value);

if (debounceTimeoutRef.current !== null) {
clearTimeout(debounceTimeoutRef.current);
}

debounceTimeoutRef.current = setTimeout(
() => fetchData(e.target.value),
500,
);
};

const onClickSuggestion = (suggestion: Poi) => {
const { name } = suggestion;
setInputValue(name);
setSelectedSuggestion(name);
onSuggestionSelected(suggestion);
};

useEffect(() => {
setInputValue(defaultValue);
}, [defaultValue]);

return (
<>
<AutocompleteInput
type="text"
value={inputValue}
onChange={onInputChange}
placeholder="์žฅ์†Œ๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ์ง€๋„์—์„œ ํด๋ฆญํ•˜์„ธ์š”."
onClick={() => setSelectedSuggestion(null)}
/>

{!selectedSuggestion && (
<SuggestionsList>
{suggestions?.map((suggestion: Poi, index: number) => (
<SuggestionItem
key={index}
onClick={() => {
onClickSuggestion(suggestion);
}}
>
{suggestion.name}
<Address $fontSize="small" color="gray" $fontWeight="normal">
{suggestion.upperAddrName} {suggestion.middleAddrName}{' '}
{suggestion.roadName}
</Address>
</SuggestionItem>
))}
</SuggestionsList>
)}
</>
);
};

export default memo(Autocomplete);

const AutocompleteInput = styled(Input)`
width: 100%;
`;

const SuggestionsList = styled.ul`
border: 1px solid #ccc;
border-top: none;
border-bottom: none;
border-radius: 4px;
box-shadow: 0px 4px 5px -2px rgba(0, 0, 0, 0.3);
`;

const SuggestionItem = styled.li`
padding: ${({ theme }) => theme.spacing['2']};
cursor: pointer;
&:hover {
background-color: #f7f7f7;
}
`;

const Address = styled(Text)``;

const Description = styled.div`
font-size: ${({ theme }) => theme.fontSize.small};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
5 changes: 5 additions & 0 deletions frontend/src/hooks/useClickedCoordinate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ export default function useClickedCoordinate(map: TMap | null) {

useEffect(() => {
if (!map) return;
const currentZoom = map.getZoom();
if (clickedCoordinate.address) displayClickedMarker(map);

// ์„ ํƒ๋œ ์ขŒํ‘œ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ์ขŒํ‘œ๋กœ ์ง€๋„์˜ ์ค‘์‹ฌ์„ ์ด๋™
if (clickedCoordinate.latitude && clickedCoordinate.longitude) {
if (currentZoom <= 17) {
map.setZoom(17);
}

map.panTo(
new Tmapv3.LatLng(
clickedCoordinate.latitude,
Expand Down
64 changes: 18 additions & 46 deletions frontend/src/pages/NewPin.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable no-nested-ternary */
import { FormEvent, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { styled } from 'styled-components';

import { getApi } from '../apis/getApi';
import { getMapApi } from '../apis/getMapApi';
import { postApi } from '../apis/postApi';
import Button from '../components/common/Button';
import Flex from '../components/common/Flex';
import Input from '../components/common/Input';
import Autocomplete from '../components/common/Input/Autocomplete';
import Space from '../components/common/Space';
import Text from '../components/common/Text';
import InputContainer from '../components/InputContainer';
Expand All @@ -24,6 +24,7 @@ import useSetLayoutWidth from '../hooks/useSetLayoutWidth';
import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight';
import useToast from '../hooks/useToast';
import { NewPinFormProps } from '../types/FormValues';
import { Poi } from '../types/Poi';
import { TopicCardProps } from '../types/Topic';
import { hasErrorMessage, hasNullValue } from '../validations';

Expand Down Expand Up @@ -51,7 +52,7 @@ function NewPin() {
const { routePage } = useNavigator();
const { showToast } = useToast();
const { width } = useSetLayoutWidth(SIDEBAR);
const { openModal, closeModal } = useContext(ModalContext);
const { openModal } = useContext(ModalContext);
const { compressImageList } = useCompressImage();

const [formImages, setFormImages] = useState<File[]>([]);
Expand Down Expand Up @@ -131,12 +132,10 @@ function NewPin() {
return;
}
let postTopicId = topic?.id;
let postName = formValues.name;

if (!topic) {
// ํ† ํ”ฝ์ด ์—†์œผ๋ฉด selectedTopic์„ ํ†ตํ•ด ํ† ํ”ฝ์„ ์ƒ์„ฑํ•œ๋‹ค.
postTopicId = selectedTopic?.topicId;
postName = selectedTopic?.topicName;
}

if (postTopicId) routePage(`/topics/${postTopicId}`, [postTopicId]);
Expand All @@ -148,39 +147,6 @@ function NewPin() {
}
};

const onClickAddressInput = (
e:
| React.MouseEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLInputElement>,
) => {
if (!(e.type === 'click') && e.currentTarget.value) return;

const width = 500; // ํŒ์—…์˜ ๋„ˆ๋น„
const height = 600; // ํŒ์—…์˜ ๋†’์ด
new window.daum.Postcode({
width, // ์ƒ์„ฑ์ž์— ํฌ๊ธฐ ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
height,
async onComplete(data: any) {
const addr = data.roadAddress; // ์ฃผ์†Œ ๋ณ€์ˆ˜

// data๋ฅผ ํ†ตํ•ด ๋ฐ›์•„์˜จ ๊ฐ’์„ Tmap api๋ฅผ ํ†ตํ•ด ์œ„๋„์™€ ๊ฒฝ๋„๋ฅผ ๊ตฌํ•œ๋‹ค.
const { ConvertAdd } = await getMapApi<any>(
`https://apis.openapi.sk.com/tmap/geo/convertAddress?version=1&format=json&callback=result&searchTypCd=NtoO&appKey=P2MX6F1aaf428AbAyahIl9L8GsIlES04aXS9hgxo&coordType=WGS84GEO&reqAdd=${addr}`,
);
const lat = ConvertAdd.oldLat;
const lng = ConvertAdd.oldLon;

setClickedCoordinate({
latitude: lat,
longitude: lng,
address: addr,
});
},
}).open({
popupKey: 'postPopUp',
});
};

const onPinImageChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
Expand Down Expand Up @@ -215,6 +181,17 @@ function NewPin() {
setShowedImages(imageUrlLists);
};

const onSuggestionSelected = (suggestion: Poi) => {
const { noorLat, noorLon } = suggestion;
const address = `${suggestion.upperAddrName} ${suggestion.middleAddrName} ${suggestion.roadName}[${suggestion.name}]`;

setClickedCoordinate({
latitude: Number(noorLat),
longitude: Number(noorLon),
address,
});
};

useEffect(() => {
const getTopicId = async () => {
if (topicId && topicId.split(',').length === 1) {
Expand Down Expand Up @@ -329,14 +306,9 @@ function NewPin() {
</Text>
</Flex>
<Space size={0} />
<Input
name="address"
readOnly
value={clickedCoordinate.address}
onClick={onClickAddressInput}
onKeyDown={onClickAddressInput}
tabIndex={2}
placeholder="์ง€๋„๋ฅผ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ์žฅ์†Œ์˜ ์œ„์น˜๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
<Autocomplete
defaultValue={clickedCoordinate.address}
onSuggestionSelected={onSuggestionSelected}
/>

<Space size={5} />
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/types/Poi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export interface EvCharger {
evCharger: any[];
}

export interface NewAddress {
centerLat: string;
centerLon: string;
frontLat: string;
}

export interface NewAddressList {
newAddress: NewAddress[];
}

export interface Poi {
id: string;
pkey: string;
navSeq: string;
collectionType: string;
name: string;

adminDongCode: string;
bizName: string;
dataKind: string;
desc: string;

evChargers: EvCharger;
firstBuildNo: string;
firstNo: string;
frontLat: string;
frontLon: string;

legalDongCode: string;

lowerAddrName: string;

middleAddrName: string;
mlClass: string;

newAddressList: NewAddressList;

noorLat: string;
noorLon: string;

parkFlag: string;

radius: string;

roadName: string;

rpFlag: string;

secondBuildNo: string;

secondNo: string;

telNo: string;

upperAddrName: string;

zipCode: String;
}

export interface Pois {
poi: Poi[];
}

export interface SearchPoiInfo {
totalCount: string;
count: string;
page: string;
pois: Pois;
}

export interface PoiApiResponse {
searchPoiInfo: SearchPoiInfo;
}
Loading

0 comments on commit d21a556

Please sign in to comment.