1717import org .springframework .transaction .annotation .Transactional ;
1818import org .springframework .util .CollectionUtils ;
1919
20- import java .util .List ;
21- import java .util .Map ;
22- import java .util .NoSuchElementException ;
20+ import java .util .* ;
21+ import java .util .regex . Matcher ;
22+ import java .util .regex . Pattern ;
2323import java .util .stream .Collectors ;
2424
2525@ Service
@@ -33,93 +33,92 @@ public class CocktailService {
3333 @ Transactional (readOnly = true )
3434 public Cocktail getCocktailById (Long id ) {
3535
36- return cocktailRepository .findById (id )
37- .orElseThrow (() -> new IllegalArgumentException ("User not found. id=" + id ));
36+ return cocktailRepository .findById (id )
37+ .orElseThrow (() -> new IllegalArgumentException ("Cocktail not found. id=" + id ));
3838 }
3939
40- // 칵테일 무한스크롤 조회
41- @ Transactional (readOnly = true )
42- public List <CocktailSummaryResponseDto > getCocktails (Long lastId , Integer size )
43- { // 무한스크롤 조회, 클라이언트 쪽에서 lastId와 size 정보를 받음.(스크롤 이벤트)
44- int fetchSize = (size != null ) ? size : DEFAULT_SIZE ;
45-
46- List <Cocktail > cocktails ;
47- if (lastId == null ) {
48- // 첫 요청 → 최신 데이터부터
49- cocktails = cocktailRepository .findAllByOrderByIdDesc (PageRequest .of (0 , fetchSize ));
50- } else {
51- // 무한스크롤 → 마지막 ID보다 작은 데이터 조회
52- cocktails = cocktailRepository .findByIdLessThanOrderByIdDesc (lastId , PageRequest .of (0 , fetchSize ));
53- }
54- return cocktails .stream ()
55- .map (c -> new CocktailSummaryResponseDto (c .getId (), c .getCocktailName (), c .getCocktailNameKo (), c .getCocktailImgUrl (), c .getAlcoholStrength ().getDescription ()))
56- .collect (Collectors .toList ());
40+ // 칵테일 무한스크롤 조회
41+ @ Transactional (readOnly = true )
42+ public List <CocktailSummaryResponseDto > getCocktails (Long lastId , Integer size ) { // 무한스크롤 조회, 클라이언트 쪽에서 lastId와 size 정보를 받음.(스크롤 이벤트)
43+ int fetchSize = (size != null ) ? size : DEFAULT_SIZE ;
44+
45+ List <Cocktail > cocktails ;
46+ if (lastId == null ) {
47+ // 첫 요청 → 최신 데이터부터
48+ cocktails = cocktailRepository .findAllByOrderByIdDesc (PageRequest .of (0 , fetchSize ));
49+ } else {
50+ // 무한스크롤 → 마지막 ID보다 작은 데이터 조회
51+ cocktails = cocktailRepository .findByIdLessThanOrderByIdDesc (lastId , PageRequest .of (0 , fetchSize ));
5752 }
53+ return cocktails .stream ()
54+ .map (c -> new CocktailSummaryResponseDto (c .getId (), c .getCocktailName (), c .getCocktailNameKo (), c .getCocktailImgUrl (), c .getAlcoholStrength ().getDescription ()))
55+ .collect (Collectors .toList ());
56+ }
5857
59- // 칵테일 검색기능
60- @ Transactional (readOnly = true )
61- public List <Cocktail > cocktailSearch (String keyword ){
62- // cockTailName, ingredient이 하나만 있을 수도 있고 둘 다 있을 수도 있음
63- if (keyword == null || keyword .trim ().isEmpty ()) {
64- // 아무 검색어 없으면 전체 반환 처리
65- return cocktailRepository .findAll ();
66- } else {
67- // 이름 또는 재료 둘 중 하나라도 매칭되면 결과 반환
68- return cocktailRepository .findByCocktailNameContainingIgnoreCaseOrIngredientContainingIgnoreCase (keyword , keyword );
69- }
58+ // 칵테일 검색기능
59+ @ Transactional (readOnly = true )
60+ public List <Cocktail > cocktailSearch (String keyword ) {
61+ // cockTailName, ingredient이 하나만 있을 수도 있고 둘 다 있을 수도 있음
62+ if (keyword == null || keyword .trim ().isEmpty ()) {
63+ // 아무 검색어 없으면 전체 반환 처리
64+ return cocktailRepository .findAll ();
65+ } else {
66+ // 이름 또는 재료 둘 중 하나라도 매칭되면 결과 반환
67+ return cocktailRepository .findByCocktailNameContainingIgnoreCaseOrIngredientContainingIgnoreCase (keyword , keyword );
7068 }
69+ }
7170
72- // 칵테일 검색,필터기능
73- @ Transactional (readOnly = true )
74- public List <CocktailSearchResponseDto > searchAndFilter (CocktailSearchRequestDto cocktailSearchRequestDto ){
75- // 기본값 페이지/사이즈 정하기(PAGE 기본값 0, 사이즈 10)
76- int page = cocktailSearchRequestDto .getPage () != null && cocktailSearchRequestDto .getPage () >= 0
77- ? cocktailSearchRequestDto .getPage () : 0 ;
78-
79- int size = cocktailSearchRequestDto .getSize () != null && cocktailSearchRequestDto .getSize () > 0
80- ? cocktailSearchRequestDto .getSize () : DEFAULT_SIZE ;
81-
82- // searchAndFilters에서 조회한 결과값을 pageResult에 저장.
83- Pageable pageable = PageRequest .of (page , size );
84-
85- // 빈 리스트(null 또는 [])는 null로 변환
86- List <AlcoholStrength > strengths = CollectionUtils .isEmpty (cocktailSearchRequestDto .getAlcoholStrengths ())
87- ? null
88- : cocktailSearchRequestDto .getAlcoholStrengths ();
89-
90- List <CocktailType > types = CollectionUtils .isEmpty (cocktailSearchRequestDto .getCocktailTypes ())
91- ? null
92- : cocktailSearchRequestDto .getCocktailTypes ();
93-
94- List <AlcoholBaseType > bases = CollectionUtils .isEmpty (cocktailSearchRequestDto .getAlcoholBaseTypes ())
95- ? null
96- : cocktailSearchRequestDto .getAlcoholBaseTypes ();
97-
98- // Repository 호출
99- Page <Cocktail > pageResult = cocktailRepository .searchWithFilters (
100- cocktailSearchRequestDto .getKeyword (),
101- strengths , // List<AlcoholStrength>
102- types , // List<CocktailType>
103- bases , // List<AlcoholBaseType>
104- pageable
105- );
71+ // 칵테일 검색,필터기능
72+ @ Transactional (readOnly = true )
73+ public List <CocktailSearchResponseDto > searchAndFilter (CocktailSearchRequestDto cocktailSearchRequestDto ) {
74+ // 기본값 페이지/사이즈 정하기(PAGE 기본값 0, 사이즈 10)
75+ int page = cocktailSearchRequestDto .getPage () != null && cocktailSearchRequestDto .getPage () >= 0
76+ ? cocktailSearchRequestDto .getPage () : 0 ;
77+
78+ int size = cocktailSearchRequestDto .getSize () != null && cocktailSearchRequestDto .getSize () > 0
79+ ? cocktailSearchRequestDto .getSize () : DEFAULT_SIZE ;
80+
81+ // searchAndFilters에서 조회한 결과값을 pageResult에 저장.
82+ Pageable pageable = PageRequest .of (page , size );
83+
84+ // 빈 리스트(null 또는 [])는 null로 변환
85+ List <AlcoholStrength > strengths = CollectionUtils .isEmpty (cocktailSearchRequestDto .getAlcoholStrengths ())
86+ ? null
87+ : cocktailSearchRequestDto .getAlcoholStrengths ();
88+
89+ List <CocktailType > types = CollectionUtils .isEmpty (cocktailSearchRequestDto .getCocktailTypes ())
90+ ? null
91+ : cocktailSearchRequestDto .getCocktailTypes ();
92+
93+ List <AlcoholBaseType > bases = CollectionUtils .isEmpty (cocktailSearchRequestDto .getAlcoholBaseTypes ())
94+ ? null
95+ : cocktailSearchRequestDto .getAlcoholBaseTypes ();
96+
97+ // Repository 호출
98+ Page <Cocktail > pageResult = cocktailRepository .searchWithFilters (
99+ cocktailSearchRequestDto .getKeyword (),
100+ strengths , // List<AlcoholStrength>
101+ types , // List<CocktailType>
102+ bases , // List<AlcoholBaseType>
103+ pageable
104+ );
106105
107- //Cocktail 엔티티 → CocktailResponseDto 응답 DTO로 바꿔주는 과정
108- List <CocktailSearchResponseDto > resultDtos = pageResult .stream ()
109- .map (c -> new CocktailSearchResponseDto (
110- c .getId (),
111- c .getCocktailName (),
112- c .getCocktailNameKo (),
113- c .getAlcoholStrength ().getDescription (),
114- c .getCocktailType ().getDescription (),
115- c .getAlcoholBaseType ().getDescription (),
116- c .getCocktailImgUrl (),
117- c .getCocktailStory ()
118- ))
119- .collect (Collectors .toList ());
120-
121- return resultDtos ;
122- }
106+ //Cocktail 엔티티 → CocktailResponseDto 응답 DTO로 바꿔주는 과정
107+ List <CocktailSearchResponseDto > resultDtos = pageResult .stream ()
108+ .map (c -> new CocktailSearchResponseDto (
109+ c .getId (),
110+ c .getCocktailName (),
111+ c .getCocktailNameKo (),
112+ c .getAlcoholStrength ().getDescription (),
113+ c .getCocktailType ().getDescription (),
114+ c .getAlcoholBaseType ().getDescription (),
115+ c .getCocktailImgUrl (),
116+ c .getCocktailStory ()
117+ ))
118+ .collect (Collectors .toList ());
119+
120+ return resultDtos ;
121+ }
123122
124123// private <T> List<T> nullIfEmpty(List<T> list) {
125124// return CollectionUtils.isEmpty(list) ? null : list;
@@ -132,7 +131,7 @@ public CocktailDetailResponseDto getCocktailDetailById(Long cocktailId) {
132131 .orElseThrow (() -> new NoSuchElementException ("칵테일을 찾을 수 없습니다. id: " + cocktailId ));
133132
134133 // ingredient 분수 변환
135- String formattedIngredient = convertFractions (cocktail .getIngredient ());
134+ List < IngredientDto > formattedIngredient = parseIngredients ( convertFractions (cocktail .getIngredient () ));
136135
137136 return new CocktailDetailResponseDto (
138137 cocktail .getId (),
@@ -171,4 +170,46 @@ private String convertFractions(String ingredient) {
171170
172171 return ingredient ;
173172 }
173+
174+ // Ingredient DTO
175+ public record IngredientDto (
176+ String ingredientName ,
177+ String amount ,
178+ String unit
179+ ) {
180+ }
181+
182+ private List <IngredientDto > parseIngredients (String ingredientStr ) {
183+ if (ingredientStr == null || ingredientStr .isBlank ()) return Collections .emptyList ();
184+
185+ List <IngredientDto > result = new ArrayList <>();
186+ String [] items = ingredientStr .split (",\\ s*" );
187+
188+ for (String item : items ) {
189+ String [] parts = item .split (":" );
190+ if (parts .length != 2 ) continue ;
191+
192+ String name = parts [0 ].trim ();
193+ String amountUnit = parts [1 ].trim ();
194+
195+ // (숫자 + 선택적 분수) + (공백) + (단위)
196+ Pattern pattern = Pattern .compile (
197+ "^([0-9]*\\ s*[½⅓⅔¼¾⅛⅜⅝⅞]?)\\ s*(.*)$" ,
198+ Pattern .UNICODE_CHARACTER_CLASS
199+ );
200+ Matcher matcher = pattern .matcher (amountUnit );
201+
202+ if (matcher .matches ()) {
203+ String amount = matcher .group (1 ).trim ();
204+ String unit = matcher .group (2 ).trim ();
205+
206+ result .add (new IngredientDto (name , amount , unit ));
207+ } else {
208+ // 패턴 매치 실패 시 전체를 amount로 처리
209+ result .add (new IngredientDto (name , amountUnit , "" ));
210+ }
211+ }
212+
213+ return result ;
214+ }
174215}
0 commit comments