diff --git a/backend-java/src/main/java/com/tasteby/config/SecurityConfig.java b/backend-java/src/main/java/com/tasteby/config/SecurityConfig.java index 10c2eaa..580549f 100644 --- a/backend-java/src/main/java/com/tasteby/config/SecurityConfig.java +++ b/backend-java/src/main/java/com/tasteby/config/SecurityConfig.java @@ -30,13 +30,14 @@ public class SecurityConfig { .authorizeHttpRequests(auth -> auth // Public endpoints .requestMatchers("/api/health").permitAll() + .requestMatchers("/api/version").permitAll() // #338 — 빌드 정보 공개 .requestMatchers("/api/auth/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/restaurants/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/channels").permitAll() .requestMatchers(HttpMethod.GET, "/api/search").permitAll() .requestMatchers(HttpMethod.GET, "/api/restaurants/*/reviews").permitAll() .requestMatchers("/api/stats/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/daemon/config").permitAll() + // #275 — /api/daemon/config는 admin-only로 변경 (이전 permitAll 제거) // Everything else requires authentication (controller-level admin checks) .anyRequest().authenticated() ) diff --git a/backend-java/src/main/java/com/tasteby/controller/ChannelController.java b/backend-java/src/main/java/com/tasteby/controller/ChannelController.java index eec618a..a7cebb3 100644 --- a/backend-java/src/main/java/com/tasteby/controller/ChannelController.java +++ b/backend-java/src/main/java/com/tasteby/controller/ChannelController.java @@ -62,7 +62,8 @@ public class ChannelController { } try { String id = channelService.create(channelId, channelName, titleFilter); - cache.flush(); + // #333 — 전체 flush 대신 channels 키만 evict (다른 모듈 캐시 보존) + cache.del(cache.makeKey("channels")); return Map.of("id", id, "channel_id", channelId); } catch (DataIntegrityViolationException e) { // #295 — 유니크 충돌을 메시지 문자열 매칭 대신 typed 예외로 감지 (제약명 변경에도 견고). diff --git a/backend-java/src/main/java/com/tasteby/controller/HealthController.java b/backend-java/src/main/java/com/tasteby/controller/HealthController.java index 735c34e..b220d7f 100644 --- a/backend-java/src/main/java/com/tasteby/controller/HealthController.java +++ b/backend-java/src/main/java/com/tasteby/controller/HealthController.java @@ -1,5 +1,6 @@ package com.tasteby.controller; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -8,8 +9,20 @@ import java.util.Map; @RestController public class HealthController { + // #338 — 배포 시 set되는 빌드 정보. 미설정 시 "dev"로 표시. + @Value("${app.build.version:dev}") + private String version; + + @Value("${app.build.commit:unknown}") + private String commit; + @GetMapping("/api/health") public Map health() { return Map.of("status", "ok"); } + + @GetMapping("/api/version") + public Map version() { + return Map.of("version", version, "commit", commit); + } } diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index b997d83..acf899b 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -64,6 +64,11 @@ app: # 0.57은 cohere embed-v4 한국어 시맨틱 적합도 기준 경험값. max-distance: ${SEARCH_MAX_DISTANCE:0.57} + build: + # #338 — 배포 시 deploy.sh가 env로 주입. dev에서는 dev/unknown. + version: ${APP_VERSION:dev} + commit: ${APP_COMMIT:unknown} + daemon: # 인스턴스 차원 스케줄러 활성화. dev/prod가 같은 DB를 공유하므로 # dev .env에 DAEMON_ENABLED=false를 설정해 dev 폴링을 끄고 prod만 동작시킨다. diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ff46e54..9ce6335 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -168,10 +168,15 @@ function findRegionFromCoords( } let best: { country: string; city: string } | null = null; let bestDist = Infinity; + // #320 — 유클리드 거리는 적도/극지에서 경도·위도의 실거리 차이가 커서 왜곡됨. + // cos(lat) 가중치(equirectangular approximation)로 위도 의존 보정. + const cosLat = Math.cos((lat * Math.PI) / 180); for (const g of groups.values()) { const cLat = g.lats.reduce((a, b) => a + b, 0) / g.lats.length; const cLng = g.lngs.reduce((a, b) => a + b, 0) / g.lngs.length; - const dist = (cLat - lat) ** 2 + (cLng - lng) ** 2; + const dLat = cLat - lat; + const dLng = (cLng - lng) * cosLat; + const dist = dLat * dLat + dLng * dLng; if (dist < bestDist) { bestDist = dist; best = { country: g.country, city: g.city }; diff --git a/frontend/src/components/MapView.tsx b/frontend/src/components/MapView.tsx index 85db334..a3ad87e 100644 --- a/frontend/src/components/MapView.tsx +++ b/frontend/src/components/MapView.tsx @@ -209,6 +209,8 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh zIndex={100} >
handleMarkerClick(r)} zIndex={isSelected ? 1000 : 1} > -
+
)} {channelNames.length > 0 && ( -
+
{channelNames.map((ch) => (