Skip to content

Commit c4e4a2f

Browse files
authored
Merge pull request #21 from GDGoC-OneWave33/dev
deploy: main <- dev
2 parents b8de7c6 + 1668710 commit c4e4a2f

12 files changed

Lines changed: 194 additions & 29 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build/
44
!gradle/wrapper/gradle-wrapper.jar
55
!**/src/main/**/build/
66
!**/src/test/**/build/
7-
7+
.env
88
### STS ###
99
.apt_generated
1010
.classpath

src/main/java/com/example/demo/VoidApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class VoidApplication {
810

911
public static void main(String[] args) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.example.demo.core.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
5+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6+
7+
@Configuration
8+
public class WebConfig implements WebMvcConfigurer {
9+
10+
@Override
11+
public void addCorsMappings(CorsRegistry registry) {
12+
registry.addMapping("/**") // 모든 경로에 대해
13+
.allowedOrigins( "https://jyhdevstore.store","https://void-fe-nine.vercel.app","https://voidvoid.store/") // 프론트엔드, NER, BE
14+
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
15+
.allowedHeaders("*")
16+
.allowCredentials(true)
17+
.maxAge(3600);
18+
}
19+
20+
}

src/main/java/com/example/demo/domain/gemini/controller/GeminiController.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.example.demo.domain.gemini.controller;
22

33
import com.example.demo.domain.gemini.service.GeminiService;
4+
import com.example.demo.domain.nerfilter.dto.AiResponse;
5+
import com.example.demo.domain.nerfilter.dto.TextRequest;
46
import com.example.demo.shared.exception.CustomException;
57
import com.example.demo.shared.response.ApiResponse;
68
import com.example.demo.shared.response.ErrorCode;
@@ -24,32 +26,29 @@ public class GeminiController {
2426

2527
@Operation(
2628
summary = "감정 배출 및 키워드 추출",
27-
description = "유저의 감정(content)을 보내면 gemini가 답변과 핵심 키워드를 반환합니다."
29+
description = "유저의 감정(text)을 보내면 gemini가 답변과 핵심 키워드를 반환합니다."
2830
)
2931
@io.swagger.v3.oas.annotations.parameters.RequestBody(
3032
description = "상담할 고민 내용",
3133
required = true,
3234
content = @Content(
3335
mediaType = "application/json",
34-
schema = @Schema(example = "{\"content\": \"오늘 해커톤 너무 힘들다.\"}"),
36+
schema = @Schema(example = "{\"text\": \"오늘 해커톤 너무 힘들다.\"}"),
3537
examples = @ExampleObject(
3638
name = "고민 상담 예시",
37-
value = "{\"content\": \"해커톤 프로젝트가 생각보다 어려워서 속상해...\"}"
39+
value = "{\"text\": \"해커톤 프로젝트가 생각보다 어려워서 속상해...\"}"
3840
)
3941
)
4042
)
4143
@PostMapping("/ask")
42-
public ApiResponse<Map<String, Object>> ask(@RequestBody Map<String, String> request) {
43-
String userContent = request.get("content");
44-
45-
if (userContent == null || userContent.isBlank()) {
46-
return ApiResponse.onSuccess(
47-
Map.of("answer", "내용을 입력해 주세요!", "keyword", "입력 필요"),
48-
SuccessCode.OK
49-
);
44+
public ApiResponse<Map<String, Object>> ask(@RequestBody TextRequest textRequest) {
45+
String userText = textRequest.getText();
46+
47+
if (userText == null || userText.isBlank()) {
48+
throw new CustomException(ErrorCode.INVALID_REQUEST);
5049
}
5150

52-
Map<String, Object> result = geminiService.askGemini(userContent);
51+
Map<String, Object> result = geminiService.askGemini(userText);
5352

5453
return ApiResponse.onSuccess(result, SuccessCode.OK);
5554
}

src/main/java/com/example/demo/domain/gemini/exception/GeminiErrorCode.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public enum GeminiErrorCode implements BaseCode {
4747
HttpStatus.BAD_GATEWAY,
4848
"GEMINI-007",
4949
"GEMINI 응답에 아무 내용이 없습니다."
50+
),
51+
52+
GEMINI_INVALID_INPUT(
53+
HttpStatus.BAD_REQUEST,
54+
"GEMINI-008",
55+
"의미있는 고민을 입력해주세요."
5056
);
5157

5258
private final HttpStatus httpStatus;

src/main/java/com/example/demo/domain/gemini/service/GeminiService.java

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import com.example.demo.domain.gemini.dto.GeminiRequest;
44
import com.example.demo.domain.gemini.dto.GeminiResponse;
55
import com.example.demo.domain.gemini.exception.GeminiErrorCode;
6+
import com.example.demo.domain.nerfilter.dto.AiResponse;
7+
import com.example.demo.domain.nerfilter.dto.TextRequest;
8+
import com.example.demo.domain.nerfilter.service.AiAnalysisService;
69
import com.example.demo.domain.ranking.service.RankingService;
710
import com.example.demo.shared.exception.CustomException;
811
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -31,20 +34,38 @@ public class GeminiService {
3134
private final ObjectMapper objectMapper;
3235
private final RankingService rankingService;
3336

37+
private final AiAnalysisService aiAnalysisService;
38+
3439
public Map<String, Object> askGemini(String userContent) {
40+
TextRequest textRequest = new TextRequest(userContent, true);
41+
AiResponse aiResponse = aiAnalysisService.getAnalysis(textRequest);
42+
43+
String filteredContent = aiResponse.getFilteredText();
44+
if (filteredContent == null || filteredContent.isBlank()) {
45+
throw new CustomException(GeminiErrorCode.GEMINI_NO_CONTENT);
46+
}
47+
48+
3549
String cleanKey = apiKey.trim();
3650

3751
String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + cleanKey;
3852

39-
String systemPrompt = "유저의 고민을 듣고 다정하게 위로해줘." +
40-
"너무 ai 같지 않게 사람답게 말하고, 유저를 절대 비난해서는 안돼." +
41-
"무조건적인 공감과 지지를 보내줘. 핵심 키워드 3개 정도를 욕설 제외하고 뽑아줘. 갯수에 제한 받기 보다는, 정말 중요하다 생각되는 키워드를 뽑아."
42-
+ "반드시 JSON 형식으로만 대답해. 형식: {\"keyword\": [\"단어1\", \"단어2\", \"단어3\"], \"answer\": \"내용\"}";
53+
String systemPrompt = "너는 유저의 고민을 듣고 위로해주는 상담사야. " +
54+
"먼저 유저의 입력이 위로가 필요한 상황인지 판단해. " +
55+
"거부해야 할 입력: 숫자만 나열, 명백한 테스트 메시지(예: 'test', '테스트', '123'), 봇/스팸 같은 입력, 의미없는 단어 무한 반복. " +
56+
"위로해줘야 할 입력: 고민/감정 표현, 한글 자음모음을 막 친 것(빡치거나 힘들어서 그럴 수 있음), 욕설이나 분노 표현, 짧은 한숨이나 감정 표현. " +
57+
"만약 위로가 필요한 상황이라면: 다정하게 위로해주고, 너무 AI 같지 않게 사람답게 말해. " +
58+
"유저를 절대 비난하지 말고, 무조건적인 공감과 지지를 보내줘. " +
59+
"키보드를 막 친 것 같으면 '많이 힘드셨나봐요' 같은 공감을 표현해줘. " +
60+
"핵심 키워드를 욕설 제외하고 3개 정도 뽑아줘. 감정 키워드도 괜찮아. 갯수에 제한받기보다는 정말 중요한 키워드만 뽑아. " +
61+
"반드시 JSON 형식으로만 대답해. " +
62+
"위로할 때: {\"isValid\": true, \"keyword\": [\"단어1\", \"단어2\"], \"answer\": \"위로 내용\"} " +
63+
"거부할 때: {\"isValid\": false, \"keyword\": [], \"answer\": \"\"}";
4364

4465
HttpHeaders headers = new HttpHeaders();
4566
headers.setContentType(MediaType.APPLICATION_JSON);
4667

47-
GeminiRequest request = GeminiRequest.of(systemPrompt, userContent);
68+
GeminiRequest request = GeminiRequest.of(systemPrompt, filteredContent);
4869
HttpEntity<GeminiRequest> entity = new HttpEntity<>(request, headers);
4970

5071
try {
@@ -59,6 +80,13 @@ public Map<String, Object> askGemini(String userContent) {
5980
String cleanedJson = rawContent.replaceAll("(?s)```json|```", "").trim();
6081
Map<String, Object> result = objectMapper.readValue(cleanedJson, Map.class);
6182

83+
// 무의미한 입력인지 확인
84+
Object isValidObj = result.get("isValid");
85+
boolean isValid = isValidObj instanceof Boolean ? (Boolean) isValidObj : true;
86+
if (!isValid) {
87+
throw new CustomException(GeminiErrorCode.GEMINI_INVALID_INPUT);
88+
}
89+
6290
Object keywordObj = result.get("keyword");
6391
if (keywordObj instanceof List) {
6492
List<String> keywords = (List<String>) keywordObj;
@@ -72,11 +100,16 @@ public Map<String, Object> askGemini(String userContent) {
72100
}
73101
}
74102

103+
// isValid 필드 제거 후 반환
104+
result.remove("isValid");
75105
return result;
76106
} catch (JsonProcessingException e) {
77107
throw new CustomException(GeminiErrorCode.GEMINI_PARSE_ERROR);
78108
}
79109

110+
} catch (CustomException e) {
111+
// CustomException은 그대로 던지기
112+
throw e;
80113
} catch (HttpClientErrorException.NotFound e) {
81114
System.err.println("### 404 에러 상세: " + e.getResponseBodyAsString());
82115
throw new CustomException(GeminiErrorCode.GEMINI_API_ERROR);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.example.demo.domain.nerfilter.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
@Getter
13+
@Setter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class AiResponse {
17+
18+
@JsonProperty("original_text")
19+
private String originalText;
20+
21+
@JsonProperty("corrected_text")
22+
private String correctedText;
23+
24+
@JsonProperty("filtered_text")
25+
private String filteredText;
26+
27+
@JsonProperty("detected_entities")
28+
private List<Map<String, Object>> detectedEntities; // ✅ Object로 변경
29+
30+
@JsonProperty("spelling_errors")
31+
private List<Map<String, Object>> spellingErrors; // ✅ Object로 변경
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.demo.domain.nerfilter.dto;
2+
3+
public record NerEntity(
4+
String entity,
5+
double score,
6+
String word,
7+
int start,
8+
int end
9+
) {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.demo.domain.nerfilter.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor
10+
@AllArgsConstructor
11+
public class TextRequest {
12+
13+
@JsonProperty("text")
14+
private String text;
15+
16+
@JsonProperty("fix_spelling")
17+
private boolean fixSpelling = true; // 자바 관례대로 camelCase를 쓰고 JSON 이름만 지정
18+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.example.demo.domain.nerfilter.service;
2+
3+
import com.example.demo.domain.nerfilter.dto.AiResponse;
4+
import com.example.demo.domain.nerfilter.dto.TextRequest;
5+
import org.springframework.http.HttpEntity;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.web.client.RestClient;
9+
10+
import org.springframework.http.MediaType;
11+
import org.springframework.web.client.RestTemplate;
12+
13+
@Service
14+
public class AiAnalysisService {
15+
16+
private final RestTemplate restTemplate;
17+
private final String baseUrl = "https://jyhdevstore.store";
18+
19+
public AiAnalysisService() {
20+
this.restTemplate = new RestTemplate();
21+
}
22+
23+
public AiResponse getAnalysis(TextRequest text) {
24+
HttpHeaders headers = new HttpHeaders();
25+
headers.setContentType(MediaType.APPLICATION_JSON);
26+
27+
HttpEntity<TextRequest> request = new HttpEntity<>(text, headers);
28+
29+
return restTemplate.postForObject(
30+
baseUrl + "/filter",
31+
request,
32+
AiResponse.class
33+
);
34+
}
35+
}

0 commit comments

Comments
 (0)