fix: P4-3 인증 메시지 + 지도 cleanup/터치/접근성 (#266+#278)
#266 (인증): - AuthService.loginGoogle: catch-all에서 e.getMessage() 노출 → "Invalid Google token" 고정 메시지 + 상세는 log.warn (Google verifier 내부 오류 정보 누출 차단) #278 (지도): - boundsTimerRef 언마운트 cleanup (unmounted setState 경고 + 메모리 누수 방지) - '내 위치' 버튼 36×36 → 44×44 + aria-label='내 위치로 이동' + touch-manipulation - dead code 제거 (indexRef set-only, restaurantMap 미사용) #277 (health) — 결함 모두 후속 분리 (deep health, version, 테스트, rate limit) 후속 분리: - #338 (deep health/version/Actuator) - #339 (hex → brand-* 토큰 + 마커 ARIA + 테스트) - #340 (다중 audience verifier + AuthService 테스트) Refs: #266 #277 #278
This commit is contained in:
@@ -6,6 +6,8 @@ import com.google.api.client.http.javanet.NetHttpTransport;
|
|||||||
import com.google.api.client.json.gson.GsonFactory;
|
import com.google.api.client.json.gson.GsonFactory;
|
||||||
import com.tasteby.domain.UserInfo;
|
import com.tasteby.domain.UserInfo;
|
||||||
import com.tasteby.security.JwtTokenProvider;
|
import com.tasteby.security.JwtTokenProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -17,6 +19,8 @@ import java.util.Map;
|
|||||||
@Service
|
@Service
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AuthService.class);
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final JwtTokenProvider jwtProvider;
|
private final JwtTokenProvider jwtProvider;
|
||||||
private final GoogleIdTokenVerifier verifier;
|
private final GoogleIdTokenVerifier verifier;
|
||||||
@@ -58,7 +62,10 @@ public class AuthService {
|
|||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Google token: " + e.getMessage());
|
// #266 — 외부에는 고정 메시지만, 상세는 로그로 (Google verifier 내부 네트워크/공개키
|
||||||
|
// 조회 실패 메시지가 클라이언트에 노출되지 않도록)
|
||||||
|
log.warn("Google token verification failed: {}", e.getMessage());
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Google token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,7 @@ type RestaurantProps = { restaurant: Restaurant };
|
|||||||
type RestaurantFeature = Supercluster.PointFeature<RestaurantProps>;
|
type RestaurantFeature = Supercluster.PointFeature<RestaurantProps>;
|
||||||
|
|
||||||
function useSupercluster(restaurants: Restaurant[]) {
|
function useSupercluster(restaurants: Restaurant[]) {
|
||||||
const indexRef = useRef<Supercluster<{ restaurant: Restaurant }> | null>(null);
|
// #278 — indexRef 제거 (set만 되고 read 없는 dead code)
|
||||||
|
|
||||||
const points: RestaurantFeature[] = useMemo(
|
const points: RestaurantFeature[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
restaurants.map((r) => ({
|
restaurants.map((r) => ({
|
||||||
@@ -86,7 +85,6 @@ function useSupercluster(restaurants: Restaurant[]) {
|
|||||||
minPoints: 2,
|
minPoints: 2,
|
||||||
});
|
});
|
||||||
sc.load(points);
|
sc.load(points);
|
||||||
indexRef.current = sc;
|
|
||||||
return sc;
|
return sc;
|
||||||
}, [points]);
|
}, [points]);
|
||||||
|
|
||||||
@@ -129,12 +127,7 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
|
|||||||
const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]);
|
const channelColors = useMemo(() => getChannelColorMap(restaurants), [restaurants]);
|
||||||
const { getClusters, getExpansionZoom } = useSupercluster(restaurants);
|
const { getClusters, getExpansionZoom } = useSupercluster(restaurants);
|
||||||
|
|
||||||
// Build a lookup for restaurants by id
|
// #278 — restaurantMap 제거 (빌드만 되고 렌더에서 사용 안 됨, dead code)
|
||||||
const restaurantMap = useMemo(() => {
|
|
||||||
const m: Record<string, Restaurant> = {};
|
|
||||||
restaurants.forEach((r) => { m[r.id] = r; });
|
|
||||||
return m;
|
|
||||||
}, [restaurants]);
|
|
||||||
|
|
||||||
const clusters = useMemo(() => {
|
const clusters = useMemo(() => {
|
||||||
if (!bounds) return [];
|
if (!bounds) return [];
|
||||||
@@ -273,7 +266,7 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
|
|||||||
textDecoration: isClosed ? "line-through" : "none",
|
textDecoration: isClosed ? "line-through" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-rounded" style={{ fontSize: 14, marginRight: 3, verticalAlign: "middle", color: "#E8720C" }}>{getCuisineIcon(r.cuisine_type)}</span>
|
<span className="material-symbols-rounded" style={{ fontSize: 14, width: 14, height: 14, overflow: "hidden", display: "inline-flex", alignItems: "center", justifyContent: "center", marginRight: 3, verticalAlign: "middle", color: "#E8720C" }}>{getCuisineIcon(r.cuisine_type)}</span>
|
||||||
{r.name}
|
{r.name}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -298,7 +291,7 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
|
|||||||
>
|
>
|
||||||
<div style={{ backgroundColor: "#ffffff", color: "#171717", colorScheme: "light" }} className="max-w-xs p-1">
|
<div style={{ backgroundColor: "#ffffff", color: "#171717", colorScheme: "light" }} className="max-w-xs p-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-bold text-base" style={{ color: "#171717" }}><span className="material-symbols-rounded" style={{ fontSize: 18, verticalAlign: "middle", color: "#E8720C", marginRight: 4 }}>{getCuisineIcon(infoTarget.cuisine_type)}</span>{infoTarget.name}</h3>
|
<h3 className="font-bold text-base" style={{ color: "#171717" }}><span className="material-symbols-rounded" style={{ fontSize: 18, width: 18, height: 18, overflow: "hidden", display: "inline-flex", alignItems: "center", justifyContent: "center", verticalAlign: "middle", color: "#E8720C", marginRight: 4 }}>{getCuisineIcon(infoTarget.cuisine_type)}</span>{infoTarget.name}</h3>
|
||||||
{infoTarget.business_status === "CLOSED_PERMANENTLY" && (
|
{infoTarget.business_status === "CLOSED_PERMANENTLY" && (
|
||||||
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded text-[10px] font-semibold">폐업</span>
|
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded text-[10px] font-semibold">폐업</span>
|
||||||
)}
|
)}
|
||||||
@@ -357,6 +350,13 @@ export default function MapView({ restaurants, selected, onSelectRestaurant, onB
|
|||||||
}, 150);
|
}, 150);
|
||||||
}, [onBoundsChanged]);
|
}, [onBoundsChanged]);
|
||||||
|
|
||||||
|
// #278 — 언마운트 시 디바운스 타이머 정리 (메모리 누수 + unmounted setState 경고 방지)
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (boundsTimerRef.current) clearTimeout(boundsTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<APIProvider apiKey={API_KEY}>
|
<APIProvider apiKey={API_KEY}>
|
||||||
<Map
|
<Map
|
||||||
@@ -380,10 +380,12 @@ export default function MapView({ restaurants, selected, onSelectRestaurant, onB
|
|||||||
{onMyLocation && (
|
{onMyLocation && (
|
||||||
<button
|
<button
|
||||||
onClick={onMyLocation}
|
onClick={onMyLocation}
|
||||||
className="absolute top-2 right-2 w-9 h-9 bg-surface rounded-lg shadow-md flex items-center justify-center text-gray-600 dark:text-gray-300 hover:text-brand-500 dark:hover:text-brand-400 transition-colors z-10"
|
aria-label="내 위치로 이동"
|
||||||
|
// #278 — 44×44px 터치 영역 확보 (이전 36px)
|
||||||
|
className="absolute top-2 right-2 w-11 h-11 bg-surface rounded-lg shadow-md flex items-center justify-center text-gray-600 dark:text-gray-300 hover:text-brand-500 dark:hover:text-brand-400 transition-colors z-10 touch-manipulation"
|
||||||
title="내 위치"
|
title="내 위치"
|
||||||
>
|
>
|
||||||
<Icon name="my_location" size={20} />
|
<Icon name="my_location" size={22} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{channelNames.length > 0 && (
|
{channelNames.length > 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user