Skip to content

Commit 9028ee4

Browse files
authored
Merge pull request #116 from th-D138/feat/#113
[FEATURE] 주변 장소 찾기 api 연결 및 주변 기업 찾기 수정
2 parents 448eb35 + dae4202 commit 9028ee4

33 files changed

Lines changed: 2968 additions & 48 deletions

locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"recruitmentInfo": "recruitment",
66
"companiesInfo": "Companies",
77
"nearBy": "Surrounding",
8+
"nearByPlaces": "Nearby Places",
89
"community": "Community",
910
"communityPage": {
1011
"writePost": "Write Post",

locales/ko/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"recruitmentInfo": "채용정보",
66
"companiesInfo": "기업정보",
77
"nearBy": "주변기업찾기",
8+
"nearByPlaces": "주변 장소 찾기",
89
"community": "커뮤니티",
910
"communityPage": {
1011
"writePost": "글쓰기",

src/components/nearby-companies/controlBar/ControlBar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { Dispatch, SetStateAction, useState } from 'react';
2-
import {
3-
selectIndustryOptions,
4-
selectRegionOptions,
5-
} from '@/components/jobs/DetailSearchForm';
2+
import { Option } from '@/components/common/select/Select';
3+
import { selectIndustryOptions } from '@/components/jobs/DetailSearchForm';
64

75
import { ActivateButton } from '@/components/common/activateButton/ActivateButton';
86
import Select from '@/components/common/select/Select';
@@ -21,6 +19,7 @@ interface Props {
2119
setMode: Dispatch<SetStateAction<string>>;
2220
onlyOnRecruitMode: boolean;
2321
setOnlyOnRecruitMode: Dispatch<SetStateAction<boolean>>;
22+
regionOptions: Option[];
2423
}
2524

2625
export const ControlBar = ({
@@ -34,6 +33,7 @@ export const ControlBar = ({
3433
setMode,
3534
setOnlyOnRecruitMode,
3635
setRegion,
36+
regionOptions,
3737
}: Props) => {
3838
void useState;
3939
return (
@@ -45,7 +45,7 @@ export const ControlBar = ({
4545
name='region'
4646
icon='map-pin'
4747
width='300px'
48-
options={selectRegionOptions}
48+
options={regionOptions}
4949
value={region}
5050
onChange={setRegion}
5151
/>
Lines changed: 128 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,68 @@
11
import { 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';
45
import styles from './mapComponent.module.scss';
56

67
interface 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
};
Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,84 @@
11
.container {
2+
position: relative;
23
width: 100%;
34
background-color: #fff;
45
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
56
border: 1px solid var(--color-gray-200);
67
padding: 10px;
7-
margin: 10px;
88
border-radius: 4px;
9+
overflow: hidden;
10+
}
11+
12+
.mapControls {
13+
position: absolute;
14+
bottom: 18px;
15+
right: 18px;
16+
z-index: 2;
17+
display: flex;
18+
flex-direction: column;
19+
gap: 0.4rem;
20+
}
21+
22+
.controlButton {
23+
width: 42px;
24+
height: 42px;
25+
border: 1px solid #c9d8e6;
26+
border-radius: 10px;
27+
background: rgba(255, 255, 255, 0.95);
28+
color: #12344e;
29+
display: inline-flex;
30+
align-items: center;
31+
justify-content: center;
32+
cursor: pointer;
33+
box-shadow: 0 3px 10px rgba(20, 52, 79, 0.16);
34+
}
35+
36+
.controlButton:hover {
37+
background: #f0f8ff;
38+
}
39+
40+
.controlButton:active {
41+
transform: translateY(1px);
42+
}
43+
44+
.currentPin {
45+
width: 12px;
46+
height: 12px;
47+
border-radius: 999px;
48+
background: #0c4a6e;
49+
border: 2px solid #fff;
50+
box-shadow: 0 0 0 5px rgba(12, 74, 110, 0.2);
51+
}
52+
53+
.clusterBadge {
54+
border: 1px solid #bcd4e6;
55+
border-radius: 999px;
56+
background: rgba(255, 255, 255, 0.95);
57+
box-shadow: 0 4px 12px rgba(17, 52, 77, 0.2);
58+
padding: 0.35rem 0.65rem;
59+
display: inline-flex;
60+
align-items: center;
61+
gap: 0.35rem;
62+
cursor: pointer;
63+
white-space: nowrap;
64+
}
65+
66+
.clusterRegion {
67+
font-size: 0.75rem;
68+
color: #34566f;
69+
}
70+
71+
.clusterCount {
72+
font-size: 0.78rem;
73+
color: #0c4a6e;
74+
}
75+
76+
.activeCluster {
77+
border-color: #0c4a6e;
78+
background: #0c4a6e;
79+
}
80+
81+
.activeCluster .clusterRegion,
82+
.activeCluster .clusterCount {
83+
color: #fff;
984
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { fetcher } from '@/lib/fetcher';
4+
5+
interface DeleteFavoritePlaceResponse {
6+
success: string;
7+
data: null;
8+
}
9+
10+
const deleteFavoritePlace = async (placeId: number) => {
11+
return fetcher.delete<DeleteFavoritePlaceResponse>(
12+
`/api/v1/map/favorites/places/${placeId}`,
13+
);
14+
};
15+
16+
const useDeleteFavoritePlace = () => {
17+
const queryClient = useQueryClient();
18+
19+
return useMutation({
20+
mutationKey: ['useDeleteFavoritePlace'],
21+
mutationFn: deleteFavoritePlace,
22+
onSuccess: () => {
23+
queryClient.invalidateQueries({ queryKey: ['useGetMapFavorites'] });
24+
},
25+
});
26+
};
27+
28+
export default useDeleteFavoritePlace;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { fetcher } from '@/lib/fetcher';
4+
5+
interface DeleteFavoriteRegionResponse {
6+
success: string;
7+
data: null;
8+
}
9+
10+
const deleteFavoriteRegion = async (regionCode: string) => {
11+
return fetcher.delete<DeleteFavoriteRegionResponse>(
12+
`/api/v1/map/favorites/regions/${regionCode}`,
13+
);
14+
};
15+
16+
const useDeleteFavoriteRegion = () => {
17+
const queryClient = useQueryClient();
18+
19+
return useMutation({
20+
mutationKey: ['useDeleteFavoriteRegion'],
21+
mutationFn: deleteFavoriteRegion,
22+
onSuccess: () => {
23+
queryClient.invalidateQueries({ queryKey: ['useGetMapFavorites'] });
24+
},
25+
});
26+
};
27+
28+
export default useDeleteFavoriteRegion;

0 commit comments

Comments
 (0)