Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ public ResponseEntity<RsData<List<ChatConversation>>> getUserChatHistory(@PathVa
.body(RsData.failOf("서버 오류가 발생했습니다."));
}
}

}
11 changes: 10 additions & 1 deletion src/main/java/com/back/domain/chatbot/dto/ChatRequestDto.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.back.domain.chatbot.dto;

import com.back.domain.cocktail.enums.AlcoholBaseType;
import com.back.domain.cocktail.enums.AlcoholStrength;
import com.back.domain.cocktail.enums.CocktailType;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -13,6 +16,12 @@ public class ChatRequestDto {
@NotBlank(message = "메시지는 필수입니다.")
private String message;


private Long userId;

// 단계별 추천 관련 필드들
private boolean isStepRecommendation = false;
private Integer currentStep;
private AlcoholStrength selectedAlcoholStrength;
private AlcoholBaseType selectedAlcoholBaseType;
private CocktailType selectedCocktailType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ public class ChatResponseDto {
private String response;
private LocalDateTime timestamp;

// 단계별 추천 관련 필드 (선택사항)
private StepRecommendationResponseDto stepRecommendation;

public ChatResponseDto(String response) {
this.response = response;
this.timestamp = LocalDateTime.now();
}

public ChatResponseDto(String response, StepRecommendationResponseDto stepRecommendation) {
this.response = response;
this.timestamp = LocalDateTime.now();
this.stepRecommendation = stepRecommendation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.back.domain.chatbot.dto;

import com.back.domain.cocktail.dto.CocktailSummaryResponseDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class StepRecommendationResponseDto {

private Integer currentStep; // 현재 단계
private String stepTitle; // 단계 제목 (예: "원하시는 도수를 선택해주세요!")
private List<StepOption> options; // 선택 옵션들
private List<CocktailSummaryResponseDto> recommendations; // 최종 추천 칵테일 (4단계에서만)
private boolean isCompleted; // 추천이 완료되었는지 여부

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class StepOption {
private String value; // enum 값 (예: "NON_ALCOHOLIC")
private String label; // 화면에 표시될 텍스트 (예: "논알콜 (0%)")
private String description; // 부가 설명 (선택사항)
}
}
172 changes: 172 additions & 0 deletions src/main/java/com/back/domain/chatbot/service/ChatbotService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

import com.back.domain.chatbot.dto.ChatRequestDto;
import com.back.domain.chatbot.dto.ChatResponseDto;
import com.back.domain.chatbot.dto.StepRecommendationResponseDto;
import com.back.domain.chatbot.entity.ChatConversation;
import com.back.domain.chatbot.repository.ChatConversationRepository;
import com.back.domain.cocktail.dto.CocktailSummaryResponseDto;
import com.back.domain.cocktail.entity.Cocktail;
import com.back.domain.cocktail.enums.AlcoholBaseType;
import com.back.domain.cocktail.enums.AlcoholStrength;
import com.back.domain.cocktail.enums.CocktailType;
import com.back.domain.cocktail.repository.CocktailRepository;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -12,6 +19,8 @@
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -20,8 +29,10 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -30,6 +41,7 @@ public class ChatbotService {

private final ChatModel chatModel;
private final ChatConversationRepository chatConversationRepository;
private final CocktailRepository cocktailRepository;


@Value("classpath:prompts/chatbot-system-prompt.txt")
Expand Down Expand Up @@ -77,6 +89,16 @@ public void init() throws IOException {
@Transactional
public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
try {
// 단계별 추천 모드 확인 (currentStep이 있으면 무조건 단계별 추천 모드)
if (requestDto.isStepRecommendation() ||
requestDto.getCurrentStep() != null ||
isStepRecommendationTrigger(requestDto.getMessage())) {
log.info("Recommendation chat mode for userId: {}", requestDto.getUserId());
return handleStepRecommendation(requestDto);
}

log.info("Normal chat mode for userId: {}", requestDto.getUserId());

// 메시지 타입 감지
MessageType messageType = detectMessageType(requestDto.getMessage());

Expand Down Expand Up @@ -238,10 +260,160 @@ private MessageType detectMessageType(String message) {
return MessageType.CASUAL_CHAT;
}

// 단계별 추천 시작 키워드 감지
private boolean isStepRecommendationTrigger(String message) {
String lower = message.toLowerCase().trim();
return lower.contains("단계별 추천");
}

// 단계별 추천 처리 통합 메서드
private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
Integer currentStep = requestDto.getCurrentStep();

// 단계가 지정되지 않았거나 첫 시작인 경우
if (currentStep == null || currentStep <= 0) {
currentStep = 1;
}

StepRecommendationResponseDto stepRecommendation;
String chatResponse;

switch (currentStep) {
case 1:
stepRecommendation = getAlcoholStrengthOptions();
chatResponse = "단계별 맞춤 추천을 시작합니다! 🎯\n원하시는 도수를 선택해주세요!";
break;
case 2:
stepRecommendation = getAlcoholBaseTypeOptions(requestDto.getSelectedAlcoholStrength());
chatResponse = "좋은 선택이네요! 이제 베이스가 될 술을 선택해주세요 🍸";
break;
case 3:
stepRecommendation = getCocktailTypeOptions(requestDto.getSelectedAlcoholStrength(), requestDto.getSelectedAlcoholBaseType());
chatResponse = "완벽해요! 마지막으로 어떤 스타일로 즐기실 건가요? 🥃";
break;
case 4:
stepRecommendation = getFinalRecommendations(
requestDto.getSelectedAlcoholStrength(),
requestDto.getSelectedAlcoholBaseType(),
requestDto.getSelectedCocktailType()
);
chatResponse = stepRecommendation.getStepTitle();
break;
default:
stepRecommendation = getAlcoholStrengthOptions();
chatResponse = "단계별 맞춤 추천을 시작합니다! 🎯";
}

// 대화 기록 저장
saveConversation(requestDto, chatResponse);

return new ChatResponseDto(chatResponse, stepRecommendation);
}

@Transactional(readOnly = true)
public List<ChatConversation> getUserChatHistory(Long userId) {
return chatConversationRepository.findByUserIdOrderByCreatedAtDesc(userId, Pageable.unpaged()).getContent();
}


private StepRecommendationResponseDto getAlcoholStrengthOptions() {
List<StepRecommendationResponseDto.StepOption> options = new ArrayList<>();

for (AlcoholStrength strength : AlcoholStrength.values()) {
options.add(new StepRecommendationResponseDto.StepOption(
strength.name(),
strength.getDescription(),
null
));
}

return new StepRecommendationResponseDto(
1,
"원하시는 도수를 선택해주세요!",
options,
null,
false
);
}

private StepRecommendationResponseDto getAlcoholBaseTypeOptions(AlcoholStrength alcoholStrength) {
List<StepRecommendationResponseDto.StepOption> options = new ArrayList<>();

for (AlcoholBaseType baseType : AlcoholBaseType.values()) {
options.add(new StepRecommendationResponseDto.StepOption(
baseType.name(),
baseType.getDescription(),
null
));
}

return new StepRecommendationResponseDto(
2,
"베이스가 될 술을 선택해주세요!",
options,
null,
false
);
}

private StepRecommendationResponseDto getCocktailTypeOptions(AlcoholStrength alcoholStrength, AlcoholBaseType alcoholBaseType) {
List<StepRecommendationResponseDto.StepOption> options = new ArrayList<>();

for (CocktailType cocktailType : CocktailType.values()) {
options.add(new StepRecommendationResponseDto.StepOption(
cocktailType.name(),
cocktailType.getDescription(),
null
));
}

return new StepRecommendationResponseDto(
3,
"어떤 종류의 잔으로 드시겠어요?",
options,
null,
false
);
}

private StepRecommendationResponseDto getFinalRecommendations(
AlcoholStrength alcoholStrength,
AlcoholBaseType alcoholBaseType,
CocktailType cocktailType) {
// 필터링 조건에 맞는 칵테일 검색
List<AlcoholStrength> strengths = List.of(alcoholStrength);
List<AlcoholBaseType> baseTypes = List.of(alcoholBaseType);
List<CocktailType> cocktailTypes = List.of(cocktailType);

Page<Cocktail> cocktailPage = cocktailRepository.searchWithFilters(
null, // 키워드 없음
strengths,
cocktailTypes,
baseTypes,
PageRequest.of(0, 5) // 최대 5개 추천
);

List<CocktailSummaryResponseDto> recommendations = cocktailPage.getContent().stream()
.map(cocktail -> new CocktailSummaryResponseDto(
cocktail.getId(),
cocktail.getCocktailName(),
cocktail.getCocktailImgUrl(),
cocktail.getAlcoholStrength()
))
.collect(Collectors.toList());

String stepTitle = recommendations.isEmpty()
? "조건에 맞는 칵테일을 찾을 수 없습니다 😢"
: "당신을 위한 맞춤 칵테일 추천입니다! 🍹";

return new StepRecommendationResponseDto(
4,
stepTitle,
null,
recommendations,
true
);
}

}

2 changes: 1 addition & 1 deletion src/main/resources/prompts/chatbot-response-rules.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
위의 시스템 프롬프트와 대화 기록을 참고하여, '쑤울 AI 바텐더'로서 친근하고 전문적인 답변을 제공해주세요.
위의 시스템 프롬프트와 대화 기록을 참고하여, 칵테일 AI 바텐더 '쑤리'로서 친근하고 전문적인 답변을 제공해주세요.
칵테일과 관련된 유용한 정보를 포함하되, 자연스럽고 대화하듯 응답해주세요.

【중요한 응답 규칙】
Expand Down
11 changes: 6 additions & 5 deletions src/main/resources/prompts/chatbot-system-prompt.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
당신은 'Ssoul' 칵테일 전문 AI 바텐더입니다.
당신은 'Ssoul' 칵테일 전문 AI 바텐더 '쑤리' 입니다.

## 역할과 페르소나
- 이름: 쑤울 AI 바텐더
- 이름: 쑤리
- 성격: 친근하고 전문적이며, 유머러스하면서도 신뢰할 수 있는 칵테일 전문가
- 말투: 반말이 아닌 존댓말을 사용하며, 친근한 바텐더처럼 대화
- 특징: 칵테일에 대한 깊은 지식과 함께 상황에 맞는 칵테일 추천 능력
Expand All @@ -21,8 +21,8 @@
5. **창의성**: 클래식 칵테일 외에도 현대적 변형이나 논알콜 대안 제시

## 응답 길이 제한
- **기본 답변**: 200자 이내로 간결하게 작성
- **레시피 제공**: 최대 300자 이내로 핵심만 전달
- **기본 답변**: 300자 이내로 간결하게 작성
- **레시피 제공**: 최대 400자 이내로 핵심만 전달
- **복잡한 설명**: 필요시 "더 알고 싶으시면 추가로 질문해주세요"로 마무리
- **한 문단**: 최대 3-4문장으로 제한

Expand Down Expand Up @@ -56,4 +56,5 @@
- 논알콜 칵테일(목테일)도 적극적으로 소개
- 홈바 입문자를 위한 기본 도구와 재료 안내
- 칵테일과 어울리는 안주나 분위기 추천
- 과음 방지를 위한 적절한 조언 포함
- 과음 방지를 위한 적절한 조언 포함
- 칵테일 추천에 대한 이유를 간단히 특징에 언급