-
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 all 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,84 @@ | ||
| 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; | ||
| clickable?: boolean; | ||
| }) => 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,149 @@ | ||
| import { useEffect, useRef, useState } from 'react'; | ||
| import type { KakaoMapInstance, KakaoMaps, KakaoMarker, KakaoOverlay, RepairShop } from './types'; | ||
|
|
||
| const getKakaoMaps = () => (window as Window & { kakao?: { maps?: KakaoMaps } }).kakao?.maps; | ||
|
|
||
| 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 maps = getKakaoMaps(); | ||
|
|
||
| if (!mapRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| const initMap = () => { | ||
| if (!maps || !mapRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| const center = new maps.LatLng(37.5665, 126.978); | ||
| const options = { center, level: 4 }; | ||
|
|
||
| const map = new maps.Map(mapRef.current, options); | ||
| mapInstanceRef.current = map; | ||
| overlayRef.current = new maps.CustomOverlay({ yAnchor: 1, zIndex: 10, clickable: true }); | ||
| maps.event.addListener(map, 'click', () => { | ||
| overlayRef.current?.setMap(null); | ||
| }); | ||
| setIsMapReady(true); | ||
| }; | ||
|
|
||
| if (maps?.load) { | ||
| maps.load(initMap); | ||
| return; | ||
| } | ||
|
|
||
| const timer = window.setInterval(() => { | ||
| if (maps?.load) { | ||
| window.clearInterval(timer); | ||
| 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 maps = getKakaoMaps(); | ||
| const map = mapInstanceRef.current; | ||
| if (!maps || !map) { | ||
| return; | ||
| } | ||
| map.setCenter(new maps.LatLng(lat, lng)); | ||
| }; | ||
|
|
||
| const openOverlayForShop = (shop: RepairShop) => { | ||
| const maps = getKakaoMaps(); | ||
| const map = mapInstanceRef.current; | ||
| if (!maps || !map) { | ||
| return; | ||
| } | ||
|
|
||
| const position = new maps.LatLng(shop.lat, shop.lng); | ||
| map.panTo(position); | ||
| overlayRef.current?.setMap(null); | ||
|
|
||
| const overlay = new maps.CustomOverlay({ | ||
| content: buildOverlayContent(shop), | ||
| position, | ||
| yAnchor: 1, | ||
| zIndex: 10, | ||
| clickable: true, | ||
| }); | ||
|
|
||
| overlay.setMap(map); | ||
| overlayRef.current = overlay; | ||
| }; | ||
|
|
||
| const setMarkers = (shops: RepairShop[]) => { | ||
| const maps = getKakaoMaps(); | ||
| const map = mapInstanceRef.current; | ||
| if (!maps || !map) { | ||
| return; | ||
| } | ||
|
|
||
| clearMarkers(); | ||
| if (shops.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const markerImage = new maps.MarkerImage('/repair-marker.svg', new maps.Size(36, 48), { | ||
| offset: new maps.Point(18, 48), | ||
| }); | ||
|
|
||
| const bounds = new maps.LatLngBounds(); | ||
| shops.forEach((shop) => { | ||
| const position = new maps.LatLng(shop.lat, shop.lng); | ||
| const marker = new maps.Marker({ map, position, image: markerImage }); | ||
| 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.
상수 처리했습니다!