fix(crud): P4-1 백엔드 CRUD 결함 일괄 수정 (#290+#294+#295)

#294 (리뷰/메모):
- MemoService.upsert: 동시성 INSERT 시 DuplicateKeyException 폴백 → UPDATE
- ReviewService.toggleFavorite: 동시성 INSERT 시 DuplicateKeyException ignored (토글 ON)
- ReviewController: rating(0~5) Bean validation 헬퍼, body.rating null/비숫자 → 400
- ReviewMapper.xml getAvgRating: NVL로 0건 시에도 0.0 보장

#295 (채널):
- ChannelController.create: typed DataIntegrityViolationException으로 유니크 충돌 감지 (제약명 문자열 매칭 폐기)
- ChannelController.create: channel_id/channel_name null/빈값 → 400
- ChannelService.deactivate: "UC..." 형식 검증으로 명시적 분기 (이전 폴백 방식의 의도 모호함 해결)
- ChannelMapper.xml findByChannelId: description/tags/sort_order까지 SELECT

#290 (식당 CRUD):
- RestaurantController: @PreDestroy로 virtual thread executor shutdown
- RestaurantController: 캐시 역직렬화 실패를 silent ignore → log.warn + cache.del 자동 evict
- RestaurantController: setTablingUrl/setCatchtableUrl URL 스킴 화이트리스트 검증
- CacheService: 단일 키 del() 메서드 추가

후속 분리:
- #333 (#290 DTO 화이트리스트 + DDG 대체)
- #334 (#295 cache.flush 세분화 + scan 비동기)
- #335 (#294 테스트)

Refs: #290 #294 #295
This commit is contained in:
joungmin
2026-06-15 14:14:41 +09:00
parent eb1eaa91a6
commit 4b02293046
9 changed files with 89 additions and 22 deletions

View File

@@ -7,6 +7,7 @@ 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;
@@ -52,15 +53,20 @@ public class ChannelController {
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);
cache.flush();
return Map.of("id", id, "channel_id", channelId);
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().toUpperCase().contains("UQ_CHANNELS_CID")) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Channel already exists");
}
throw e;
} catch (DataIntegrityViolationException e) {
// #295 — 유니크 충돌을 메시지 문자열 매칭 대신 typed 예외로 감지 (제약명 변경에도 견고).
throw new ResponseStatusException(HttpStatus.CONFLICT, "Channel already exists");
}
}