diff --git a/src/main/java/com/back/domain/cocktail/controller/CocktailRecommendController.java b/src/main/java/com/back/domain/cocktail/controller/CocktailRecommendController.java new file mode 100644 index 00000000..d8b877f4 --- /dev/null +++ b/src/main/java/com/back/domain/cocktail/controller/CocktailRecommendController.java @@ -0,0 +1,30 @@ +package com.back.domain.cocktail.controller; + +import com.back.domain.cocktail.dto.CocktailRecommendResponseDto; +import com.back.domain.cocktail.service.RecommendService; +import com.back.global.rsData.RsData; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/cocktails/recommend") +@Tag(name = "ApiCocktailRecommendController", description = "API 칵테일 추천 컨트롤러") +@RequiredArgsConstructor +public class CocktailRecommendController { + + private final RecommendService recommendService; + + // 상세페이지 추천 (DTO로 반환) + @Operation(summary = "상세페이지 유사 칵테일 추천", description = "현재 칵테일과 유사한 칵테일 최대 3개를 반환합니다.") + @GetMapping("/related") + public RsData> recommendRelated(@RequestParam Long cocktailId) { + return RsData.successOf(recommendService.recommendRelatedCocktails(cocktailId, 3)); + } +} diff --git a/src/main/java/com/back/domain/cocktail/dto/CocktailRecommendResponseDto.java b/src/main/java/com/back/domain/cocktail/dto/CocktailRecommendResponseDto.java new file mode 100644 index 00000000..061a9839 --- /dev/null +++ b/src/main/java/com/back/domain/cocktail/dto/CocktailRecommendResponseDto.java @@ -0,0 +1,12 @@ +package com.back.domain.cocktail.dto; + +public record CocktailRecommendResponseDto( + Long id, // 상세페이지 이동용 ID + String cocktailNameKo, // 한글 이름 + String cocktailName, // 영문 이름 + String cocktailImgUrl, // 이미지 URL (썸네일) + String alcoholStrength, // 도수 (라이트/미디엄/스트롱 등) + String alcoholBaseType // 베이스 주종 (진, 럼, 보드카 등) +) { +} + diff --git a/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java b/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java index 5d63554a..961be10f 100644 --- a/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java +++ b/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java @@ -36,4 +36,12 @@ Page searchWithFilters(@Param("keyword") String keyword, @Param("types") List types, @Param("bases") List bases, Pageable pageable); + //유사칵테일 추천관련 + List findByAlcoholStrengthAndIdNot(AlcoholStrength strength, Long excludeId); + + //유사칵테일 추천관련 + List findByCocktailTypeAndIdNot(CocktailType type, Long excludeId); + + //유사칵테일 추천관련 + List findByAlcoholBaseTypeAndIdNot(AlcoholBaseType baseType, Long excludeId); } diff --git a/src/main/java/com/back/domain/cocktail/service/RecommendService.java b/src/main/java/com/back/domain/cocktail/service/RecommendService.java new file mode 100644 index 00000000..c0a6c96e --- /dev/null +++ b/src/main/java/com/back/domain/cocktail/service/RecommendService.java @@ -0,0 +1,53 @@ +package com.back.domain.cocktail.service; + +import com.back.domain.cocktail.dto.CocktailRecommendResponseDto; +import com.back.domain.cocktail.entity.Cocktail; +import com.back.domain.cocktail.repository.CocktailRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class RecommendService { + + private final CocktailRepository cocktailRepository; + + public List recommendRelatedCocktails(Long cocktailId, int maxSize) { + Cocktail current = cocktailRepository.findById(cocktailId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 칵테일입니다.")); + + // 3가지 조건으로 유사 칵테일 조회 + List byAlcoholStrength = cocktailRepository.findByAlcoholStrengthAndIdNot(current.getAlcoholStrength(), current.getId()); + List byCocktailType = cocktailRepository.findByCocktailTypeAndIdNot(current.getCocktailType(), current.getId()); + List byAlcoholBase = cocktailRepository.findByAlcoholBaseTypeAndIdNot(current.getAlcoholBaseType(), current.getId()); + + // 합치고 중복 제거 + Set combined = new LinkedHashSet<>(); + combined.addAll(byAlcoholStrength); + combined.addAll(byCocktailType); + combined.addAll(byAlcoholBase); + + List combinedList = new ArrayList<>(combined); + if (combinedList.size() > maxSize) { + combinedList = combinedList.subList(0, maxSize); + } + + // DTO로 변환 + return combinedList.stream() + .map(c -> new CocktailRecommendResponseDto( + c.getId(), + c.getCocktailNameKo(), + c.getCocktailName(), + c.getCocktailImgUrl(), + c.getAlcoholStrength().name(), + c.getAlcoholBaseType().name() + )) + .toList(); + } +} + diff --git a/src/main/java/com/back/global/globalExceptionHandler/GlobalExceptionHandler.java b/src/main/java/com/back/global/globalExceptionHandler/GlobalExceptionHandler.java index edc77bfa..1a3a2f50 100644 --- a/src/main/java/com/back/global/globalExceptionHandler/GlobalExceptionHandler.java +++ b/src/main/java/com/back/global/globalExceptionHandler/GlobalExceptionHandler.java @@ -156,4 +156,9 @@ public ResponseEntity> handleIOException(IOException e) { .body(RsData.of(500, "서버 내부 오류가 발생했습니다.", null)); } + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(RsData.of(404, ex.getMessage())); + } } diff --git a/src/main/resources/data-h2.sql b/src/main/resources/data-h2.sql index 38d33353..26b8475f 100644 --- a/src/main/resources/data-h2.sql +++ b/src/main/resources/data-h2.sql @@ -1,7 +1,7 @@ -- 테이블 생성 CREATE TABLE IF NOT EXISTS cocktail ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - cocktail_name VARCHAR(255), + id BIGINT AUTO_INCREMENT PRIMARY KEY, + cocktail_name VARCHAR(255), cocktail_name_ko VARCHAR(255), alcohol_strength VARCHAR(50), cocktail_story CLOB, @@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS cocktail ( -- CSV 파일에서 데이터 읽어오기 INSERT INTO cocktail ( - cocktail_name, cocktail_name_ko, alcohol_strength, + cocktail_name,cocktail_name_ko, alcohol_strength, cocktail_story, cocktail_type, alcohol_base_type, ingredient, recipe, cocktail_img_url )