diff --git a/oversweet-api/src/main/java/com/depromeet/oversweet/drink/controller/DrinkController.java b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/controller/DrinkController.java index b92291d..b6a7b70 100644 --- a/oversweet-api/src/main/java/com/depromeet/oversweet/drink/controller/DrinkController.java +++ b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/controller/DrinkController.java @@ -1,10 +1,13 @@ package com.depromeet.oversweet.drink.controller; +import com.depromeet.oversweet.drink.dto.request.DrinkInfoRequest; import com.depromeet.oversweet.drink.dto.request.DrinkWeeklySugarDateRequest; import com.depromeet.oversweet.drink.dto.response.DrinkDailySugarStatisticsResponse; +import com.depromeet.oversweet.drink.dto.response.DrinkDetailInfoResponse; import com.depromeet.oversweet.drink.dto.response.DrinkWeeklySugarStatisticsResponse; import com.depromeet.oversweet.drink.service.DrinkDailyStatisticsService; +import com.depromeet.oversweet.drink.service.DrinkDetailSearchService; import com.depromeet.oversweet.drink.service.DrinkWeeklyStatisticsService; import com.depromeet.oversweet.response.DataResponse; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +31,7 @@ public class DrinkController { private final DrinkDailyStatisticsService drinkDailyStatisticsService; private final DrinkWeeklyStatisticsService drinkWeeklyStatisticsService; + private final DrinkDetailSearchService drinkDetailSearchService; /** * 유저 하루(데일리) 먹은 당 통계 및 음료 목록 조회. @@ -56,4 +60,16 @@ public ResponseEntity> retrieve return ResponseEntity.ok() .body(DataResponse.of(HttpStatus.OK, "유저가 먹은 주간 당 통계 조회 성공", response)); } + + /** + * 음료 상세 조회 + * 추후 로그인 기능 구현 후, 로그인한 유저의 ID를 받아와야 함 (ex. @AuthenticationPrincipal User user) + */ + @Operation(summary = "음료 상세 조회", description = "음료 상세 정보를 조회합니다.") + @ApiResponses(@ApiResponse(responseCode = "200", description = "음료 상세 조회.")) + @GetMapping("/detail") + public ResponseEntity> retrieveDrinkDetail(@RequestBody @Valid final DrinkInfoRequest request) { + DrinkDetailInfoResponse response = drinkDetailSearchService.retrieveDrinkDetail(100L, request); + return ResponseEntity.ok().body(DataResponse.of(HttpStatus.OK, "음료 상세 조회 성공", response)); + } } diff --git a/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/request/DrinkInfoRequest.java b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/request/DrinkInfoRequest.java new file mode 100644 index 0000000..64e5cb8 --- /dev/null +++ b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/request/DrinkInfoRequest.java @@ -0,0 +1,23 @@ +package com.depromeet.oversweet.drink.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +/** + * 음료 상세 정보를 보기 위한 요청 DTO + */ +@Getter +public class DrinkInfoRequest { + + @Schema(description = "프랜차이즈 ID", example = "1") + @NotNull(message = "프랜차이즈 ID는 필수 값입니다.") + private Long franchiseId; + + @Schema(description = "음료 이름", example = "아메리카노") + @NotEmpty(message = "음료 이름은 필수 값입니다.") + private String drinkName; + + +} diff --git a/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/response/DrinkDetailInfoResponse.java b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/response/DrinkDetailInfoResponse.java new file mode 100644 index 0000000..8ed2bc5 --- /dev/null +++ b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/dto/response/DrinkDetailInfoResponse.java @@ -0,0 +1,23 @@ +package com.depromeet.oversweet.drink.dto.response; + +import com.depromeet.oversweet.bookmark.dto.response.FranchiseInfo; +import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus; +import lombok.Getter; + +import java.util.List; + +/** + * 상세 음료 정보를 응답하는 DTO + */ +@Getter +public class DrinkDetailInfoResponse { + + private final FranchiseInfo franchise; + + private final List drinks; + + public DrinkDetailInfoResponse(FranchiseInfo franchise, List drinks) { + this.franchise = franchise; + this.drinks = drinks; + } +} diff --git a/oversweet-api/src/main/java/com/depromeet/oversweet/drink/service/DrinkDetailSearchService.java b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/service/DrinkDetailSearchService.java new file mode 100644 index 0000000..f9b0a49 --- /dev/null +++ b/oversweet-api/src/main/java/com/depromeet/oversweet/drink/service/DrinkDetailSearchService.java @@ -0,0 +1,46 @@ +package com.depromeet.oversweet.drink.service; + +import com.depromeet.oversweet.bookmark.dto.response.FranchiseInfo; +import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus; +import com.depromeet.oversweet.domain.drink.repository.FindDrinkRepository; +import com.depromeet.oversweet.domain.franchise.entity.FranchiseEntity; +import com.depromeet.oversweet.domain.franchise.repository.FindFranchiseRepository; +import com.depromeet.oversweet.drink.dto.request.DrinkInfoRequest; +import com.depromeet.oversweet.drink.dto.response.DrinkDetailInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 음료 상세 정보를 보기 위한 서비스 + */ +@Service +@RequiredArgsConstructor +public class DrinkDetailSearchService { + + private final FindDrinkRepository findDrinkRepository; + private final FindFranchiseRepository findFranchiseRepository; + + /** + * 음료 상세 정보 조회 + * + * @param memberId API 접근자 memberId + * @param request 음료 정보를 확인하기 위해 필요한 값을 담고 있는 request dto + * @return 음료 상세 정보 + */ + public DrinkDetailInfoResponse retrieveDrinkDetail(Long memberId, DrinkInfoRequest request) { + // 프랜차이즈 ID와 음료 이름으로 => 음료가 존재하는지 학인 => 없으면 exception + findDrinkRepository.checkDrinkExist(request.getFranchiseId(), request.getDrinkName()); + + // 프랜차이즈 응답을 위한 조회 + FranchiseEntity franchise = findFranchiseRepository.findFranchiseById(request.getFranchiseId()); + + // 음료 정보를 조회 (음료 정보 & 사이즈별 즐겨 찾기 여부) + List drinkDetails = findDrinkRepository.findDrinkDetail(memberId, franchise.getId(), request.getDrinkName()); + + return new DrinkDetailInfoResponse(new FranchiseInfo(franchise), drinkDetails); + } + + +} diff --git a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/dto/DrinkInfoWithScrapStatus.java b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/dto/DrinkInfoWithScrapStatus.java new file mode 100644 index 0000000..e553129 --- /dev/null +++ b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/dto/DrinkInfoWithScrapStatus.java @@ -0,0 +1,36 @@ +package com.depromeet.oversweet.domain.drink.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +/** + * 음료 상세 정보를 보기 위한 응답 DTO + */ +@Getter +public class DrinkInfoWithScrapStatus { + + @Schema(name = "음료 ID", example = "1") + private final Long id; + @Schema(name = "음료 이름", example = "아메리카노") + private final String name; + @Schema(name = "음료 이미지 URL") + private final String drinkImageUrl; + @Schema(name = "음료 사이즈", example = "355") + private final Integer size; + @Schema(name = "음료 칼로리", example = "10") + private final Integer calorie; + @Schema(name = "음료 당도", example = "10") + private final Integer sugar; + @Schema(name = "즐겨찾기 여부", example = "true") + private final Boolean scrapStatus; + + public DrinkInfoWithScrapStatus(Long id, String name, String drinkImageUrl, Integer size, Integer calorie, Integer sugar, Boolean scrapStatus) { + this.id = id; + this.name = name; + this.drinkImageUrl = drinkImageUrl; + this.size = size; + this.calorie = calorie; + this.sugar = sugar; + this.scrapStatus = scrapStatus; + } +} diff --git a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/entity/DrinkEntity.java b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/entity/DrinkEntity.java index dc1d269..506a7e2 100644 --- a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/entity/DrinkEntity.java +++ b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/entity/DrinkEntity.java @@ -3,24 +3,17 @@ import com.depromeet.oversweet.domain.common.entity.BaseTimeEntity; import com.depromeet.oversweet.domain.drink.enums.DrinkCategory; import com.depromeet.oversweet.domain.franchise.entity.FranchiseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.ForeignKey; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -@Table(name = "drink") +@Table(name = "drink", + indexes = { + @Index(name = "idx_franchise_id_and_name", columnList = "franchise_id,name") + } +) @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/DrinkJpaRepository.java b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/DrinkJpaRepository.java index 863396c..2e03b99 100644 --- a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/DrinkJpaRepository.java +++ b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/DrinkJpaRepository.java @@ -1,7 +1,22 @@ package com.depromeet.oversweet.domain.drink.repository; +import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus; import com.depromeet.oversweet.domain.drink.entity.DrinkEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface DrinkJpaRepository extends JpaRepository { + + List findByFranchiseIdAndName(Long franchiseId, String drinkName); + + @Query("SELECT new com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus(d.id, d.name, d.imageUrl, d.size, d.calorie, d.sugar, " + + "(CASE WHEN db IS NULL THEN false ELSE true END)) " + + "FROM DrinkEntity d " + + "LEFT JOIN DrinkBookmarkEntity db ON d.id = db.drink.id AND db.member.id = :memberId " + + "WHERE d.franchise.id = :franchiseId AND d.name = :drinkName ORDER BY d.size ASC") + List findDrinkWithBookmarkStatus(@Param("memberId") Long memberId, @Param("franchiseId") Long franchiseId, @Param("drinkName") String drinkName); + } diff --git a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepository.java b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepository.java index ee7cfa0..5cd9aaa 100644 --- a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepository.java +++ b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepository.java @@ -1,10 +1,17 @@ package com.depromeet.oversweet.domain.drink.repository; +import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus; import com.depromeet.oversweet.domain.drink.entity.DrinkEntity; +import java.util.List; + /** * 음료 정보 조회 Interface */ public interface FindDrinkRepository { DrinkEntity findDrinkById(final Long drinkId); + + List findDrinkDetail(final Long memberId, final Long franchiseId, final String drinkName); + + void checkDrinkExist(final Long franchiseId, final String drinkName); } diff --git a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepositoryImpl.java b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepositoryImpl.java index 968aef3..4bc99b8 100644 --- a/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepositoryImpl.java +++ b/oversweet-domain/src/main/java/com/depromeet/oversweet/domain/drink/repository/FindDrinkRepositoryImpl.java @@ -1,11 +1,14 @@ package com.depromeet.oversweet.domain.drink.repository; +import com.depromeet.oversweet.domain.drink.dto.DrinkInfoWithScrapStatus; import com.depromeet.oversweet.domain.drink.entity.DrinkEntity; import com.depromeet.oversweet.exception.drink.NotFoundDrinkException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + import static com.depromeet.oversweet.exception.ErrorCode.NOT_FOUND_DRINK; /** @@ -29,4 +32,33 @@ public DrinkEntity findDrinkById(final Long drinkId) { return drinkJpaRepository.findById(drinkId) .orElseThrow(() -> new NotFoundDrinkException(NOT_FOUND_DRINK)); } + + /** + * 음료 상세 정보 조회 + * 프랜차이즈 정보 및 즐겨 찾기 여부를 함께 조회한다. + * + * @param franchiseId 상세 보기를 원하는 음료의 프랜차이즈 ID + * @param drinkName 상세 보기를 원하는 음료의 이름 + */ + @Override + @Transactional(readOnly = true) + public List findDrinkDetail(final Long memberId, final Long franchiseId, final String drinkName) { + return drinkJpaRepository.findDrinkWithBookmarkStatus(memberId, franchiseId, drinkName); + } + + /** + * 음료가 존재하는지 확인 + * + * @param franchiseId 음료의 프랜차이즈 ID + * @param drinkName 음료 이름 + */ + @Override + @Transactional(readOnly = true) + public void checkDrinkExist(final Long franchiseId, final String drinkName) { + if (drinkJpaRepository.findByFranchiseIdAndName(franchiseId, drinkName) + .isEmpty()){ + throw new NotFoundDrinkException(NOT_FOUND_DRINK); + } + } + }