Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/#564 poi 적용 #566

Merged
merged 10 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
semnil5202 marked this conversation as resolved.
Show resolved Hide resolved
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 || '' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public의 index.html 에 있는 키는 못 숨기겠죠?? ㅋㅋㅋㅋ 어차피 도메인 검사를 한다고는 들었는데

},
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
);

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 = ({
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

콘솔 의도하신 부분인가요??

poi 검색 결과가 없으면 에러를 던지는데 showToast 하는게 좋을까요? 아니면 에러를 던지지 않고 suggestions에 UI 적으로 '검색 결과가 없습니다.' 라고 하는게 나을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 setSuggestions로 suggestions를 '검색결과가 없습니다'라고 하면 좋을 것 같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image 이 부분분 에러인거 같은데 제가 확인바로는 input이 비어지는 순간 저 에러가 발생하는데 무시해도 되는 에러라고 보는데 어떻게 생각하시나요?(사실 정확히 뭐가 문젠지 잘 모름)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 에러를 반환하는 경우가 두 가지가 있네요.

  • 검색어에 해당하는 장소가 없을 때 (예시 : ㅁㄴㅇㄹㅈㄷㄹ) -> network 에러는 안나고 콘솔에 에러(정확히는 예외상황)
  • 검색어를 빈 값으로 보낼 때 -> network 에러 발생

첫 번째로 위 두 상황은 모두 검색어에 대한 검색 결과가 없을 때 발생한다는 공통점이 있습니다. 그래서 catch 문에 말씀하신 대로 setSuggestions(['검색결과가 없습니다']); 와 같은 구문이 필요해보입니다. 👍

두 번째로 검색어를 빈문자열로 보내면 network 에러가 발생하고 이는 결과적으로 불필요한 행동이므로 아래 처럼 e.target.value가 빈 값이면 api 요청 자체를 하지 않는 로직을 추가해보면 어떨가 싶습니다. (간단하게 짰습니다.)

// 54번째 줄부터

debounceTimeoutRef.current = setTimeout(
  () => e.target.value !== '' && fetchData(e.target.value),
  500,
);

이렇게 하면 network 에러 발생 안 하고 불필요한 api 호출도 하지 않네여~

Oct-12-2023 14-42-06

}
};

const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.trim() === '') {
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
setSuggestions([]);
setInputValue('');
return;
}

setInputValue(e.target.value);

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

debounceTimeoutRef.current = setTimeout(
() => fetchData(e.target.value),
500,
semnil5202 marked this conversation as resolved.
Show resolved Hide resolved
);
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
};

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);
}}
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
>
{suggestion.name}
<Address $fontSize="small" color="gray" $fontWeight="normal">
{suggestion.upperAddrName} {suggestion.middleAddrName}{' '}
{suggestion.roadName}
</Address>
</SuggestionItem>
))}
</SuggestionsList>
)}
</>
);
};

export default memo(Autocomplete);

jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
const AutocompleteInput = styled(Input)`
width: 100%;
`;

const SuggestionsList = styled.ul`
border: 1px solid #ccc;
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
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}
jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
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[];
}

jiwonh423 marked this conversation as resolved.
Show resolved Hide resolved
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