Compare commits
2 Commits
3304b9c54f
...
f126664117
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f126664117 | ||
|
|
a0e8878d9a |
@@ -6,6 +6,13 @@
|
|||||||
|
|
||||||
## 2026-06-15
|
## 2026-06-15
|
||||||
|
|
||||||
|
### 🔧 P5-2 작은 후속 (v0.1.26)
|
||||||
|
- #338: /api/version 신규 (HealthController + permitAll), application.yml app.build.{version,commit} env 주입 준비
|
||||||
|
- #320: findRegionFromCoords 거리 보정 (유클리드 → cos(lat) 가중치)
|
||||||
|
- #340: MapView 클러스터/마커/범례에 role/aria-label
|
||||||
|
- #333: ChannelController cache.flush() → cache.del("channels") (다른 모듈 캐시 보존)
|
||||||
|
- Refs: #338 #320 #340 #333 (close)
|
||||||
|
|
||||||
### 🧹 P5-1 작은 후속 묶음 (v0.1.24)
|
### 🧹 P5-1 작은 후속 묶음 (v0.1.24)
|
||||||
- #325: ThreadLocalRandom 통일, rebuildVectors not_implemented 이벤트, getTranscript JavaDoc 명세
|
- #325: ThreadLocalRandom 통일, rebuildVectors not_implemented 이벤트, getTranscript JavaDoc 명세
|
||||||
- #319: buildSearchQuery 헬퍼 + fn-doc(BottomSheet snap 정책)
|
- #319: buildSearchQuery 헬퍼 + fn-doc(BottomSheet snap 정책)
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ public class SecurityConfig {
|
|||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// Public endpoints
|
// Public endpoints
|
||||||
.requestMatchers("/api/health").permitAll()
|
.requestMatchers("/api/health").permitAll()
|
||||||
|
.requestMatchers("/api/version").permitAll() // #338 — 빌드 정보 공개
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/restaurants/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/restaurants/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/search").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/search").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/restaurants/*/reviews").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/restaurants/*/reviews").permitAll()
|
||||||
.requestMatchers("/api/stats/**").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)
|
// Everything else requires authentication (controller-level admin checks)
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ public class ChannelController {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String id = channelService.create(channelId, channelName, titleFilter);
|
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);
|
return Map.of("id", id, "channel_id", channelId);
|
||||||
} catch (DataIntegrityViolationException e) {
|
} catch (DataIntegrityViolationException e) {
|
||||||
// #295 — 유니크 충돌을 메시지 문자열 매칭 대신 typed 예외로 감지 (제약명 변경에도 견고).
|
// #295 — 유니크 충돌을 메시지 문자열 매칭 대신 typed 예외로 감지 (제약명 변경에도 견고).
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.tasteby.controller;
|
package com.tasteby.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -8,8 +9,20 @@ import java.util.Map;
|
|||||||
@RestController
|
@RestController
|
||||||
public class HealthController {
|
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")
|
@GetMapping("/api/health")
|
||||||
public Map<String, String> health() {
|
public Map<String, String> health() {
|
||||||
return Map.of("status", "ok");
|
return Map.of("status", "ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/version")
|
||||||
|
public Map<String, String> version() {
|
||||||
|
return Map.of("version", version, "commit", commit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ app:
|
|||||||
# 0.57은 cohere embed-v4 한국어 시맨틱 적합도 기준 경험값.
|
# 0.57은 cohere embed-v4 한국어 시맨틱 적합도 기준 경험값.
|
||||||
max-distance: ${SEARCH_MAX_DISTANCE:0.57}
|
max-distance: ${SEARCH_MAX_DISTANCE:0.57}
|
||||||
|
|
||||||
|
build:
|
||||||
|
# #338 — 배포 시 deploy.sh가 env로 주입. dev에서는 dev/unknown.
|
||||||
|
version: ${APP_VERSION:dev}
|
||||||
|
commit: ${APP_COMMIT:unknown}
|
||||||
|
|
||||||
daemon:
|
daemon:
|
||||||
# 인스턴스 차원 스케줄러 활성화. dev/prod가 같은 DB를 공유하므로
|
# 인스턴스 차원 스케줄러 활성화. dev/prod가 같은 DB를 공유하므로
|
||||||
# dev .env에 DAEMON_ENABLED=false를 설정해 dev 폴링을 끄고 prod만 동작시킨다.
|
# dev .env에 DAEMON_ENABLED=false를 설정해 dev 폴링을 끄고 prod만 동작시킨다.
|
||||||
|
|||||||
@@ -168,10 +168,15 @@ function findRegionFromCoords(
|
|||||||
}
|
}
|
||||||
let best: { country: string; city: string } | null = null;
|
let best: { country: string; city: string } | null = null;
|
||||||
let bestDist = Infinity;
|
let bestDist = Infinity;
|
||||||
|
// #320 — 유클리드 거리는 적도/극지에서 경도·위도의 실거리 차이가 커서 왜곡됨.
|
||||||
|
// cos(lat) 가중치(equirectangular approximation)로 위도 의존 보정.
|
||||||
|
const cosLat = Math.cos((lat * Math.PI) / 180);
|
||||||
for (const g of groups.values()) {
|
for (const g of groups.values()) {
|
||||||
const cLat = g.lats.reduce((a, b) => a + b, 0) / g.lats.length;
|
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 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) {
|
if (dist < bestDist) {
|
||||||
bestDist = dist;
|
bestDist = dist;
|
||||||
best = { country: g.country, city: g.city };
|
best = { country: g.country, city: g.city };
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
|
|||||||
zIndex={100}
|
zIndex={100}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
aria-label={`${point_count}개 식당이 모인 클러스터, 클릭하면 확대됩니다`}
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
@@ -246,7 +248,10 @@ function MapContent({ restaurants, selected, onSelectRestaurant, flyTo, activeCh
|
|||||||
onClick={() => handleMarkerClick(r)}
|
onClick={() => handleMarkerClick(r)}
|
||||||
zIndex={isSelected ? 1000 : 1}
|
zIndex={isSelected ? 1000 : 1}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", transition: "transform 0.2s ease", transform: isSelected ? "scale(1.15)" : "scale(1)", opacity: isClosed ? 0.5 : 1 }}>
|
<div
|
||||||
|
role="button"
|
||||||
|
aria-label={`${r.name}${isClosed ? ' (폐업)' : ''}, 클릭하면 상세 정보가 표시됩니다`}
|
||||||
|
style={{ display: "flex", flexDirection: "column", alignItems: "center", transition: "transform 0.2s ease", transform: isSelected ? "scale(1.15)" : "scale(1)", opacity: isClosed ? 0.5 : 1 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
@@ -389,10 +394,15 @@ export default function MapView({ restaurants, selected, onSelectRestaurant, onB
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{channelNames.length > 0 && (
|
{channelNames.length > 0 && (
|
||||||
<div className="absolute bottom-2 left-2 bg-surface/90 backdrop-blur-sm rounded-lg shadow px-2.5 py-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[11px] z-10">
|
<div
|
||||||
|
role="region"
|
||||||
|
aria-label="채널 범례"
|
||||||
|
className="absolute bottom-2 left-2 bg-surface/90 backdrop-blur-sm rounded-lg shadow px-2.5 py-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[11px] z-10"
|
||||||
|
>
|
||||||
{channelNames.map((ch) => (
|
{channelNames.map((ch) => (
|
||||||
<div key={ch} className="flex items-center gap-1">
|
<div key={ch} className="flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
className="inline-block w-2.5 h-2.5 rounded-full border"
|
className="inline-block w-2.5 h-2.5 rounded-full border"
|
||||||
style={{ backgroundColor: channelColors[ch].border, borderColor: channelColors[ch].border }}
|
style={{ backgroundColor: channelColors[ch].border, borderColor: channelColors[ch].border }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user