22
33import com .back .domain .chatbot .dto .ChatRequestDto ;
44import com .back .domain .chatbot .dto .ChatResponseDto ;
5+ import com .back .domain .chatbot .dto .StepRecommendationResponseDto ;
56import com .back .domain .chatbot .entity .ChatConversation ;
67import com .back .domain .chatbot .repository .ChatConversationRepository ;
8+ import com .back .domain .cocktail .dto .CocktailSummaryResponseDto ;
9+ import com .back .domain .cocktail .entity .Cocktail ;
10+ import com .back .domain .cocktail .enums .AlcoholBaseType ;
11+ import com .back .domain .cocktail .enums .AlcoholStrength ;
12+ import com .back .domain .cocktail .enums .CocktailType ;
13+ import com .back .domain .cocktail .repository .CocktailRepository ;
714import jakarta .annotation .PostConstruct ;
815import lombok .RequiredArgsConstructor ;
916import lombok .extern .slf4j .Slf4j ;
1219import org .springframework .ai .openai .OpenAiChatOptions ;
1320import org .springframework .beans .factory .annotation .Value ;
1421import org .springframework .core .io .Resource ;
22+ import org .springframework .data .domain .Page ;
23+ import org .springframework .data .domain .PageRequest ;
1524import org .springframework .data .domain .Pageable ;
1625import org .springframework .stereotype .Service ;
1726import org .springframework .transaction .annotation .Transactional ;
2029import java .io .IOException ;
2130import java .nio .charset .StandardCharsets ;
2231import java .time .LocalDateTime ;
32+ import java .util .ArrayList ;
2333import java .util .Collections ;
2434import java .util .List ;
35+ import java .util .stream .Collectors ;
2536
2637@ Service
2738@ RequiredArgsConstructor
@@ -30,6 +41,7 @@ public class ChatbotService {
3041
3142 private final ChatModel chatModel ;
3243 private final ChatConversationRepository chatConversationRepository ;
44+ private final CocktailRepository cocktailRepository ;
3345
3446
3547 @ Value ("classpath:prompts/chatbot-system-prompt.txt" )
@@ -77,6 +89,16 @@ public void init() throws IOException {
7789 @ Transactional
7890 public ChatResponseDto sendMessage (ChatRequestDto requestDto ) {
7991 try {
92+ // 단계별 추천 모드 확인 (currentStep이 있으면 무조건 단계별 추천 모드)
93+ if (requestDto .isStepRecommendation () ||
94+ requestDto .getCurrentStep () != null ||
95+ isStepRecommendationTrigger (requestDto .getMessage ())) {
96+ log .info ("Recommendation chat mode for userId: {}" , requestDto .getUserId ());
97+ return handleStepRecommendation (requestDto );
98+ }
99+
100+ log .info ("Normal chat mode for userId: {}" , requestDto .getUserId ());
101+
80102 // 메시지 타입 감지
81103 MessageType messageType = detectMessageType (requestDto .getMessage ());
82104
@@ -238,10 +260,160 @@ private MessageType detectMessageType(String message) {
238260 return MessageType .CASUAL_CHAT ;
239261 }
240262
263+ // 단계별 추천 시작 키워드 감지
264+ private boolean isStepRecommendationTrigger (String message ) {
265+ String lower = message .toLowerCase ().trim ();
266+ return lower .contains ("단계별 추천" );
267+ }
268+
269+ // 단계별 추천 처리 통합 메서드
270+ private ChatResponseDto handleStepRecommendation (ChatRequestDto requestDto ) {
271+ Integer currentStep = requestDto .getCurrentStep ();
272+
273+ // 단계가 지정되지 않았거나 첫 시작인 경우
274+ if (currentStep == null || currentStep <= 0 ) {
275+ currentStep = 1 ;
276+ }
277+
278+ StepRecommendationResponseDto stepRecommendation ;
279+ String chatResponse ;
280+
281+ switch (currentStep ) {
282+ case 1 :
283+ stepRecommendation = getAlcoholStrengthOptions ();
284+ chatResponse = "단계별 맞춤 추천을 시작합니다! 🎯\n 원하시는 도수를 선택해주세요!" ;
285+ break ;
286+ case 2 :
287+ stepRecommendation = getAlcoholBaseTypeOptions (requestDto .getSelectedAlcoholStrength ());
288+ chatResponse = "좋은 선택이네요! 이제 베이스가 될 술을 선택해주세요 🍸" ;
289+ break ;
290+ case 3 :
291+ stepRecommendation = getCocktailTypeOptions (requestDto .getSelectedAlcoholStrength (), requestDto .getSelectedAlcoholBaseType ());
292+ chatResponse = "완벽해요! 마지막으로 어떤 스타일로 즐기실 건가요? 🥃" ;
293+ break ;
294+ case 4 :
295+ stepRecommendation = getFinalRecommendations (
296+ requestDto .getSelectedAlcoholStrength (),
297+ requestDto .getSelectedAlcoholBaseType (),
298+ requestDto .getSelectedCocktailType ()
299+ );
300+ chatResponse = stepRecommendation .getStepTitle ();
301+ break ;
302+ default :
303+ stepRecommendation = getAlcoholStrengthOptions ();
304+ chatResponse = "단계별 맞춤 추천을 시작합니다! 🎯" ;
305+ }
306+
307+ // 대화 기록 저장
308+ saveConversation (requestDto , chatResponse );
309+
310+ return new ChatResponseDto (chatResponse , stepRecommendation );
311+ }
312+
241313 @ Transactional (readOnly = true )
242314 public List <ChatConversation > getUserChatHistory (Long userId ) {
243315 return chatConversationRepository .findByUserIdOrderByCreatedAtDesc (userId , Pageable .unpaged ()).getContent ();
244316 }
245317
318+
319+ private StepRecommendationResponseDto getAlcoholStrengthOptions () {
320+ List <StepRecommendationResponseDto .StepOption > options = new ArrayList <>();
321+
322+ for (AlcoholStrength strength : AlcoholStrength .values ()) {
323+ options .add (new StepRecommendationResponseDto .StepOption (
324+ strength .name (),
325+ strength .getDescription (),
326+ null
327+ ));
328+ }
329+
330+ return new StepRecommendationResponseDto (
331+ 1 ,
332+ "원하시는 도수를 선택해주세요!" ,
333+ options ,
334+ null ,
335+ false
336+ );
337+ }
338+
339+ private StepRecommendationResponseDto getAlcoholBaseTypeOptions (AlcoholStrength alcoholStrength ) {
340+ List <StepRecommendationResponseDto .StepOption > options = new ArrayList <>();
341+
342+ for (AlcoholBaseType baseType : AlcoholBaseType .values ()) {
343+ options .add (new StepRecommendationResponseDto .StepOption (
344+ baseType .name (),
345+ baseType .getDescription (),
346+ null
347+ ));
348+ }
349+
350+ return new StepRecommendationResponseDto (
351+ 2 ,
352+ "베이스가 될 술을 선택해주세요!" ,
353+ options ,
354+ null ,
355+ false
356+ );
357+ }
358+
359+ private StepRecommendationResponseDto getCocktailTypeOptions (AlcoholStrength alcoholStrength , AlcoholBaseType alcoholBaseType ) {
360+ List <StepRecommendationResponseDto .StepOption > options = new ArrayList <>();
361+
362+ for (CocktailType cocktailType : CocktailType .values ()) {
363+ options .add (new StepRecommendationResponseDto .StepOption (
364+ cocktailType .name (),
365+ cocktailType .getDescription (),
366+ null
367+ ));
368+ }
369+
370+ return new StepRecommendationResponseDto (
371+ 3 ,
372+ "어떤 종류의 잔으로 드시겠어요?" ,
373+ options ,
374+ null ,
375+ false
376+ );
377+ }
378+
379+ private StepRecommendationResponseDto getFinalRecommendations (
380+ AlcoholStrength alcoholStrength ,
381+ AlcoholBaseType alcoholBaseType ,
382+ CocktailType cocktailType ) {
383+ // 필터링 조건에 맞는 칵테일 검색
384+ List <AlcoholStrength > strengths = List .of (alcoholStrength );
385+ List <AlcoholBaseType > baseTypes = List .of (alcoholBaseType );
386+ List <CocktailType > cocktailTypes = List .of (cocktailType );
387+
388+ Page <Cocktail > cocktailPage = cocktailRepository .searchWithFilters (
389+ null , // 키워드 없음
390+ strengths ,
391+ cocktailTypes ,
392+ baseTypes ,
393+ PageRequest .of (0 , 5 ) // 최대 5개 추천
394+ );
395+
396+ List <CocktailSummaryResponseDto > recommendations = cocktailPage .getContent ().stream ()
397+ .map (cocktail -> new CocktailSummaryResponseDto (
398+ cocktail .getId (),
399+ cocktail .getCocktailName (),
400+ cocktail .getCocktailImgUrl (),
401+ cocktail .getAlcoholStrength ()
402+ ))
403+ .collect (Collectors .toList ());
404+
405+ String stepTitle = recommendations .isEmpty ()
406+ ? "조건에 맞는 칵테일을 찾을 수 없습니다 😢"
407+ : "당신을 위한 맞춤 칵테일 추천입니다! 🍹" ;
408+
409+ return new StepRecommendationResponseDto (
410+ 4 ,
411+ stepTitle ,
412+ null ,
413+ recommendations ,
414+ true
415+ );
416+ }
417+
246418}
247419
0 commit comments