Files
tasteby/backend-java/src/main/java/com/tasteby/controller/ChannelController.java
joungmin a0e8878d9a feat: P5-2 작은 후속 (#338+#320+#340+#333)
#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
2026-06-15 14:48:32 +09:00

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);
}
}