-
Notifications
You must be signed in to change notification settings - Fork 4
[Feat] 수리점 페이지 제작 #76
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
The head ref may contain hidden characters: "73-feat/\uC218\uB9AC\uC810-\uD398\uC774\uC9C0-\uC81C\uC791"
Changes from 14 commits
ca40745
db746fb
fe80206
0b3eaa8
cac5f32
980b444
8e6e3a3
28f9ead
195d13d
8b85044
58da8f6
fd6c274
0715c5e
e540c0f
7e8efe2
65ae7a2
9f20394
38e367f
f3fa682
951effc
eca23b4
f072a72
21bb1a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { RepairPage } from '@pages/repair'; | ||
|
|
||
| export default RepairPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as RepairPage } from './ui/RepairPage'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| export type RepairShop = { | ||
| id: string; | ||
| name: string; | ||
| address: string; | ||
| lat: number; | ||
| lng: number; | ||
| phone?: string; | ||
| distance?: number; | ||
| placeUrl?: string; | ||
| }; | ||
|
|
||
| export type KakaoAddressResult = { | ||
| x: string; | ||
| y: string; | ||
| }; | ||
|
|
||
| export type KakaoPlace = { | ||
| id: string; | ||
| place_name: string; | ||
| road_address_name: string; | ||
| address_name: string; | ||
| y: string; | ||
| x: string; | ||
| phone: string; | ||
| distance?: string; | ||
| place_url?: string; | ||
| }; | ||
|
|
||
| export type KakaoPagination = { | ||
| hasNextPage: boolean; | ||
| nextPage: () => void; | ||
| }; | ||
|
|
||
| export type KakaoMapInstance = { | ||
| setCenter: (latLng: unknown) => void; | ||
| setBounds: (bounds: unknown) => void; | ||
| panTo: (latLng: unknown) => void; | ||
| }; | ||
|
|
||
| export type KakaoMarker = { | ||
| setMap: (map: KakaoMapInstance | null) => void; | ||
| }; | ||
|
|
||
| export type KakaoOverlay = { | ||
| setMap: (map: KakaoMapInstance | null) => void; | ||
| }; | ||
|
|
||
| export type KakaoLatLngBounds = { | ||
| extend: (latLng: unknown) => void; | ||
| }; | ||
|
|
||
| export type KakaoMapsServices = { | ||
| Geocoder: new () => { | ||
| addressSearch: (query: string, cb: (result: unknown[], status: string) => void) => void; | ||
| }; | ||
| Places: new () => { | ||
| keywordSearch: ( | ||
| keyword: string, | ||
| cb: (data: unknown[], status: string, pagination: KakaoPagination) => void, | ||
| options: { location: unknown; radius: number } | ||
| ) => void; | ||
| }; | ||
| Status: { OK: string }; | ||
| }; | ||
|
|
||
| export type KakaoMaps = { | ||
| LatLng: new (lat: number, lng: number) => unknown; | ||
| Map: new (container: HTMLElement, options: { center: unknown; level: number }) => KakaoMapInstance; | ||
| CustomOverlay: new (options: { | ||
| yAnchor: number; | ||
| zIndex: number; | ||
| content?: string; | ||
| position?: unknown; | ||
| }) => KakaoOverlay; | ||
|
Comment on lines
+69
to
+75
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. clickable 타입 정의해야 할 것 같습니다 TypeScript 에러 발생할 수 도 있을 것 같습니다...!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 정의해서 추가했습니다! |
||
| Marker: new (options: { map: KakaoMapInstance; position: unknown; image?: unknown }) => KakaoMarker; | ||
| MarkerImage: new (src: string, size: unknown, options: { offset: unknown }) => unknown; | ||
| Size: new (width: number, height: number) => unknown; | ||
| Point: new (x: number, y: number) => unknown; | ||
| LatLngBounds: new () => KakaoLatLngBounds; | ||
| event: { addListener: (target: unknown, event: string, handler: () => void) => void }; | ||
| load?: (callback: () => void) => void; | ||
| services?: KakaoMapsServices; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import type { KakaoMapInstance, KakaoMaps, KakaoMarker, KakaoOverlay, RepairShop } from './types'; | ||
|
|
||
| const buildOverlayContent = (shop: RepairShop) => { | ||
| const phoneLink = shop.phone ? `<a href="tel:${shop.phone}">전화</a>` : ''; | ||
| const routeLink = `https://map.kakao.com/link/to/${encodeURIComponent(shop.name)},${shop.lat},${shop.lng}`; | ||
| const detailLink = shop.placeUrl ? `<a href="${shop.placeUrl}" target="_blank" rel="noreferrer">상세</a>` : ''; | ||
| const actions = [phoneLink, detailLink, `<a href="${routeLink}" target="_blank" rel="noreferrer">길찾기</a>`] | ||
| .filter(Boolean) | ||
| .join(' · '); | ||
|
|
||
| return ` | ||
| <div style="position:relative; transform:translateY(-8px);"> | ||
| <div style="padding:var(--padding-m) var(--spacing-s); font-size:12px; line-height:1.4; width:260px; background:var(--color-green-100); border-radius:var(--radius-m); border:2px solid var(--color-green-700); box-shadow:0 8px 20px rgba(17,203,176,0.18);"> | ||
| <div style="font-weight:700; margin-bottom:var(--spacing-xxxxs); word-break:break-word; color:var(--color-green-900);">${shop.name}</div> | ||
| <div style="color:var(--color-green-800); margin-bottom:6px; word-break:break-word;">${shop.address}</div> | ||
| <div style="color:var(--color-green-900); font-weight:600; word-break:keep-all;">${actions}</div> | ||
| </div> | ||
| <div style="position:absolute; left:50%; bottom:-8px; transform:translateX(-50%); width:0; height:0; border-left:8px solid transparent; border-right:8px solid transparent; border-top:8px solid var(--color-green-100);"></div> | ||
| <div style="position:absolute; left:50%; bottom:-10px; transform:translateX(-50%); width:0; height:0; border-left:9px solid transparent; border-right:9px solid transparent; border-top:9px solid var(--color-green-700);"></div> | ||
| </div> | ||
| `; | ||
| }; | ||
|
|
||
| export const useRepairMap = () => { | ||
| const mapRef = useRef<HTMLDivElement | null>(null); | ||
| const mapInstanceRef = useRef<KakaoMapInstance | null>(null); | ||
| const markersRef = useRef<KakaoMarker[]>([]); | ||
| const overlayRef = useRef<KakaoOverlay | null>(null); | ||
| const [isMapReady, setIsMapReady] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동일한 선언이 중복되고 있는 것 같습니다 상수로 추출하거나 헬퍼 함수로 분리하면 좋을 것 같아요! 😄
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getKakaoMaps() 헬퍼 함수로 분리했습니다! |
||
|
|
||
| if (!mapRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| const initMap = () => { | ||
| if (!kakao?.maps || !mapRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| const center = new kakao.maps.LatLng(37.5665, 126.978); | ||
| const options = { center, level: 4 }; | ||
|
|
||
| const map = new kakao.maps.Map(mapRef.current, options); | ||
| mapInstanceRef.current = map; | ||
| overlayRef.current = new kakao.maps.CustomOverlay({ yAnchor: 1, zIndex: 10, clickable: true }); | ||
| kakao.maps.event.addListener(map, 'click', () => { | ||
| overlayRef.current?.setMap(null); | ||
| }); | ||
| setIsMapReady(true); | ||
| }; | ||
|
|
||
| if (kakao?.maps?.load) { | ||
| kakao.maps.load(initMap); | ||
| return; | ||
| } | ||
|
|
||
| const timer = window.setInterval(() => { | ||
| if (kakao?.maps?.load) { | ||
| window.clearInterval(timer); | ||
| kakao.maps.load(initMap); | ||
| } | ||
| }, 50); | ||
|
|
||
| return () => window.clearInterval(timer); | ||
| }, []); | ||
|
|
||
| const clearMarkers = () => { | ||
| markersRef.current.forEach((marker) => marker.setMap(null)); | ||
| markersRef.current = []; | ||
| }; | ||
|
|
||
| const setCenter = (lat: number, lng: number) => { | ||
| const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao; | ||
| const map = mapInstanceRef.current; | ||
| if (!kakao?.maps || !map) { | ||
| return; | ||
| } | ||
| map.setCenter(new kakao.maps.LatLng(lat, lng)); | ||
| }; | ||
|
|
||
| const openOverlayForShop = (shop: RepairShop) => { | ||
| const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao; | ||
| const map = mapInstanceRef.current; | ||
| if (!kakao?.maps || !map) { | ||
| return; | ||
| } | ||
|
|
||
| const position = new kakao.maps.LatLng(shop.lat, shop.lng); | ||
| map.panTo(position); | ||
| overlayRef.current?.setMap(null); | ||
|
|
||
| const overlay = new kakao.maps.CustomOverlay({ | ||
| content: buildOverlayContent(shop), | ||
| position, | ||
| yAnchor: 1, | ||
| zIndex: 10, | ||
| clickable: true, | ||
| }); | ||
|
|
||
| overlay.setMap(map); | ||
| overlayRef.current = overlay; | ||
| }; | ||
|
|
||
| const setMarkers = (shops: RepairShop[]) => { | ||
| const kakao = (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao; | ||
| const map = mapInstanceRef.current; | ||
| if (!kakao?.maps || !map) { | ||
| return; | ||
| } | ||
|
|
||
| clearMarkers(); | ||
| if (shops.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const markerImage = new kakao.maps.MarkerImage('/repair-marker.svg', new kakao.maps.Size(36, 48), { | ||
| offset: new kakao.maps.Point(18, 48), | ||
| }); | ||
|
|
||
| const bounds = new kakao.maps.LatLngBounds(); | ||
| shops.forEach((shop) => { | ||
| const position = new kakao.maps.LatLng(shop.lat, shop.lng); | ||
| const marker = new kakao.maps.Marker({ map, position, image: markerImage }); | ||
| kakao.maps.event.addListener(marker, 'click', () => { | ||
| openOverlayForShop(shop); | ||
| }); | ||
|
|
||
| markersRef.current.push(marker); | ||
| bounds.extend(position); | ||
| }); | ||
|
|
||
| map.setBounds(bounds); | ||
| }; | ||
|
|
||
| return { | ||
| mapRef, | ||
| isMapReady, | ||
| setCenter, | ||
| setMarkers, | ||
| clearMarkers, | ||
| openOverlayForShop, | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 SDK URL 직접 하드 코딩 되어있는데 상수로 분리하거나 env에 넣어서 관리하면 더 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상수 처리했습니다!