Skip to content

Commit 2a58aef

Browse files
ziuneeeeeeeziunscholar-star
authored
Frontend/seunga/design (#17)
# 변경점 👍 1 현재 나의 위치 범위 250 내의 각 카테고리 당 램덤한 7개의 가게 정보 불러옴 2 현재 위치 반경 250m 내의 각 카테고리 에서 7개 불러온 가게 중 랜덤하게 하나씩 뽑아 지도에 띄워주고 전체 카테고리에 띄워주기 3 길찾기 경로에 해당하는 거리와 시간 계산 탐색하기 버튼 누르면 길찾기 경로 그려줌 - 같은 경로추천 카테고리에서 한번 더 탐색하기 누르면 다시 랜덤 경유지 뽑아와서 경로 그려줌 - 다른 카테고리 탐색하기 누르면 미리 불러와둔 경로 정보로 바로 그려줌 4 로딩화면 구성 5 경유지 마커 제거 및 선 스타일 수정 # 비고 ✏ 기말전에 작업 했던걸 이제서야.. 그래서 추가해놓고 기억이 안나는 기능들도 있을 수 있으나 버그가 생긴다면 곧바로 수정하겠습니다. --------- Co-authored-by: ziun <jiwun3041@gmail.com> Co-authored-by: scholar-star <cheve1224@pusan.ac.kr>
1 parent a589b63 commit 2a58aef

9 files changed

Lines changed: 546 additions & 344 deletions

File tree

backend/src/main/java/_team/onmyway/controller/RouteController.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package _team.onmyway.controller;
22

3-
import _team.onmyway.dto.AllCategoryRecommendationsDTO;
43
import _team.onmyway.dto.PositionDTO;
54
import _team.onmyway.dto.RouteResponseDTO;
6-
import _team.onmyway.service.RecommendationService;
75
import _team.onmyway.service.RouteService;
8-
import com.fasterxml.jackson.databind.JsonNode;
96
import com.fasterxml.jackson.databind.ObjectMapper;
107
import com.fasterxml.jackson.databind.node.ObjectNode;
118
import lombok.RequiredArgsConstructor;
@@ -15,7 +12,6 @@
1512
import org.springframework.web.bind.annotation.RequestBody;
1613
import org.springframework.web.bind.annotation.RequestMapping;
1714
import org.springframework.web.bind.annotation.RestController;
18-
import reactor.core.publisher.Mono;
1915

2016
import java.util.List;
2117
import java.util.function.Function;
@@ -25,7 +21,6 @@
2521
@RequiredArgsConstructor
2622
public class RouteController {
2723
private final RouteService routeService;
28-
private final RecommendationService recommendationService;
2924
private final ObjectMapper objectMapper;
3025

3126
@PostMapping("/findOut")

frontend/ganeungil/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
33
import Layout from "./components/Layout";
44
import Onboard_new from "./pages/home/Onboard_1.0";
55
import Onboard20 from "./pages/find-route/Onboard_2.0";
6+
import Loading20 from "./pages/find-route/Loading_2.0";
67
import Onboard30 from "./pages/explore/Onboard_3.0";
78
import DiscoverPage from "./pages/discover/discover_page";
89
import LoginPage from "./pages/signup/LoginPage";
@@ -31,6 +32,7 @@ function AppRoutes() {
3132
<Route path="/explore" element={<Onboard30 />} />
3233
<Route path="/discover" element={<DiscoverPage />} />
3334
<Route path="/login" element={<LoginPage />} />
35+
<Route path="/loading" element={<Loading20 />} />
3436
</Route>
3537
</Routes>
3638
</BrowserRouter>

frontend/ganeungil/components/Header.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function Header() {
4646
- transformOrigin: top left → 왼쪽 상단 기준으로 축소
4747
*/}
4848
<div
49-
className="flex items-center pl-[30%] pr-[18.2%] pt-[57px]"
49+
className="flex items-center px-[352px] pt-[57px]"
5050
style={{
5151
transform: `scale(${scale})`,
5252
transformOrigin: "top left",

frontend/ganeungil/hooks/useRoute.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function useRoute(kakaoMapRef) {
3333
});
3434
};
3535

36-
const displayRoute = (features) => {
36+
const displayRoute = (features, modeId, padding = {}) => {
3737
const map = kakaoMapRef.current;
3838
if (!map || !features) return;
3939

@@ -54,16 +54,17 @@ export function useRoute(kakaoMapRef) {
5454
const polyline = new window.kakao.maps.Polyline({
5555
path,
5656
strokeWeight: 5,
57-
strokeColor: "#7BC4A0",
58-
strokeOpacity: 0.8,
57+
strokeColor: "rgba(123, 196, 160, 0.75)",
58+
strokeOpacity: 1,
5959
});
6060
polyline.setMap(map);
6161
polylineRef.current = polyline;
6262
routeMarkersRef.current = [];
6363

6464
const bounds = new window.kakao.maps.LatLngBounds();
6565
path.forEach(pos => bounds.extend(pos));
66-
map.setBounds(bounds);
66+
const { top = 60, right = 60, bottom = 60, left = 60 } = padding;
67+
map.setBounds(bounds, top, right, bottom, left);
6768
};
6869

6970
const clearRoute = () => {
Lines changed: 148 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useRef } from "react";
1+
import { useState, useRef, useEffect } from "react";
22
import api from "../api/api";
33

44
// 점 → 선분(segment) 최단거리 (미터). 좌표계: equirectangular 근사
@@ -35,38 +35,112 @@ function sampleRouteCoords(coords, maxN) {
3535
}
3636

3737
export const ROUTE_MODES = [
38-
{ id: "right", label: "바른 길", endpoint: "/route/right" },
39-
{ id: "slow", label: "여유로운 길", endpoint: "/route/slow" },
40-
{ id: "findOut", label: "발견하는 길", endpoint: "/route/findOut" },
38+
{ id: "findOut", label: "바른 길", endpoint: "/route/right" },
39+
{ id: "slow", label: "여유로운 길", endpoint: "/route/slow" },
40+
{ id: "right", label: "발견하는 길", endpoint: "/route/findOut" },
4141
];
4242

43-
export function useRouteSearch({ userCoords, onDestinationSelect, onDrawRoute, onRouteRecs, onRecsHide, onRecsShow, onDestinationClear }) {
44-
const [destText, setDestText] = useState("");
45-
const [destFocused, setDestFocused] = useState(false);
46-
const [deptText, setDeptText] = useState("");
47-
const [deptFocused, setDeptFocused] = useState(false);
48-
const [searchResults, setSearchResults] = useState([]);
49-
const [selectedResult, setSelectedResult] = useState(null);
50-
const [isSearching, setIsSearching] = useState(false);
51-
const [selectedMode, setSelectedMode] = useState(null);
52-
const [routeInfo, setRouteInfo] = useState({});
53-
54-
const destInputRef = useRef(null);
55-
const deptInputRef = useRef(null);
56-
57-
const isSearchMode = destFocused || deptFocused;
58-
const showResults = searchResults.length > 0;
43+
export function useRouteSearch({ userCoords, onDestinationSelect, onDrawRoute, onRecsHide, onRecsShow, onDestinationClear, onRouteLoadingChange }) {
44+
const [destText, setDestText] = useState("");
45+
const [destFocused, setDestFocused] = useState(false);
46+
const [deptText, setDeptText] = useState("");
47+
const [deptFocused, setDeptFocused] = useState(false);
48+
const [searchResults, setSearchResults] = useState([]);
49+
const [deptSearchResults, setDeptSearchResults] = useState([]);
50+
const [selectedResult, setSelectedResult] = useState(null);
51+
const [customDeptCoords, setCustomDeptCoords] = useState(null); // null = userCoords 사용
52+
const [isSearching, setIsSearching] = useState(false);
53+
const [selectedMode, setSelectedMode] = useState("slow");
54+
const [exploredMode, setExploredMode] = useState(null);
55+
const [routeStats, setRouteStats] = useState({});
56+
const [routeFeatures, setRouteFeatures] = useState({});
57+
58+
const destInputRef = useRef(null);
59+
const deptInputRef = useRef(null);
60+
const debounceRef = useRef(null);
61+
const deptDebounceRef = useRef(null);
62+
63+
const isSearchMode = destFocused || deptFocused;
64+
const showResults = searchResults.length > 0;
65+
const showDeptResults = deptSearchResults.length > 0;
66+
67+
// 목적지 debounce 자동완성 (350ms)
68+
useEffect(() => {
69+
if (!destText.trim() || !destFocused) {
70+
setSearchResults([]);
71+
return;
72+
}
73+
clearTimeout(debounceRef.current);
74+
debounceRef.current = setTimeout(() => {
75+
if (!window.kakao?.maps?.services) return;
76+
setIsSearching(true);
77+
const ps = new window.kakao.maps.services.Places();
78+
ps.keywordSearch(destText, (results, status) => {
79+
setIsSearching(false);
80+
if (status === window.kakao.maps.services.Status.OK) setSearchResults(results.slice(0, 5));
81+
else setSearchResults([]);
82+
});
83+
}, 350);
84+
return () => clearTimeout(debounceRef.current);
85+
}, [destText, destFocused]);
86+
87+
// 출발지 debounce 자동완성 (350ms)
88+
useEffect(() => {
89+
if (!deptText.trim() || !deptFocused) {
90+
setDeptSearchResults([]);
91+
return;
92+
}
93+
clearTimeout(deptDebounceRef.current);
94+
deptDebounceRef.current = setTimeout(() => {
95+
if (!window.kakao?.maps?.services) return;
96+
const ps = new window.kakao.maps.services.Places();
97+
ps.keywordSearch(deptText, (results, status) => {
98+
if (status === window.kakao.maps.services.Status.OK) setDeptSearchResults(results.slice(0, 5));
99+
else setDeptSearchResults([]);
100+
});
101+
}, 350);
102+
return () => clearTimeout(deptDebounceRef.current);
103+
}, [deptText, deptFocused]);
59104

60105
const handleDestFocus = () => { setDestFocused(true); onRecsHide?.(); };
61106
const handleDeptFocus = () => { setDeptFocused(true); onRecsHide?.(); };
62107

63108
const handleCancel = () => {
64109
setDestText(""); setDeptText("");
65110
setDestFocused(false); setDeptFocused(false);
66-
setSearchResults([]); setSelectedResult(null);
111+
setSearchResults([]); setDeptSearchResults([]);
112+
setSelectedResult(null); setCustomDeptCoords(null);
113+
setRouteStats({}); setRouteFeatures({}); setExploredMode(null);
67114
onRecsShow?.(); onDestinationClear?.();
68115
};
69116

117+
// 출발지 결과 선택
118+
const handleDeptResultClick = (result) => {
119+
const newCoords = { lat: parseFloat(result.y), lng: parseFloat(result.x) };
120+
setDeptText(result.place_name);
121+
setCustomDeptCoords(newCoords);
122+
setDeptSearchResults([]);
123+
setDeptFocused(false);
124+
setRouteStats({}); setRouteFeatures({}); setExploredMode(null);
125+
// 목적지가 이미 선택된 상태면 새 출발지로 경로 자동 재탐색
126+
if (selectedResult) {
127+
Promise.all(ROUTE_MODES.map(m => fetchOne(m.id, selectedResult, newCoords)));
128+
}
129+
};
130+
131+
// 출발지 초기화 (현재위치 / 부산대정문으로 복귀)
132+
const handleDeptClear = () => {
133+
setDeptText("");
134+
setCustomDeptCoords(null);
135+
setDeptSearchResults([]);
136+
setDeptFocused(false);
137+
setRouteStats({}); setRouteFeatures({}); setExploredMode(null);
138+
// 목적지가 이미 선택된 상태면 기본 출발지로 경로 자동 재탐색
139+
if (selectedResult && userCoords) {
140+
Promise.all(ROUTE_MODES.map(m => fetchOne(m.id, selectedResult, userCoords)));
141+
}
142+
};
143+
70144
const handleDestSubmit = (e) => {
71145
e?.preventDefault();
72146
if (!destText.trim() || !window.kakao?.maps?.services) return;
@@ -81,113 +155,88 @@ export function useRouteSearch({ userCoords, onDestinationSelect, onDrawRoute, o
81155

82156
const handleDeptSubmit = (e) => { e?.preventDefault(); };
83157

84-
const fetchRouteData = async (modeId, result, { draw = true } = {}) => {
85-
if (!userCoords || !result) {
86-
console.warn("[경로] 중단 - userCoords:", userCoords, "result:", result);
87-
return;
88-
}
158+
// 단일 모드 fetch (startCoordsOverride: 출발지 변경 직후 state 반영 전 명시적으로 전달)
159+
const fetchOne = async (modeId, result, startCoordsOverride) => {
160+
const startCoords = startCoordsOverride ?? customDeptCoords ?? userCoords;
161+
if (!startCoords || !result) return null;
89162
const mode = ROUTE_MODES.find(m => m.id === modeId);
90-
console.log(`[경로] ${mode.label} 요청`, { from: userCoords, to: { y: result.y, x: result.x } });
91163
try {
92164
const response = await api.post(mode.endpoint, [
93-
{ lat: userCoords.lat, lon: userCoords.lng },
165+
{ lat: startCoords.lat, lon: startCoords.lng },
94166
{ lat: parseFloat(result.y), lon: parseFloat(result.x) },
95167
]);
96-
console.log("[경로] 응답:", response.data);
97168
const features = response.data.route?.features;
98-
console.log("[경로] features:", features?.length, "개");
99-
if (features && Array.isArray(features)) {
100-
if (draw) onDrawRoute?.(features);
101-
const summary = features[0]?.properties;
102-
if (summary?.totalTime != null) {
103-
setRouteInfo(prev => ({
104-
...prev,
105-
[modeId]: {
106-
time: Math.round(summary.totalTime / 60),
107-
distance: (summary.totalDistance / 1000).toFixed(1),
108-
},
109-
}));
110-
}
111-
const cats = response.data.recommendations?.categories ?? [];
112-
if (modeId === "findOut") {
113-
const routeCoords = features
114-
.filter(f => f.geometry.type === "LineString")
115-
.flatMap(f => f.geometry.coordinates); // [lng, lat]
116-
117-
// 경로를 따라 최대 6개 지점 샘플 → 각 지점마다 /places/recommend 호출
118-
const samples = sampleRouteCoords(routeCoords, 6);
119-
const results = await Promise.all(
120-
samples.map(([lng, lat]) =>
121-
api.get("/places/recommend", { params: { lat, lng } })
122-
.then(r => r.data.categories ?? [])
123-
.catch(() => [])
124-
)
125-
);
126-
127-
// 전체 장소 합치기 + lat/lng 기준 중복 제거
128-
const seen = new Set();
129-
const allPlaces = results
130-
.flat()
131-
.flatMap(cat => cat.places ?? [])
132-
.filter(p => {
133-
if (!p.lat || !p.lng) return false;
134-
const key = `${p.lat},${p.lng}`;
135-
if (seen.has(key)) return false;
136-
seen.add(key);
137-
return true;
138-
});
139-
140-
const nearby = filterPlacesNearRoute(allPlaces, routeCoords, 50);
141-
142-
// 카테고리별 1개씩만
143-
const seenCat = new Set();
144-
const onePerCategory = nearby.filter(p => {
145-
if (seenCat.has(p.category)) return false;
146-
seenCat.add(p.category);
147-
return true;
148-
});
149-
150-
console.log("[발견하는 길] 수집 장소:", allPlaces.length, "→ 50m 이내:", nearby.length, "→ 카테고리별 1개:", onePerCategory.length);
151-
if (draw) onRouteRecs?.(onePerCategory);
152-
} else {
153-
if (draw) onRouteRecs?.([]);
154-
}
155-
} else {
156-
console.warn("[경로] features 없음. 응답 구조:", JSON.stringify(response.data).slice(0, 200));
169+
if (!features || !Array.isArray(features)) return null;
170+
171+
const props = features[0]?.properties;
172+
if (props?.totalDistance != null && props?.totalTime != null) {
173+
setRouteStats(prev => ({
174+
...prev,
175+
[modeId]: { distance: props.totalDistance, time: props.totalTime },
176+
}));
157177
}
178+
setRouteFeatures(prev => ({ ...prev, [modeId]: features }));
179+
return features;
158180
} catch (error) {
159-
console.error("[경로] API 에러:", error.response?.status, error.response?.data ?? error.message);
181+
console.error(`[경로] ${modeId} 에러:`, error.response?.status, error.response?.data ?? error.message);
182+
return null;
160183
}
161184
};
162185

163-
const handleResultClick = (result) => {
186+
// 목적지 선택 시 3개 모드 동시 fetch (지도에는 그리지 않음)
187+
const handleResultClick = async (result) => {
164188
setSelectedResult(result);
165189
setSearchResults([]);
166190
setDestText(result.place_name);
191+
setExploredMode(null);
167192
onDestinationSelect?.(result);
193+
if (!customDeptCoords && !userCoords) { alert("현재 위치를 먼저 잡아주세요."); return; }
194+
195+
await Promise.all(ROUTE_MODES.map(m => fetchOne(m.id, result)));
168196
};
169197

198+
// 모드 카드 클릭: 선택만 변경
170199
const handleModeChange = (modeId) => {
171200
setSelectedMode(modeId);
172201
};
173202

174-
const handleSearch = () => {
175-
if (!userCoords) { alert("현재 위치를 먼저 잡아주세요."); return; }
203+
// 탐색하기:
204+
// - 처음 or 다른 모드 → 캐시된 경로 즉시 사용
205+
// - 같은 모드로 재클릭 → 새로 fetch (랜덤 재탐색)
206+
const handleExplore = () => {
176207
if (!selectedResult) return;
177-
const activeMode = selectedMode ?? "slow";
178-
ROUTE_MODES.forEach(mode => {
179-
fetchRouteData(mode.id, selectedResult, { draw: mode.id === activeMode });
180-
});
208+
if (exploredMode === selectedMode) {
209+
onRouteLoadingChange?.(true);
210+
fetchOne(selectedMode, selectedResult).then(f => {
211+
if (f) onDrawRoute?.(f, selectedMode);
212+
onRouteLoadingChange?.(false);
213+
});
214+
} else {
215+
const cached = routeFeatures[selectedMode];
216+
if (cached) {
217+
onDrawRoute?.(cached, selectedMode);
218+
setExploredMode(selectedMode);
219+
} else {
220+
onRouteLoadingChange?.(true);
221+
fetchOne(selectedMode, selectedResult).then(f => {
222+
if (f) { onDrawRoute?.(f, selectedMode); setExploredMode(selectedMode); }
223+
onRouteLoadingChange?.(false);
224+
});
225+
}
226+
}
181227
};
182228

183229
return {
184230
destText, setDestText, destFocused,
185231
deptText, setDeptText, deptFocused,
186-
searchResults, selectedResult, isSearching, selectedMode, routeInfo, handleSearch,
232+
searchResults, deptSearchResults,
233+
selectedResult, customDeptCoords,
234+
isSearching, selectedMode,
187235
destInputRef, deptInputRef,
188-
isSearchMode, showResults,
236+
isSearchMode, showResults, showDeptResults, routeStats,
189237
handleDestFocus, handleDeptFocus, handleCancel,
190238
handleDestSubmit, handleDeptSubmit,
191-
handleResultClick, handleModeChange,
239+
handleDeptResultClick, handleDeptClear,
240+
handleResultClick, handleModeChange, handleExplore,
192241
};
193242
}

0 commit comments

Comments
 (0)