@@ -62,6 +62,14 @@ public class ChatbotService {
6262 private String responseRules ;
6363 private ChatClient chatClient ;
6464
65+ // 로딩 메시지 상수
66+ private static final String RECOMMENDATION_LOADING_MESSAGE =
67+ "당신에게 어울리는 칵테일은? 두구❤️두구💛두구💚두구💙두구💜두구🖤두구🤍두구🤎" ;
68+
69+ // 처리 완료 플래그 키워드
70+ private static final String PROCESS_STEP_RECOMMENDATION = "PROCESS_STEP_RECOMMENDATION" ;
71+ private static final String PROCESS_QA_RECOMMENDATION = "PROCESS_QA_RECOMMENDATION" ;
72+
6573 @ PostConstruct
6674 public void init () throws IOException {
6775 this .systemPrompt = StreamUtils .copyToString (
@@ -97,9 +105,95 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
97105 currentStep == 0 ? "QA" : "STEP" );
98106
99107 if (currentStep == 0 ) {
100- // 질문형 추천 (일반 AI 대화)
101- log .info ("질문형 추천 모드 진입 - userId: {}" , requestDto .getUserId ());
102- return generateAIResponseWithContext (requestDto , "질문형 추천" );
108+ // 질문형 추천 선택 시 안내 메시지와 INPUT 타입 반환
109+ if ("QA" .equalsIgnoreCase (requestDto .getMessage ()) ||
110+ requestDto .getMessage ().contains ("질문형" )) {
111+
112+ log .info ("질문형 추천 시작 - userId: {}" , requestDto .getUserId ());
113+
114+ // 사용자 선택 메시지 저장
115+ ChatConversation userChoice = ChatConversation .builder ()
116+ .userId (requestDto .getUserId ())
117+ .message ("질문형 취향 찾기" )
118+ .sender (MessageSender .USER )
119+ .createdAt (LocalDateTime .now ())
120+ .build ();
121+ chatConversationRepository .save (userChoice );
122+
123+ String guideMessage = "칵테일에 관련된 질문을 입력해주세요!" ;
124+ /*
125+ String guideMessage = "좋아요! 질문형 추천을 시작할게요 🎯\n" +
126+ "칵테일에 관련된 질문을 자유롭게 입력해주세요!\n" +
127+ "예시: 달콤한 칵테일 추천해줘, 파티용 칵테일이 필요해, 초보자용 칵테일 알려줘";
128+ */
129+
130+ ChatConversation botGuide = ChatConversation .builder ()
131+ .userId (requestDto .getUserId ())
132+ .message (guideMessage )
133+ .sender (MessageSender .CHATBOT )
134+ .createdAt (LocalDateTime .now ())
135+ .build ();
136+ ChatConversation savedGuide = chatConversationRepository .save (botGuide );
137+
138+ // INPUT 타입으로 반환하여 사용자 입력 유도
139+ return ChatResponseDto .builder ()
140+ .id (savedGuide .getId ())
141+ .userId (requestDto .getUserId ())
142+ .message (guideMessage )
143+ .sender (MessageSender .CHATBOT )
144+ .type (MessageType .INPUT )
145+ .createdAt (savedGuide .getCreatedAt ())
146+ .metaData (ChatResponseDto .MetaData .builder ()
147+ .currentStep (0 )
148+ .actionType ("질문형 추천" )
149+ .build ())
150+ .build ();
151+ }
152+
153+ // 실제 질문이 들어온 경우 - 먼저 로딩 메시지 반환
154+ if (requestDto .getMessage () != null && !requestDto .getMessage ().trim ().isEmpty ()) {
155+ // 로딩 메시지인지 확인 (두구두구 메시지 이후의 실제 처리 요청)
156+ if (requestDto .getMessage ().contains ("PROCESS_RECOMMENDATION" )) {
157+ log .info ("질문형 추천 실제 처리 - userId: {}" , requestDto .getUserId ());
158+ return generateAIResponseWithContext (requestDto , "질문형 추천" );
159+ }
160+
161+ // 사용자 질문 저장
162+ ChatConversation userQuestion = ChatConversation .builder ()
163+ .userId (requestDto .getUserId ())
164+ .message (requestDto .getMessage ())
165+ .sender (MessageSender .USER )
166+ .createdAt (LocalDateTime .now ())
167+ .build ();
168+ chatConversationRepository .save (userQuestion );
169+
170+ // 고정 로딩 메시지
171+ String loadingMessage = "당신에게 어울리는 칵테일은?\n 두구❤️두구💛두구💚두구💙두구💜두구🖤두구🤍두구🤎" ;
172+
173+ ChatConversation loadingBot = ChatConversation .builder ()
174+ .userId (requestDto .getUserId ())
175+ .message (loadingMessage )
176+ .sender (MessageSender .CHATBOT )
177+ .createdAt (LocalDateTime .now ())
178+ .build ();
179+ ChatConversation savedLoading = chatConversationRepository .save (loadingBot );
180+
181+ // 로딩 메시지 반환 (FE에서 이후 자동으로 실제 추천 요청)
182+ return ChatResponseDto .builder ()
183+ .id (savedLoading .getId ())
184+ .userId (requestDto .getUserId ())
185+ .message (loadingMessage )
186+ .sender (MessageSender .CHATBOT )
187+ .type (MessageType .LOADING )
188+ .createdAt (savedLoading .getCreatedAt ())
189+ .metaData (ChatResponseDto .MetaData .builder ()
190+ .currentStep (0 )
191+ .actionType ("LOADING_QA" )
192+ .isTyping (true )
193+ .delay (2000 ) // 2초 후 자동 요청
194+ .build ())
195+ .build ();
196+ }
103197 }
104198 else if (currentStep >= 1 && currentStep <= 4 ) {
105199 // 단계별 추천
@@ -117,8 +211,6 @@ else if (currentStep >= 1 && currentStep <= 4) {
117211 // ========== 2순위: 키워드 감지 (하위 호환성) ==========
118212 if (isStepRecommendationTrigger (requestDto .getMessage ())) {
119213 log .info ("[LEGACY] 키워드 기반 단계별 추천 감지 - userId: {}" , requestDto .getUserId ());
120-
121- // FE에서 currentStep을 보내지 않았을 때 자동 설정
122214 requestDto .setCurrentStep (1 );
123215 return handleStepRecommendation (requestDto );
124216 }
@@ -470,9 +562,21 @@ private ChatResponseDto createErrorResponse(String errorMessage) {
470562 .createdAt (LocalDateTime .now ())
471563 .build ();
472564 }
473-
474565 private ChatResponseDto handleStepRecommendation (ChatRequestDto requestDto ) {
475566 Integer currentStep = requestDto .getCurrentStep ();
567+
568+ // 단계별 추천 선택 시 처리
569+ if (currentStep == 1 && "STEP" .equalsIgnoreCase (requestDto .getMessage ())) {
570+ // 사용자 선택 메시지 저장
571+ ChatConversation userChoice = ChatConversation .builder ()
572+ .userId (requestDto .getUserId ())
573+ .message ("단계별 취향 찾기" )
574+ .sender (MessageSender .USER )
575+ .createdAt (LocalDateTime .now ())
576+ .build ();
577+ chatConversationRepository .save (userChoice );
578+ }
579+
476580 if (currentStep == null || currentStep <= 0 ) {
477581 currentStep = 1 ;
478582 }
@@ -496,24 +600,79 @@ private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
496600
497601 case 3 :
498602 stepData = new StepRecommendationResponseDto (
499- 3 ,
500- null ,
501- null ,
502- null ,
503- false
603+ 3 ,
604+ null ,
605+ null ,
606+ null ,
607+ false
504608 );
505- message = "좋아요! 이제 원하는 칵테일 스타일을 자유롭게 말씀해주세요 💬\n 없으면 'x', 또는 '없음' 을 입력해주세요!" ;
609+ message = "좋아요! 이제 원하는 칵테일 스타일을 자유롭게 말씀해주세요 💬\n 없으면 'x', 또는 '없음'을 입력해주세요!" ;
506610 type = MessageType .INPUT ;
507611 break ;
508612
509613 case 4 :
614+ // Step 4에서 로딩 메시지 처리
615+ if (!"PROCESS_STEP_RECOMMENDATION" .equals (requestDto .getMessage ())) {
616+ // 사용자 입력 저장 (Step 3의 답변) 및 userStyleInput에 저장
617+ if (requestDto .getMessage () != null && !requestDto .getMessage ().trim ().isEmpty ()) {
618+ // DB에 저장
619+ ChatConversation userInput = ChatConversation .builder ()
620+ .userId (requestDto .getUserId ())
621+ .message (requestDto .getMessage ())
622+ .sender (MessageSender .USER )
623+ .createdAt (LocalDateTime .now ())
624+ .build ();
625+ chatConversationRepository .save (userInput );
626+
627+ // userStyleInput에 저장 (다음 요청에서 사용)
628+ requestDto .setUserStyleInput (requestDto .getMessage ());
629+ log .info ("Step 3 사용자 입력 저장: {}" , requestDto .getMessage ());
630+ }
631+
632+ // 고정 로딩 메시지
633+ String loadingMessage = "당신에게 어울리는 칵테일은?\n 두구❤️두구💛두구💚두구💙두구💜두구🖤두구🤍두구🤎" ;
634+
635+ ChatConversation loadingBot = ChatConversation .builder ()
636+ .userId (requestDto .getUserId ())
637+ .message (loadingMessage )
638+ .sender (MessageSender .CHATBOT )
639+ .createdAt (LocalDateTime .now ())
640+ .build ();
641+ ChatConversation savedLoading = chatConversationRepository .save (loadingBot );
642+
643+ // 로딩 메시지 반환
644+ return ChatResponseDto .builder ()
645+ .id (savedLoading .getId ())
646+ .userId (requestDto .getUserId ())
647+ .message (loadingMessage )
648+ .sender (MessageSender .CHATBOT )
649+ .type (MessageType .LOADING )
650+ .createdAt (savedLoading .getCreatedAt ())
651+ .metaData (ChatResponseDto .MetaData .builder ()
652+ .currentStep (4 )
653+ .totalSteps (4 )
654+ .actionType ("LOADING_STEP" )
655+ .isTyping (true )
656+ .delay (2000 ) // 2초 후 자동 요청
657+ .build ())
658+ .stepData (new StepRecommendationResponseDto (
659+ 4 ,
660+ null ,
661+ null ,
662+ null ,
663+ false
664+ ))
665+ .build ();
666+ }
667+
668+ // 실제 추천 처리 - userStyleInput 사용 (PROCESS_STEP_RECOMMENDATION 키워드 아님)
510669 stepData = getFinalRecommendationsWithMessage (
511- parseAlcoholStrength (requestDto .getSelectedAlcoholStrength ()),
512- parseAlcoholBaseType (requestDto .getSelectedAlcoholBaseType ()),
513- requestDto .getMessage ()
670+ parseAlcoholStrength (requestDto .getSelectedAlcoholStrength ()),
671+ parseAlcoholBaseType (requestDto .getSelectedAlcoholBaseType ()),
672+ requestDto .getUserStyleInput () // message 대신 userStyleInput 사용
514673 );
515674 message = stepData .getStepTitle ();
516- type = MessageType .CARD_LIST ; // 최종 추천은 카드 리스트
675+ type = MessageType .CARD_LIST ;
517676 break ;
518677
519678 default :
@@ -522,15 +681,6 @@ private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
522681 type = MessageType .RADIO_OPTIONS ;
523682 }
524683
525- // 사용자 메시지 저장 (단계별 추천 요청)
526- ChatConversation userMessage = ChatConversation .builder ()
527- .userId (requestDto .getUserId ())
528- .message (requestDto .getMessage ())
529- .sender (MessageSender .USER )
530- .createdAt (LocalDateTime .now ())
531- .build ();
532- chatConversationRepository .save (userMessage );
533-
534684 // 봇 응답 저장
535685 ChatConversation botResponse = ChatConversation .builder ()
536686 .userId (requestDto .getUserId ())
@@ -544,7 +694,7 @@ private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
544694 ChatResponseDto .MetaData metaData = ChatResponseDto .MetaData .builder ()
545695 .currentStep (currentStep )
546696 .totalSteps (4 )
547- .isTyping (true )
697+ .isTyping (type != MessageType . CARD_LIST ) // 카드리스트는 타이핑 애니메이션 불필요
548698 .delay (300 )
549699 .build ();
550700
@@ -559,7 +709,6 @@ private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
559709 .createdAt (savedResponse .getCreatedAt ())
560710 .build ();
561711 }
562-
563712 // ============ 단계별 추천 관련 메서드들 ============
564713 // "ALL" 또는 null/빈값은 null로 처리하여 전체 선택 의미
565714
0 commit comments