Skip to content

Commit b0bb4cb

Browse files
authored
[BACKEND] feat: PC 호환성 체크 API 및 실시간 SSE 스트리밍 구현 (#108)
- 비동기 스레드 풀 구현에 잘못된 점이 있어 리팩토링 예정
1 parent f51b95c commit b0bb4cb

File tree

14 files changed

+1527
-1
lines changed

14 files changed

+1527
-1
lines changed

backend/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747

4848
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
4949

50+
implementation 'com.google.genai:google-genai:1.8.0'
5051
implementation 'software.amazon.awssdk:s3:2.25.27'
5152
}
5253

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.cmg.comtogether.common.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.scheduling.annotation.EnableAsync;
6+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
7+
8+
import java.util.concurrent.Executor;
9+
10+
@Configuration
11+
@EnableAsync
12+
public class AsyncConfig {
13+
14+
@Bean(name = "compatibilityCheckExecutor")
15+
public Executor compatibilityCheckExecutor() {
16+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17+
executor.setCorePoolSize(10);
18+
executor.setMaxPoolSize(20);
19+
executor.setQueueCapacity(100);
20+
executor.setThreadNamePrefix("compatibility-check-");
21+
executor.initialize();
22+
return executor;
23+
}
24+
}
25+

backend/src/main/java/com/cmg/comtogether/common/exception/ErrorCode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public enum ErrorCode {
2929

3030
// 네이버 상품 API
3131
NAVER_API_ERROR(502, "NAVER-999", "네이버 서버와 통신 중 오류가 발생했습니다."),
32+
33+
// Gemini API
34+
GEMINI_API_ERROR(502, "GEMINI-999", "Gemini API와 통신 중 오류가 발생했습니다."),
35+
3236

3337
// 가이드
3438
GUIDE_NOT_FOUND(404, "GUIDE-001", "가이드를 찾을 수 없습니다"),

backend/src/main/java/com/cmg/comtogether/common/security/config/SecurityConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurat
3939
"/refresh/**",
4040
"/swagger-ui/**",
4141
"/v3/api-docs/**",
42-
"/users/login"
42+
"/users/login",
43+
"/compatibility/**",
44+
"/gemini/**"
4345
).permitAll()
4446
.requestMatchers(
4547
"/users/all",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.cmg.comtogether.compatibility.controller;
2+
3+
import com.cmg.comtogether.compatibility.dto.CompatibilityCheckRequestDto;
4+
import com.cmg.comtogether.compatibility.service.CompatibilityCheckService;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import jakarta.validation.Valid;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.http.MediaType;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
16+
17+
import java.io.IOException;
18+
import java.util.concurrent.CompletableFuture;
19+
20+
@Slf4j
21+
@RestController
22+
@RequestMapping("/compatibility")
23+
@RequiredArgsConstructor
24+
public class CompatibilityController {
25+
26+
private final CompatibilityCheckService compatibilityCheckService;
27+
private final ObjectMapper objectMapper;
28+
29+
/**
30+
* 호환성 체크 (SSE 스트리밍)
31+
* 각 검사 항목이 완료되는 대로 실시간으로 결과를 전송
32+
*/
33+
@PostMapping(value = "/check", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
34+
public ResponseEntity<SseEmitter> checkCompatibility(
35+
@Valid @RequestBody CompatibilityCheckRequestDto requestDto
36+
) {
37+
SseEmitter emitter = new SseEmitter(300000L); // 5분 타임아웃
38+
39+
// 초기 연결 확인
40+
try {
41+
emitter.send(SseEmitter.event()
42+
.name("connected")
43+
.data("호환성 체크를 시작합니다."));
44+
} catch (IOException e) {
45+
log.error("SSE 초기 연결 실패", e);
46+
emitter.completeWithError(e);
47+
return ResponseEntity.ok(emitter);
48+
}
49+
50+
// 비동기로 호환성 체크 실행
51+
CompletableFuture.runAsync(() -> {
52+
try {
53+
compatibilityCheckService.checkCompatibility(
54+
requestDto.getItems(),
55+
result -> {
56+
try {
57+
// JSON을 포맷팅하여 가독성 향상
58+
String formattedJson = objectMapper.writerWithDefaultPrettyPrinter()
59+
.writeValueAsString(result);
60+
61+
// 각 검사 항목 완료 시 SSE로 전송 (포맷팅된 JSON 사용)
62+
emitter.send(SseEmitter.event()
63+
.name("result")
64+
.data(formattedJson));
65+
} catch (IOException e) {
66+
log.error("SSE 전송 실패", e);
67+
emitter.completeWithError(e);
68+
}
69+
}
70+
).join();
71+
72+
// 모든 검사 완료
73+
emitter.send(SseEmitter.event()
74+
.name("completed")
75+
.data("모든 호환성 검사가 완료되었습니다."));
76+
emitter.complete();
77+
78+
} catch (Exception e) {
79+
log.error("호환성 체크 중 오류 발생", e);
80+
try {
81+
emitter.send(SseEmitter.event()
82+
.name("error")
83+
.data("호환성 체크 중 오류가 발생했습니다: " + e.getMessage()));
84+
} catch (IOException ioException) {
85+
log.error("에러 메시지 전송 실패", ioException);
86+
}
87+
emitter.completeWithError(e);
88+
}
89+
});
90+
91+
// 연결 종료 시 처리
92+
emitter.onCompletion(() -> log.debug("SSE 연결 완료"));
93+
emitter.onTimeout(() -> {
94+
log.warn("SSE 연결 타임아웃");
95+
emitter.complete();
96+
});
97+
emitter.onError((ex) -> {
98+
log.error("SSE 연결 오류", ex);
99+
emitter.completeWithError(ex);
100+
});
101+
102+
return ResponseEntity.ok(emitter);
103+
}
104+
}
105+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.cmg.comtogether.compatibility.dto;
2+
3+
import jakarta.validation.constraints.NotEmpty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class CompatibilityCheckRequestDto {
14+
15+
@NotEmpty(message = "견적 아이템 목록은 필수입니다.")
16+
private List<CompatibilityItemDto> items;
17+
}
18+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.cmg.comtogether.compatibility.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.util.List;
10+
11+
@Getter
12+
@Builder
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class CompatibilityCheckResultDto {
16+
17+
@JsonProperty("check_id")
18+
private Integer checkId; // 1~10
19+
20+
@JsonProperty("check_name")
21+
private String checkName; // 예: "CPU ↔ 메인보드 호환성"
22+
23+
private String result; // "POSITIVE", "NEGATIVE", "UNKNOWN"
24+
25+
private List<String> errors;
26+
27+
private List<String> warnings;
28+
29+
private String details;
30+
31+
@JsonProperty("status")
32+
private String status; // "PENDING", "COMPLETED", "ERROR"
33+
}
34+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.cmg.comtogether.compatibility.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class CompatibilityItemDto {
15+
16+
@NotBlank(message = "제품명은 필수입니다.")
17+
private String title;
18+
19+
@JsonProperty("category3")
20+
private String category3; // 카테고리 (예: CPU, 메인보드, RAM 등)
21+
}
22+

0 commit comments

Comments
 (0)