11import { useEffect , useState } from 'react' ;
22
3- import { Map } from 'react-kakao-maps-sdk' ;
3+ import { LocateFixed , Minus , Plus } from 'lucide-react' ;
4+ import { CustomOverlayMap , Map } from 'react-kakao-maps-sdk' ;
45import styles from './mapComponent.module.scss' ;
56
67interface Props {
78 level : number ;
9+ clusters ?: {
10+ regionCode : string ;
11+ regionName : string ;
12+ lat : number ;
13+ lng : number ;
14+ jobPostCount : number ;
15+ } [ ] ;
16+ selectedRegionCode ?: string ;
17+ onSelectRegion ?: ( regionCode : string ) => void ;
818}
919
10- export const MapComponent = ( { level } : Props ) => {
20+ export const MapComponent = ( {
21+ level,
22+ clusters = [ ] ,
23+ selectedRegionCode,
24+ onSelectRegion,
25+ } : Props ) => {
1126 const [ position , setPosition ] = useState < { lat : number ; lng : number } | null > (
1227 null ,
1328 ) ;
29+ const [ map , setMap ] = useState < kakao . maps . Map | null > ( null ) ;
30+ const [ currentLevel , setCurrentLevel ] = useState ( level ) ;
31+
32+ const fallbackPosition = { lat : 37.5665 , lng : 126.978 } ;
33+
34+ const moveToCurrentPosition = ( ) => {
35+ if ( ! navigator . geolocation ) return ;
36+ navigator . geolocation . getCurrentPosition (
37+ pos => {
38+ const nextPosition = {
39+ lat : pos . coords . latitude ,
40+ lng : pos . coords . longitude ,
41+ } ;
42+ setPosition ( nextPosition ) ;
43+ if ( map ) {
44+ map . panTo ( new kakao . maps . LatLng ( nextPosition . lat , nextPosition . lng ) ) ;
45+ }
46+ } ,
47+ err => {
48+ console . error ( '위치 정보를 가져오지 못했습니다.' , err ) ;
49+ } ,
50+ ) ;
51+ } ;
52+
53+ const handleZoomIn = ( ) => {
54+ if ( ! map ) return ;
55+ const nextLevel = Math . max ( 1 , map . getLevel ( ) - 1 ) ;
56+ map . setLevel ( nextLevel ) ;
57+ setCurrentLevel ( nextLevel ) ;
58+ } ;
59+
60+ const handleZoomOut = ( ) => {
61+ if ( ! map ) return ;
62+ const nextLevel = Math . min ( 14 , map . getLevel ( ) + 1 ) ;
63+ map . setLevel ( nextLevel ) ;
64+ setCurrentLevel ( nextLevel ) ;
65+ } ;
1466
1567 useEffect ( ( ) => {
1668 if ( navigator . geolocation ) {
@@ -23,31 +75,94 @@ export const MapComponent = ({ level }: Props) => {
2375 } ,
2476 err => {
2577 console . error ( '위치 정보를 가져오지 못했습니다.' , err ) ;
26- setPosition ( { lat : 37.5665 , lng : 126.978 } ) ;
78+ setPosition ( fallbackPosition ) ;
2779 } ,
2880 ) ;
2981 } else {
30- alert ( '브라우저가 위치 정보를 지원하지 않습니다.' ) ;
31- setPosition ( { lat : 37.5665 , lng : 126.978 } ) ;
82+ setPosition ( fallbackPosition ) ;
3283 }
3384 } , [ ] ) ;
3485
86+ useEffect ( ( ) => {
87+ setCurrentLevel ( level ) ;
88+ } , [ level ] ) ;
89+
90+ const handleSelectCluster = ( cluster : {
91+ regionCode : string ;
92+ lat : number ;
93+ lng : number ;
94+ } ) => {
95+ onSelectRegion ?.( cluster . regionCode ) ;
96+ if ( map ) {
97+ map . panTo ( new kakao . maps . LatLng ( cluster . lat , cluster . lng ) ) ;
98+ }
99+ } ;
100+
35101 return (
36102 < div className = { styles . container } >
103+ < div className = { styles . mapControls } >
104+ < button
105+ type = 'button'
106+ className = { styles . controlButton }
107+ onClick = { moveToCurrentPosition }
108+ aria-label = '현재 위치로 이동'
109+ title = '현재 위치로 이동'
110+ >
111+ < LocateFixed size = { 18 } />
112+ </ button >
113+ < button
114+ type = 'button'
115+ className = { styles . controlButton }
116+ onClick = { handleZoomIn }
117+ aria-label = '지도 확대'
118+ title = '지도 확대'
119+ >
120+ < Plus size = { 18 } />
121+ </ button >
122+ < button
123+ type = 'button'
124+ className = { styles . controlButton }
125+ onClick = { handleZoomOut }
126+ aria-label = '지도 축소'
127+ title = '지도 축소'
128+ >
129+ < Minus size = { 18 } />
130+ </ button >
131+ </ div >
37132 < Map
38133 id = 'map'
39- center = {
40- position || {
41- lat : 37.5665 ,
42- lng : 126.978 ,
43- }
44- }
134+ center = { position || fallbackPosition }
45135 style = { {
46136 width : '100%' ,
47137 height : '600px' ,
48138 } }
49- level = { level }
50- />
139+ level = { currentLevel }
140+ onCreate = { setMap }
141+ >
142+ < CustomOverlayMap position = { position || fallbackPosition } >
143+ < div className = { styles . currentPin } />
144+ </ CustomOverlayMap >
145+ { clusters . map ( cluster => (
146+ < CustomOverlayMap
147+ key = { cluster . regionCode }
148+ position = { { lat : cluster . lat , lng : cluster . lng } }
149+ >
150+ < button
151+ type = 'button'
152+ className = { `${ styles . clusterBadge } ${
153+ selectedRegionCode === cluster . regionCode ? styles . activeCluster : ''
154+ } `}
155+ onClick = { ( ) => handleSelectCluster ( cluster ) }
156+ title = { `${ cluster . regionName } ${ cluster . jobPostCount } 건` }
157+ >
158+ < span className = { styles . clusterRegion } > { cluster . regionName } </ span >
159+ < strong className = { styles . clusterCount } >
160+ { cluster . jobPostCount } 건
161+ </ strong >
162+ </ button >
163+ </ CustomOverlayMap >
164+ ) ) }
165+ </ Map >
51166 </ div >
52167 ) ;
53168} ;
0 commit comments