Skip to content

Commit 5b8bf02

Browse files
committed
Refact : converter, clovaApiClient 책임 분리
- 중복되는 DTO 제거 - Clova 응답 스키마에 대해 서비스에서 NPE 가드 추가 - 패키지 구조 정리
1 parent da02a57 commit 5b8bf02

File tree

14 files changed

+195
-258
lines changed

14 files changed

+195
-258
lines changed

src/main/java/com/perfact/be/domain/chat/controller/ChatController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.perfact.be.domain.chat.controller;
22

33

4-
import com.perfact.be.domain.chat.dto.ChatRequestDTO;
4+
import com.perfact.be.domain.chat.dto.ChatRequest;
55
import com.perfact.be.domain.chat.dto.ChatResponse;
66
import com.perfact.be.domain.chat.service.ChatService;
77
import com.perfact.be.global.apiPayload.ApiResponse;
@@ -25,7 +25,7 @@ public class ChatController {
2525
@PostMapping("/{reportId}/chat")
2626
public ApiResponse<ChatResponse.ChatResponseDTO> sendMessage(
2727
@Parameter(description = "리포트 ID", required = true, example = "1") @PathVariable Long reportId,
28-
@Parameter(description = "채팅 요청", required = true) @RequestBody ChatRequestDTO request) {
28+
@Parameter(description = "채팅 요청", required = true) @RequestBody ChatRequest request) {
2929
ChatResponse.ChatResponseDTO response = chatServiceImpl.sendMessage(reportId, request);
3030
return ApiResponse.onSuccess(response);
3131
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.perfact.be.domain.chat.converter;
2+
3+
import com.perfact.be.domain.chat.exception.ChatHandler;
4+
import com.perfact.be.domain.chat.exception.status.ChatErrorStatus;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Component;
7+
8+
@Slf4j
9+
@Component
10+
public class ChatMessageConverter {
11+
12+
// AI 응답 메시지를 다듬어서 반환
13+
public String polishMessage(String rawMessage) {
14+
if (rawMessage == null || rawMessage.trim().isEmpty()) {
15+
throw new ChatHandler(ChatErrorStatus.CHAT_MESSAGE_PARSING_FAILED);
16+
}
17+
18+
try {
19+
// 앞뒤 공백 제거
20+
String polished = rawMessage.trim()
21+
.replaceAll("\n{3,}", "\n\n")
22+
.replaceAll("([.!?])\n", "$1\n\n");
23+
log.debug("메시지 다듬기 완료 - 원본 길이: {}, 다듬은 길이: {}",
24+
rawMessage.length(), polished.length());
25+
return polished;
26+
27+
} catch (Exception e) {
28+
log.error("메시지 다듬기 실패: {}", e.getMessage(), e);
29+
throw new ChatHandler(ChatErrorStatus.CHAT_MESSAGE_PARSING_FAILED);
30+
}
31+
32+
}
33+
34+
}
Lines changed: 32 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,67 @@
11
package com.perfact.be.domain.chat.converter;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import com.perfact.be.domain.chat.dto.ClovaChatRequestDTO;
5-
import com.perfact.be.domain.chat.dto.ClovaChatResponseDTO;
6-
import com.perfact.be.domain.chat.dto.ClovaRecommendRequestDTO;
7-
import com.perfact.be.domain.chat.dto.ClovaRecommendResponseDTO;
8-
import com.perfact.be.domain.chat.exception.ChatHandler;
9-
import com.perfact.be.domain.chat.exception.status.ChatErrorStatus;
10-
import lombok.RequiredArgsConstructor;
11-
import lombok.extern.slf4j.Slf4j;
12-
import org.springframework.beans.factory.annotation.Value;
13-
import org.springframework.http.HttpEntity;
14-
import org.springframework.http.HttpHeaders;
15-
import org.springframework.http.HttpMethod;
16-
import org.springframework.http.MediaType;
17-
import org.springframework.http.ResponseEntity;
184
import org.springframework.stereotype.Component;
19-
import org.springframework.web.client.RestTemplate;
205

216
import java.util.ArrayList;
227
import java.util.List;
238

24-
@Slf4j
9+
2510
@Component
26-
@RequiredArgsConstructor
2711
public class ClovaApiConverter {
2812

29-
private final RestTemplate restTemplate;
30-
private final ObjectMapper objectMapper;
31-
32-
@Value("${api.clova.chat-url}")
33-
private String CLOVA_CHAT_URL;
34-
35-
@Value("${api.clova.api-key}")
36-
private String CLOVA_API_KEY;
13+
private static final String SYSTEM_CHAT_PROMPT = "Important: The service name is 'Perfact' (P-e-r-f-a-c-t). This is the correct spelling, not 'Perfect'. You must always use the name 'Perfact' when referring to the service.\n\nYou are a friendly and helpful AI assistant for the 'Perfact' service. Your role is to answer user questions by referring to both the original news article (`기사 원문`) and the provided AI analysis report summary (`AI 분석 리포트 요약`). Use the original article as the primary source for facts and the analysis report for context about reliability. Do not make up information. If the answer is not in the provided context, say you don't know. Answer in Korean.";
14+
private static final String SYSTEM_RECOMMEND_PROMPT = "You are an AI assistant that creates insightful recommended questions. Based on the provided analysis report summary, your goal is to generate two concise and relevant questions a user would most likely ask. The questions should highlight potential weaknesses or interesting points from the report (e.g., low scores, specific badges). Your final output MUST be a JSON array containing exactly two strings, like [\"질문 1\", \"질문 2\"]. Answer in Korean.";
15+
private static final double DEFAULT_TOP_P = 0.8;
16+
private static final int DEFAULT_TOP_K = 0;
17+
private static final int CHAT_MAX_TOKENS = 512;
18+
private static final int RECO_MAX_TOKENS = 100;
19+
private static final double DEFAULT_TEMPERATURE = 0.5;
20+
private static final double DEFAULT_REPEAT_PENALTY = 5.0;
3721

3822
// 채팅 API 호출을 위한 요청을 생성
3923
public ClovaChatRequestDTO createChatRequest(String chatbotContext, String articleContent, String userInput) {
40-
List<ClovaChatRequestDTO.Message> messages = new ArrayList<>();
41-
messages.add(ClovaChatRequestDTO.Message.builder()
42-
.role("system")
43-
.content(
44-
"Important: The service name is 'Perfact' (P-e-r-f-a-c-t). This is the correct spelling, not 'Perfect'. You must always use the name 'Perfact' when referring to the service.\n\nYou are a friendly and helpful AI assistant for the 'Perfact' service. Your role is to answer user questions by referring to both the original news article (`기사 원문`) and the provided AI analysis report summary (`AI 분석 리포트 요약`). Use the original article as the primary source for facts and the analysis report for context about reliability. Do not make up information. If the answer is not in the provided context, say you don't know. Answer in Korean.")
45-
.build());
46-
4724
// 사용자 메시지 (기사 원문 + 분석 리포트 요약 + 질문)
48-
String userContent = "[분석 대상 기사 원문]\n" + articleContent + "\n\n---\n\n[AI 분석 리포트 요약]\n" + chatbotContext
25+
String userContent = "[분석 대상 기사 원문]\n" + articleContent
26+
+ "\n\n---\n\n[AI 분석 리포트 요약]\n" + chatbotContext
4927
+ "\n\n이 내용을 기반으로 사용자의 질문에 상세히 답변하세요.\n\n---\n\n[사용자 질문]\n" + userInput;
50-
messages.add(ClovaChatRequestDTO.Message.builder()
51-
.role("user")
52-
.content(userContent)
53-
.build());
5428

5529
return ClovaChatRequestDTO.builder()
56-
.messages(messages)
57-
.topP(0.8)
58-
.topK(0)
59-
.maxTokens(512)
60-
.temperature(0.5)
61-
.repeatPenalty(5.0)
30+
.messages(
31+
List.of(
32+
ClovaChatRequestDTO.Message.builder().role("system").content(SYSTEM_CHAT_PROMPT).build(),
33+
ClovaChatRequestDTO.Message.builder().role("user").content(userContent).build()
34+
))
35+
.topP(DEFAULT_TOP_P)
36+
.topK(DEFAULT_TOP_K)
37+
.maxTokens(CHAT_MAX_TOKENS)
38+
.temperature(DEFAULT_TEMPERATURE)
39+
.repeatPenalty(DEFAULT_REPEAT_PENALTY)
6240
.stopBefore(new ArrayList<>())
6341
.includeAiFilters(true)
6442
.seed(0)
6543
.build();
44+
6645
}
6746

6847
// 추천 질문 API 호출을 위한 요청을 생성
69-
public ClovaRecommendRequestDTO createRecommendRequest(String chatbotContext) {
70-
List<ClovaRecommendRequestDTO.Message> messages = new ArrayList<>();
71-
72-
// 시스템 메시지
73-
messages.add(ClovaRecommendRequestDTO.Message.builder()
74-
.role("system")
75-
.content(
76-
"You are an AI assistant that creates insightful recommended questions. Based on the provided analysis report summary, your goal is to generate two concise and relevant questions a user would most likely ask. The questions should highlight potential weaknesses or interesting points from the report (e.g., low scores, specific badges). Your final output MUST be a JSON array containing exactly two strings, like [\"질문 1\", \"질문 2\"]. Answer in Korean.")
77-
.build());
78-
48+
public ClovaChatRequestDTO createRecommendRequest(String chatbotContext) {
7949
// 사용자 메시지 (리포트 컨텍스트)
8050
String userContent = chatbotContext + "\n\n[지시]\n이 분석 리포트에서 사용자가 가장 궁금해할 만한 핵심 질문 2개를 추천해 주세요.";
81-
messages.add(ClovaRecommendRequestDTO.Message.builder()
82-
.role("user")
83-
.content(userContent)
84-
.build());
8551

86-
return ClovaRecommendRequestDTO.builder()
87-
.messages(messages)
88-
.topP(0.8)
89-
.topK(0)
90-
.maxTokens(100)
91-
.temperature(0.5)
92-
.repeatPenalty(5.0)
52+
return ClovaChatRequestDTO.builder()
53+
.messages(List.of(
54+
ClovaChatRequestDTO.Message.builder().role("system").content(SYSTEM_RECOMMEND_PROMPT).build(),
55+
ClovaChatRequestDTO.Message.builder().role("user").content(userContent).build()))
56+
.topP(DEFAULT_TOP_P)
57+
.topK(DEFAULT_TOP_K)
58+
.maxTokens(RECO_MAX_TOKENS)
59+
.temperature(DEFAULT_TEMPERATURE)
60+
.repeatPenalty(DEFAULT_REPEAT_PENALTY)
9361
.stopBefore(new ArrayList<>())
9462
.includeAiFilters(true)
9563
.seed(0)
9664
.build();
9765
}
9866

99-
// Clova 채팅 API를 호출
100-
public ClovaChatResponseDTO callChatAPI(ClovaChatRequestDTO request) {
101-
try {
102-
log.info("Clova 채팅 API 호출");
103-
104-
HttpHeaders headers = new HttpHeaders();
105-
headers.setContentType(MediaType.APPLICATION_JSON);
106-
headers.setBearerAuth(CLOVA_API_KEY);
107-
108-
String requestBody = objectMapper.writeValueAsString(request);
109-
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
110-
111-
ResponseEntity<ClovaChatResponseDTO> response = restTemplate.exchange(
112-
CLOVA_CHAT_URL,
113-
HttpMethod.POST,
114-
entity,
115-
ClovaChatResponseDTO.class);
116-
117-
log.info("Clova 채팅 API 응답 성공");
118-
return response.getBody();
119-
120-
} catch (Exception e) {
121-
log.error("Clova 채팅 API 호출 실패: {}", e.getMessage(), e);
122-
throw new ChatHandler(ChatErrorStatus.CHAT_API_CALL_FAILED);
123-
}
124-
}
125-
126-
// Clova 추천 질문 API를 호출
127-
public ClovaRecommendResponseDTO callRecommendAPI(ClovaRecommendRequestDTO request) {
128-
try {
129-
log.info("Clova 추천 질문 API 호출");
130-
131-
HttpHeaders headers = new HttpHeaders();
132-
headers.setContentType(MediaType.APPLICATION_JSON);
133-
headers.setBearerAuth(CLOVA_API_KEY);
134-
135-
HttpEntity<ClovaRecommendRequestDTO> entity = new HttpEntity<>(request, headers);
136-
137-
ResponseEntity<ClovaRecommendResponseDTO> response = restTemplate.exchange(
138-
CLOVA_CHAT_URL,
139-
HttpMethod.POST,
140-
entity,
141-
ClovaRecommendResponseDTO.class);
142-
143-
log.info("Clova 추천 질문 API 응답 성공");
144-
return response.getBody();
145-
146-
} catch (Exception e) {
147-
log.error("Clova 추천 질문 API 호출 실패: {}", e.getMessage(), e);
148-
throw new ChatHandler(ChatErrorStatus.CHAT_API_CALL_FAILED);
149-
}
150-
}
15167
}

src/main/java/com/perfact/be/domain/chat/converter/JsonConverter.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,23 @@ public class JsonConverter {
1616

1717
private final ObjectMapper objectMapper;
1818

19-
// JSON 배열 형태의 문자열을 List<String>으로 변환
2019
public List<String> parseJsonArray(String content) {
20+
// content가 null이거나 비어있는지 확인
21+
if (content == null || content.trim().isEmpty()) {
22+
log.error("JSON 파싱할 content가 null이거나 비어있음");
23+
throw new ChatHandler(ChatErrorStatus.CHAT_MESSAGE_PARSING_FAILED);
24+
}
2125
try {
22-
log.debug("JSON 배열 파싱 시작 - content: {}", content);
23-
24-
// content가 null이거나 비어있는지 확인
25-
if (content == null || content.trim().isEmpty()) {
26-
log.error("JSON 파싱할 content가 null이거나 비어있음");
27-
throw new ChatHandler(ChatErrorStatus.CHAT_MESSAGE_PARSING_FAILED);
28-
}
29-
3026
// JSON 배열 형태의 문자열을 List<String>으로 변환
3127
List<String> result = objectMapper.readValue(content, List.class);
32-
3328
log.debug("JSON 배열 파싱 완료 - 결과 개수: {}, 결과: {}", result.size(), result);
3429
return result;
3530

3631
} catch (Exception e) {
3732
log.error("JSON 배열 파싱 실패 - content: {}, 에러: {}", content, e.getMessage(), e);
3833
throw new ChatHandler(ChatErrorStatus.CHAT_MESSAGE_PARSING_FAILED);
3934
}
35+
4036
}
37+
4138
}

src/main/java/com/perfact/be/domain/chat/dto/ChatRequestDTO.java renamed to src/main/java/com/perfact/be/domain/chat/dto/ChatRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@NoArgsConstructor
1111
@AllArgsConstructor
1212
@Schema(description = "채팅 요청 DTO")
13-
public class ChatRequestDTO {
13+
public class ChatRequest {
1414

1515
@NotBlank(message = "사용자 질문은 필수 입력값입니다.")
1616
@Schema(description = "사용자 질문", example = "왜 총점이 85점인가요?")

src/main/java/com/perfact/be/domain/chat/dto/ClovaChatRequestDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@Builder
1313
@NoArgsConstructor
1414
@AllArgsConstructor
15-
@Schema(description = "Clova 채팅 API 요청 DTO")
15+
@Schema(description = "Clova 공통 요청 DTO")
1616
public class ClovaChatRequestDTO {
1717

1818
private List<Message> messages;

src/main/java/com/perfact/be/domain/chat/dto/ClovaChatResponseDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
@Getter
99
@NoArgsConstructor
1010
@AllArgsConstructor
11-
@Schema(description = "Clova 채팅 API 응답 DTO")
11+
@Schema(description = "Clova 공통 응답 DTO")
1212
public class ClovaChatResponseDTO {
1313

1414
private Status status;

src/main/java/com/perfact/be/domain/chat/dto/ClovaRecommendRequestDTO.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/main/java/com/perfact/be/domain/chat/dto/ClovaRecommendResponseDTO.java

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/main/java/com/perfact/be/domain/chat/exception/status/ChatErrorStatus.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public enum ChatErrorStatus implements BaseErrorCode {
1313
CHAT_API_CALL_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CHAT4001", "채팅 API 호출에 실패했습니다."),
1414
CHAT_MESSAGE_PARSING_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CHAT4002", "채팅 메시지 파싱에 실패했습니다."),
1515
CHAT_LOG_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "CHAT4003", "채팅 로그 저장에 실패했습니다."),
16-
CHAT_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "CHAT4004", "해당 리포트를 찾을 수 없습니다.");
16+
CHAT_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "CHAT4004", "해당 리포트를 찾을 수 없습니다."),
1717

18+
;
1819
private final HttpStatus httpStatus;
1920
private final String code;
2021
private final String message;

0 commit comments

Comments
 (0)