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

Dev/seoro #15

Merged
merged 4 commits into from
Feb 25, 2024
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
Binary file added public/pasta.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 11 additions & 5 deletions src/app/Map/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use client';

import { useEffect, useState } from 'react';
import Drawer from '@/ui/drawer';
import MapNode from '@/service/MapObject/MapNode';
import searchNearbyPlace from '../../service/search';

function Map() {
const [searchAddress, setSearchAddress] = useState('');
const [searchMapNodes, setSearchMapNodes] = useState<MapNode[]>([]);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchAddress(event.target.value);
};

const handleSearchClick = () => {
searchNearbyPlace(searchAddress);
const handleSearchClick = async () => {
const sortedMapNodes: MapNode[] = await searchNearbyPlace(searchAddress);
setSearchMapNodes(sortedMapNodes);
};
// 아마 차후에 추상팩토리같은 디자인 패턴을 적용해야 하지않을까 싶네요.
const addGoogleMap = (center : google.maps.LatLngLiteral) : google.maps.Map => (
Expand All @@ -28,11 +32,13 @@ function Map() {

return (
<>
<div id="map" style={style} />
<div className="search">
{/* 검색창 잠깐 오른쪽으로 옮겼어요 */}
<div className="search" style={{ textAlign: 'right' }}>
<input type="text" id="address" value={searchAddress} style={{ color: 'black' }} onChange={handleInputChange} />
<input id="submit" type="button" value="검색" onClick={handleSearchClick} />
<input id="submit" type="button" value="검색" style={{ color: 'black' }} onClick={handleSearchClick} />
</div>
<div id="map" style={style} />
<Drawer mapNodes={searchMapNodes} />
</>
);
}
Expand Down
11 changes: 10 additions & 1 deletion src/service/MapObject/MapNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export default class MapNode {
// I. 위치를 가진다.
// II. 평점을 가진다.
// III. 거점의 경우,
id: string; // 구글맵으로부터 가져온 장소의 고유 ID

name: string; // 가게의 이름

location: LocationType; // 가게의 위치를 나타낸다.

scoreInfo: {
Expand All @@ -17,11 +21,14 @@ export default class MapNode {
costScore?:Array<number>; // 가게 자체의 가격 수준.(0~10)

/**
*
* @param id
LuticaCANARD marked this conversation as resolved.
Show resolved Hide resolved
* @param name
* @param location
* @param score
*/
constructor(
id: string,
name: string,
location: LocationType,
score: {
comment?: Array<string>;
Expand All @@ -33,6 +40,8 @@ export default class MapNode {
max_score: 5,
},
) {
this.id = id;
this.name = name;
this.location = location;
this.scoreInfo = score;
const scoreLength = score?.scores?.length;
Expand Down
122 changes: 115 additions & 7 deletions src/service/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function addressToPlaceIds(address: string) : Promise<Array<string>> {
});
resolve(placeIds);
} else {
reject(new Error('Geocoding failed'));
reject(new Error('addressToPlaceIds failed'));
}
});
});
Expand All @@ -55,7 +55,23 @@ function placeIdToCoord(placeId: string) : Promise<google.maps.LatLng> {
const coord = results[0].geometry.location;
resolve(coord);
} else {
reject(new Error('Geocoding failed'));
reject(new Error('placeIdToCoord failed'));
LuticaCANARD marked this conversation as resolved.
Show resolved Hide resolved
}
});
});
}

/**
* 장소 고유 ID로 가게 이름을 얻음
*/
function placeIdToName(placeId: string, map: google.maps.Map) : Promise<string> {
const service = new google.maps.places.PlacesService(map);
return new Promise((resolve, reject) => {
service.getDetails({ placeId }, (result, status) => {
if (status === 'OK') {
resolve(result.name);
} else {
reject(new Error('placeIdToName failed'));
}
});
});
Expand All @@ -80,6 +96,7 @@ function placeIdToTypes(placeId: string, map: google.maps.Map) : Promise<Array<s
});
});
}

/**
* 장소 고유 ID로 별점을 얻음
*/
Expand All @@ -96,6 +113,22 @@ function placeIdToRating(placeId: string, map: google.maps.Map) : Promise<number
});
}

/**
* 장소 고유 ID로 가격 점수를 얻음
*/
function placeIdToPriceLevel(placeId: string, map: google.maps.Map) : Promise<number> {
const service = new google.maps.places.PlacesService(map);
return new Promise((resolve, reject) => {
service.getDetails({ placeId }, (result, status) => {
if (status === 'OK' && result.price_level) {
resolve(result.price_level);
} else {
reject(new Error('placeIdToPriceLevel failed'));
}
});
});
}

/**
* 장소 고유 ID로 리뷰를 얻음
*/
Expand Down Expand Up @@ -139,16 +172,48 @@ function searchNearbyCoords(coord: google.maps.LatLng, map: google.maps.Map)
});
resolve(NearbyCoords);
} else {
reject(new Error('NearbySearch failed'));
reject(new Error('searchNearbyCoords failed'));
}
});
});
}

/**
* 입력 좌표의 반경 200m 맛집의 고유 ID를 얻음
*/
function searchNearbyPlaceIds(coord: google.maps.LatLng, map: google.maps.Map)
: Promise<Array<string>> {
const NearbyPlaceIds : Array<string> = [];
const service = new google.maps.places.PlacesService(map);

return new Promise((resolve, reject) => {
service.nearbySearch({
location: coord,
types: ['restaurant', 'bakery', 'bar', 'cafe'],
radius: 200.0, // 일단 200m로 설정
}, (results, status) => {
if (status === 'OK') {
results.forEach((result) => {
if (result.place_id) {
NearbyPlaceIds.push(result.place_id);
}
});
resolve(NearbyPlaceIds);
} else {
reject(new Error('searchNearbyPlaceIds failed'));
}
});
});
}

/**
* 해당 장소 ID에 대하여 MapNode 객체를 반환
*/
async function getMapNode(placeId: string, map: google.maps.Map) : Promise<MapNode> {
const comment: Array<string> = [];
const scores: Array<number> = [];

const name = await placeIdToName(placeId, map);
const coord = await placeIdToCoord(placeId);
const reviews = await placeIdToReviews(placeId, map);

Expand All @@ -167,20 +232,63 @@ async function getMapNode(placeId: string, map: google.maps.Map) : Promise<MapNo
scores,
};

return new MapNode(location, score);
return new MapNode(placeId, name, location, score);
}

/**
* 장소 ID 배열에 대하여 MapNode 객체 배열을 반환
*/
async function getMapNodes(placeIds: Array<string>, map: google.maps.Map)
: Promise<Array<MapNode>> {
const mapNodePromises : Array<Promise<MapNode | undefined>> = placeIds.map(async (placeId) => {
try {
return await getMapNode(placeId, map);
} catch (error) {
console.log('해당 장소에 대한 리뷰가 존재하지 않아 MapNode를 생성할 수 없습니다.');
}
});

const mapNodeResults = await Promise.all(mapNodePromises);
const mapNodes = mapNodeResults.filter((result): result is MapNode => result !== undefined);
return mapNodes;
}

/**
* MapNode 객체 배열을 점수를 기준으로 내림차순 정렬
*/
function sortMapNodesByScore(mapNodes: Array<MapNode>) : Array<MapNode> {
const sortedMapNodes = [...mapNodes];

sortedMapNodes.sort((left, right) => {
const leftLocation = left.location;
const rightLocation = right.location;
return right.GetScore(leftLocation) - left.GetScore(rightLocation);
});

return sortedMapNodes;
}

/**
* 사용자가 입력한 주소를 기반으로 근처 맛집들의 위치에 마커 추가
*/
export default async function searchNearbyPlace(address: string) {
export default async function searchNearbyPlace(address: string) : Promise<Array<MapNode>> {
const center: google.maps.LatLngLiteral = { lat: 37.3595316, lng: 127.1052133 };
const map = new google.maps.Map(document.getElementById('map') as HTMLElement, {
center,
zoom: 15,
});
const placeIds = await addressToPlaceIds(address);
const coord = await placeIdToCoord(placeIds[0]);
const nearbyCoords = await searchNearbyCoords(coord, map);
addMarkers(nearbyCoords, map);
const nearbyPlaceIds = await searchNearbyPlaceIds(coord, map);
const mapNodes = await getMapNodes(nearbyPlaceIds, map);
const sortedMapNodes = sortMapNodesByScore(mapNodes);

const latLngs: Array<google.maps.LatLng> = [];
sortedMapNodes.forEach((node) => {
const latLng = new google.maps.LatLng(node.location.latitude, node.location.longitude);
latLngs.push(latLng);
});
addMarkers(latLngs, map);

return sortedMapNodes;
}
123 changes: 123 additions & 0 deletions src/ui/MapNodeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import Image from 'next/image';
import MapNode from '@/service/MapObject/MapNode';
import getStarScore from '@/utils/getStarScore';

interface MapNodeCardProps {
index: number;
node: MapNode;
}

function MapNodeCard({ index, node }: MapNodeCardProps) {
const [starsArray, setStarsArray] = useState<number[]>([]);

useEffect(() => {
const score = node.GetScore(node.location);
const star = getStarScore(score);
setStarsArray(Array.from({ length: star }));
}, [node]);

return (
<>
<GlobalStyle />
<MapNodeContainer>
<MapNodeCover>
<MapNodeStar>
{starsArray.map((_, i) => (
<svg key={i} width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3203 8.93597L14.7969 12.011L15.8524 16.5891C15.9082 16.8284 15.8923 17.0789 15.8065 17.3092C15.7208 17.5396 15.5691 17.7395 15.3703 17.8841C15.1716 18.0286 14.9346 18.1114 14.6891 18.122C14.4436 18.1326 14.2004 18.0706 13.9899 17.9438L9.99689 15.5219L6.01252 17.9438C5.80202 18.0706 5.55881 18.1326 5.31328 18.122C5.06775 18.1114 4.83079 18.0286 4.63204 17.8841C4.4333 17.7395 4.28157 17.5396 4.19584 17.3092C4.1101 17.0789 4.09416 16.8284 4.15002 16.5891L5.20392 12.0157L1.6797 8.93597C1.49331 8.7752 1.35852 8.56298 1.29225 8.32592C1.22598 8.08886 1.23117 7.83751 1.30718 7.60339C1.38319 7.36927 1.52663 7.16281 1.71952 7.00988C1.9124 6.85696 2.14614 6.76439 2.39142 6.74378L7.03674 6.34143L8.85002 2.01643C8.94471 1.78949 9.10443 1.59564 9.30907 1.45929C9.51371 1.32294 9.75411 1.25018 10 1.25018C10.2459 1.25018 10.4863 1.32294 10.691 1.45929C10.8956 1.59564 11.0553 1.78949 11.15 2.01643L12.9688 6.34143L17.6125 6.74378C17.8578 6.76439 18.0915 6.85696 18.2844 7.00988C18.4773 7.16281 18.6207 7.36927 18.6968 7.60339C18.7728 7.83751 18.778 8.08886 18.7117 8.32592C18.6454 8.56298 18.5106 8.7752 18.3242 8.93597H18.3203Z" fill="#FFF500" />
</svg>
))}
</MapNodeStar>
<MapNodeIndex>
{index}
</MapNodeIndex>
</MapNodeCover>
<MapNodeContent>
<MapNodeName>
{node.name}
</MapNodeName>
<MapNodeImage>
{/* 임시 사진 */}
<Image src="/pasta.jpeg" height="75" width="75" alt="가게 사진" />
</MapNodeImage>
</MapNodeContent>
</MapNodeContainer>
</>
);
}

const GlobalStyle = createGlobalStyle`
@font-face {
src: url(//fonts.gstatic.com/ea/nanumgothic/v5/NanumGothic-Regular.eot);
font-family: 'Nanum Gothic', serif;
}
`;

const MapNodeContainer = styled.div`
display: flex;
background-color: rgba(255, 255, 255, 0.6);
border-radius: 20px;
width: 230px;
height: 130px;
margin: 4px 0px 4px 0px;
`;

const MapNodeCover = styled.div`
display: flex;
justify-content: space-between;
background-color: #FF4B4B;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
width: 70px;
height: 130px;
`;

const MapNodeContent = styled.div`
display: flex;
flex-direction: column;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
width: 160px;
height: 130px;
`;

const MapNodeIndex = styled.div`
color: white;
font-size: 18px;
font-weight: 600;
padding: 12px 8px 0px 0px;
`;

const MapNodeStar = styled.div`
display: flex;
flex-direction: column;
align-self: start;
padding: 14px 0px 0px 12px;
`;

const MapNodeName = styled.div`
color: #4B3F4E;
font-size: 18px;
font-weight: 600;
padding: 12px 0px 0px 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 8em;
`;

const MapNodeImage = styled.div`
height: 75px;
width: 75px;
margin: 10px 10px 10px 10px;

img {
width: 100%;
height: 100%;
object-fit: cover;
}
`;

export default MapNodeCard;
Loading