#338: /api/version 신규 - HealthController에 @Value 빌드 정보 + GET /api/version 추가 - SecurityConfig.permitAll에 /api/version 추가 - application.yml app.build.version/commit (env APP_VERSION/APP_COMMIT) - 부수: SecurityConfig에서 /api/daemon/config permitAll 제거 (이미 admin-only) #320: findRegionFromCoords 거리 보정 - 유클리드 거리 → cos(lat) 가중치(equirectangular approx)로 위경도 실거리 보정 - 위도가 큰 지역(부산↔서울)에서 city 추정 정확도 향상 #340: MapView 마커/범례 ARIA - 클러스터 마커: role=button + aria-label - 개별 식당 마커: role=button + aria-label (name + 폐업 여부) - 채널 범례: role=region + aria-label, 색상 점은 aria-hidden #333: ChannelController 캐시 세분화 - cache.flush() 전체 무효화 → cache.del(makeKey("channels"))로 채널 키만 evict - 다른 모듈(restaurants/search) 캐시 hit율 보존 후속: deploy.sh에 APP_VERSION/APP_COMMIT env 주입은 별도 (현재 dev/unknown 응답) Refs: #338 #320 #340 #333
105 lines
4.2 KiB
Java
105 lines
4.2 KiB
Java
package com.tasteby.controller;
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.tasteby.domain.Channel;
|
|
import com.tasteby.security.AuthUtil;
|
|
import com.tasteby.service.CacheService;
|
|
import com.tasteby.service.ChannelService;
|
|
import com.tasteby.service.YouTubeService;
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
@RestController
|
|
@RequestMapping("/api/channels")
|
|
public class ChannelController {
|
|
|
|
private final ChannelService channelService;
|
|
private final YouTubeService youtubeService;
|
|
private final CacheService cache;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public ChannelController(ChannelService channelService, YouTubeService youtubeService,
|
|
CacheService cache, ObjectMapper objectMapper) {
|
|
this.channelService = channelService;
|
|
this.youtubeService = youtubeService;
|
|
this.cache = cache;
|
|
this.objectMapper = objectMapper;
|
|
}
|
|
|
|
@GetMapping
|
|
public List<Channel> list() {
|
|
String key = cache.makeKey("channels");
|
|
String cached = cache.getRaw(key);
|
|
if (cached != null) {
|
|
try {
|
|
return objectMapper.readValue(cached, new TypeReference<List<Channel>>() {});
|
|
} catch (Exception ignored) {}
|
|
}
|
|
var result = channelService.findAllActive();
|
|
cache.set(key, result);
|
|
return result;
|
|
}
|
|
|
|
@PostMapping
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
public Map<String, Object> create(@RequestBody Map<String, String> body) {
|
|
AuthUtil.requireAdmin();
|
|
String channelId = body.get("channel_id");
|
|
String channelName = body.get("channel_name");
|
|
String titleFilter = body.get("title_filter");
|
|
// #295 — body 필수값 가드 (NOT NULL 컬럼에 빈 값 들어가 500 나는 것 방지)
|
|
if (channelId == null || channelId.isBlank()) {
|
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "channel_id는 필수입니다");
|
|
}
|
|
if (channelName == null || channelName.isBlank()) {
|
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "channel_name은 필수입니다");
|
|
}
|
|
try {
|
|
String id = channelService.create(channelId, channelName, titleFilter);
|
|
// #333 — 전체 flush 대신 channels 키만 evict (다른 모듈 캐시 보존)
|
|
cache.del(cache.makeKey("channels"));
|
|
return Map.of("id", id, "channel_id", channelId);
|
|
} catch (DataIntegrityViolationException e) {
|
|
// #295 — 유니크 충돌을 메시지 문자열 매칭 대신 typed 예외로 감지 (제약명 변경에도 견고).
|
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Channel already exists");
|
|
}
|
|
}
|
|
|
|
@PostMapping("/{channelId}/scan")
|
|
public Map<String, Object> scan(@PathVariable String channelId,
|
|
@RequestParam(defaultValue = "false") boolean full) {
|
|
AuthUtil.requireAdmin();
|
|
var result = youtubeService.scanChannel(channelId, full);
|
|
if (result == null) {
|
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Channel not found");
|
|
}
|
|
cache.flush();
|
|
return result;
|
|
}
|
|
|
|
@PutMapping("/{id}")
|
|
public Map<String, Object> update(@PathVariable String id, @RequestBody Map<String, Object> body) {
|
|
AuthUtil.requireAdmin();
|
|
Integer sortOrder = body.get("sort_order") != null ? ((Number) body.get("sort_order")).intValue() : null;
|
|
channelService.update(id, (String) body.get("description"), (String) body.get("tags"), sortOrder);
|
|
cache.flush();
|
|
return Map.of("ok", true);
|
|
}
|
|
|
|
@DeleteMapping("/{channelId}")
|
|
public Map<String, Object> delete(@PathVariable String channelId) {
|
|
AuthUtil.requireAdmin();
|
|
if (!channelService.deactivate(channelId)) {
|
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Channel not found");
|
|
}
|
|
cache.flush();
|
|
return Map.of("ok", true);
|
|
}
|
|
}
|