From 4458029f6d4e45ebc82a86270f988b3ebc6ce43e Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Tue, 21 Jan 2025 14:53:14 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20GPT=20API=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/controller/SpaceController.java | 24 +++ .../project/domain/space/entity/Space.java | 50 +++++ .../domain/space/service/SpaceService.java | 11 ++ .../space/service/SpaceServiceImpl.java | 185 ++++++++++++++++++ .../project/global/config/GptConfig.java | 38 ++++ src/main/resources/application.properties | 35 ++++ 6 files changed, 343 insertions(+) create mode 100644 src/main/java/com/edison/project/domain/space/controller/SpaceController.java create mode 100644 src/main/java/com/edison/project/domain/space/entity/Space.java create mode 100644 src/main/java/com/edison/project/domain/space/service/SpaceService.java create mode 100644 src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java create mode 100644 src/main/java/com/edison/project/global/config/GptConfig.java create mode 100644 src/main/resources/application.properties diff --git a/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/src/main/java/com/edison/project/domain/space/controller/SpaceController.java new file mode 100644 index 0000000..00ba1ef --- /dev/null +++ b/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -0,0 +1,24 @@ +package com.edison.project.domain.space.controller; + +import com.edison.project.domain.space.entity.Space; +import com.edison.project.domain.space.service.SpaceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/spaces") +public class SpaceController { + + @Autowired + private SpaceService spaceService; + + // [POST] Space OPENAI API 이용하여 처리 + @PostMapping("/process") + public ResponseEntity> processSpaces() { + List processedSpaces = spaceService.processSpaces(); + return ResponseEntity.ok(processedSpaces); + } +} diff --git a/src/main/java/com/edison/project/domain/space/entity/Space.java b/src/main/java/com/edison/project/domain/space/entity/Space.java new file mode 100644 index 0000000..20a34aa --- /dev/null +++ b/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -0,0 +1,50 @@ +package com.edison.project.domain.space.entity; + +import java.util.List; + +public class Space { + private String content; // Space 내용 + private double x; // x 좌표 + private double y; // y 좌표 + private List groups; // 속한 그룹 + + public Space(String content, double x, double y, List groups) { + this.content = content; + this.x = x; + this.y = y; + this.groups = groups; + } + + // Getter와 Setter + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } +} diff --git a/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/src/main/java/com/edison/project/domain/space/service/SpaceService.java new file mode 100644 index 0000000..ab357b3 --- /dev/null +++ b/src/main/java/com/edison/project/domain/space/service/SpaceService.java @@ -0,0 +1,11 @@ +package com.edison.project.domain.space.service; + +import com.edison.project.domain.bubble.entity.Bubble; +import com.edison.project.domain.space.entity.Space; + +import java.util.List; + +public interface SpaceService { + List processSpaces(); + +} diff --git a/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java new file mode 100644 index 0000000..62dbe82 --- /dev/null +++ b/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -0,0 +1,185 @@ +package com.edison.project.domain.space.service; + +import com.edison.project.domain.bubble.entity.Bubble; +import com.edison.project.domain.bubble.repository.BubbleRepository; +import com.edison.project.domain.space.entity.Space; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.*; + +@Service +public class SpaceServiceImpl implements SpaceService { + private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final BubbleRepository bubbleRepository; + public SpaceServiceImpl(BubbleRepository bubbleRepository) { + this.bubbleRepository = bubbleRepository; + } + + @Override + public List processSpaces() { + // 1. Bubble 테이블 전체 데이터 조회 + List bubbles = bubbleRepository.findAll(); + + // 2. Bubble 데이터를 Space 객체로 변환 + List spaces = createSpacesFromBubbles(bubbles); + + // 3. GPT 호출 및 좌표/그룹 계산 + String gptResponse = callGPTForGrouping(spaces.stream().map(Space::getContent).toList()); + return parseGptResponse(gptResponse, spaces); + } + + // Bubble 데이터를 Space로 변환 + private List createSpacesFromBubbles(List bubbles) { + List spaces = new ArrayList<>(); + + for (Bubble bubble : bubbles) { + // Bubble과 연결된 Label 정보 가져오기 + List labelNames = bubble.getLabels().stream() + .map(bubbleLabel -> bubbleLabel.getLabel().getName()) + .toList(); + + // Label 이름을 쉼표로 결합 + String labels = String.join(", ", labelNames); + + // Space content 생성 + String spaceContent = String.format( + "Title: %s\nContent: %s\nLabels: %s", + bubble.getTitle(), + bubble.getContent(), + labels.isEmpty() ? "None" : labels + ); + + spaces.add(new Space(spaceContent, 0, 0, new ArrayList<>())); + } + + return spaces; + } + + private OkHttpClient createHttpClientWithTimeout() { + return new OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 연결 타임아웃 + .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 쓰기 타임아웃 + .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // 읽기 타임아웃 + .build(); + } + + + // GPT API 호출 + private String callGPTForGrouping(List contents) { + String openaiApiKey = System.getenv("openai_key"); + if (openaiApiKey == null || openaiApiKey.isEmpty()) { + throw new RuntimeException("OpenAI API 키가 환경변수에 설정되어 있지 않습니다."); + } + + if (contents == null || contents.isEmpty()) { + throw new IllegalArgumentException("요청 데이터가 비어 있습니다."); + } + + // JSON 요청 본문 생성 + ObjectMapper objectMapper = new ObjectMapper(); + Map message = Map.of("role", "system", "content", buildPrompt(contents)); + Map requestBody = Map.of("model", "gpt-3.5-turbo", "messages", List.of(message)); + String jsonBody; + + try { + jsonBody = objectMapper.writeValueAsString(requestBody); + } catch (Exception e) { + throw new RuntimeException("JSON 생성 중 오류 발생: " + e.getMessage(), e); + } + + // API 요청 + + OkHttpClient client = createHttpClientWithTimeout(); + RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); + Request request = new Request.Builder() + .url(OPENAI_API_URL) + .post(body) + .addHeader("Authorization", "Bearer " + openaiApiKey) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + String responseBody = response.body() != null ? response.body().string() : "No response body"; + System.out.println("Response Code: " + response.code()); + System.out.println("Response Body: " + responseBody); + throw new RuntimeException("OpenAI API 호출 실패: " + response.code() + " - " + responseBody); + } + + return response.body().string(); + } catch (IOException e) { + throw new RuntimeException("OpenAI API 호출 중 오류 발생: " + e.getMessage(), e); + } + } + + + // GPT 요청 프롬프트 생성 + private String buildPrompt(List contents) { + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append("Given the following spaces, group them into categories and assign (x, y) coordinates. "); + promptBuilder.append("If a space belongs to multiple groups, place it at the average of the clusters. "); + promptBuilder.append("Return the result in JSON format:\n"); + + for (String content : contents) { + promptBuilder.append("- ").append(content).append("\n"); + } + + return promptBuilder.toString(); + } + + + // GPT 응답 파싱 및 Space 객체 생성 + private List parseGptResponse(String gptResponse, List spaces) { + System.out.println("GPT Response: " + gptResponse); + try { + // 1. OpenAI 응답 JSON 파싱 + Map responseMap = objectMapper.readValue(gptResponse, Map.class); + List> choices = (List>) responseMap.get("choices"); + + if (choices == null || choices.isEmpty()) { + throw new RuntimeException("OpenAI API 응답에서 'choices'가 비어 있습니다."); + } + + // 2. 첫 번째 choice의 message에서 content 가져오기 + Map message = (Map) choices.get(0).get("message"); + if (message == null || !message.containsKey("content")) { + throw new RuntimeException("OpenAI API 응답에서 'message'가 비어 있거나 'content'가 없습니다."); + } + + String content = (String) message.get("content"); + + // 3. content를 다시 JSON으로 파싱 + List> groups = objectMapper.readValue(content, List.class); + + // 4. Space 객체 업데이트 + for (Map group : groups) { + String groupTitle = (String) group.get("group_title"); + List> groupSpaces = (List>) group.get("spaces"); + + for (Map groupSpace : groupSpaces) { + String spaceContent = (String) groupSpace.get("content"); + double x = (double) groupSpace.get("x"); + double y = (double) groupSpace.get("y"); + + spaces.stream() + .filter(space -> space.getContent().equals(spaceContent)) + .findFirst() + .ifPresent(space -> { + space.setX(x); + space.setY(y); + space.setGroups(Collections.singletonList(groupTitle)); + }); + } + } + } catch (Exception e) { + throw new RuntimeException("GPT 응답 파싱 중 오류 발생: " + e.getMessage(), e); + } + return spaces; + } + +} diff --git a/src/main/java/com/edison/project/global/config/GptConfig.java b/src/main/java/com/edison/project/global/config/GptConfig.java new file mode 100644 index 0000000..0d4e1d3 --- /dev/null +++ b/src/main/java/com/edison/project/global/config/GptConfig.java @@ -0,0 +1,38 @@ +package com.edison.project.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class GptConfig { + + @Value("${openai_key}") + private String secretKey; + + private String model = "gpt-3.5-turbo"; + + public String getSecretKey() { + return secretKey; + } + + public String getModel() { + return model; + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public HttpHeaders httpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(secretKey); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..395e2db --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,35 @@ +spring.application.name=project + +# MySQL Database Connection +spring.datasource.url=jdbc:mysql://localhost:3306/edison_db?useSSL=false&serverTimezone=Asia/Seoul +spring.datasource.username=root +spring.datasource.password=${DB_password} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# Hibernate Configuration +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.format_sql=true + + +# JPA Console Log +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql=TRACE + +# Google OAuth2 +spring.security.oauth2.client.registration.google.client-id=${Client_ID} +spring.security.oauth2.client.registration.google.client-secret=${Client_Secret} +spring.security.oauth2.client.registration.google.scope=openid,email +spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google +spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com + +# JWT +jwt.secret=${jwt_secret} +jwt.access-token-expiration=${jwt_access_expiration:3600000} +jwt.refresh-token-expiration=${jwt_refresh_expiration:2592000000} + +# OPENAI +openai.model=gpt-3.5-turbo +openai.api.key=${openai_key} +openai.api.url= https://api.openai.com/v1/chat/completions \ No newline at end of file From 447ae3365b430423e0354b72ec0a1f8121eb6313 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Tue, 21 Jan 2025 18:34:05 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20openai=20api=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project/build.gradle | 2 + .../space/controller/SpaceController.java | 0 .../project/domain/space/entity/Space.java | 0 .../domain/space/service/SpaceService.java | 0 .../space/service/SpaceServiceImpl.java | 128 +++++++++++++----- .../project/global/config/GptConfig.java | 0 .../src/main/resources/application.properties | 2 +- src/main/resources/application.properties | 35 ----- 8 files changed, 97 insertions(+), 70 deletions(-) rename {src => project/src}/main/java/com/edison/project/domain/space/controller/SpaceController.java (100%) rename {src => project/src}/main/java/com/edison/project/domain/space/entity/Space.java (100%) rename {src => project/src}/main/java/com/edison/project/domain/space/service/SpaceService.java (100%) rename {src => project/src}/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java (55%) rename {src => project/src}/main/java/com/edison/project/global/config/GptConfig.java (100%) delete mode 100644 src/main/resources/application.properties diff --git a/project/build.gradle b/project/build.gradle index da34fbf..76ceac8 100644 --- a/project/build.gradle +++ b/project/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'jakarta.validation:jakarta.validation-api:3.0.2' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' // Lombok 설정 implementation 'org.projectlombok:lombok:1.18.30' diff --git a/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java similarity index 100% rename from src/main/java/com/edison/project/domain/space/controller/SpaceController.java rename to project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java diff --git a/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java similarity index 100% rename from src/main/java/com/edison/project/domain/space/entity/Space.java rename to project/src/main/java/com/edison/project/domain/space/entity/Space.java diff --git a/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java similarity index 100% rename from src/main/java/com/edison/project/domain/space/service/SpaceService.java rename to project/src/main/java/com/edison/project/domain/space/service/SpaceService.java diff --git a/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java similarity index 55% rename from src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java rename to project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 62dbe82..d3cdc33 100644 --- a/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.type.TypeReference; import java.io.IOException; import java.util.*; @@ -16,6 +17,7 @@ public class SpaceServiceImpl implements SpaceService { private final ObjectMapper objectMapper = new ObjectMapper(); private final BubbleRepository bubbleRepository; + public SpaceServiceImpl(BubbleRepository bubbleRepository) { this.bubbleRepository = bubbleRepository; } @@ -121,10 +123,31 @@ private String callGPTForGrouping(List contents) { // GPT 요청 프롬프트 생성 private String buildPrompt(List contents) { StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Given the following spaces, group them into categories and assign (x, y) coordinates. "); - promptBuilder.append("If a space belongs to multiple groups, place it at the average of the clusters. "); - promptBuilder.append("Return the result in JSON format:\n"); + // 프롬프트 설명 + promptBuilder.append("You are tasked with categorizing and positioning content items on a 2D grid. "); + promptBuilder.append("Each item should be assigned a unique `(x, y)` coordinate based on its category and relationships. "); + promptBuilder.append("Return only valid JSON output, with no additional text, comments, or explanations.\n\n"); + + // 규칙 추가 + promptBuilder.append("### Rules:\n"); + promptBuilder.append("1. Group similar items and assign `(x, y)` coordinates.\n"); + promptBuilder.append("2. Items in the same group should have closer coordinates.\n"); + promptBuilder.append("3. Items in different groups should be spaced farther apart.\n"); + promptBuilder.append("4. Ensure coordinates are unique and avoid clustering all items at `(0, 0)`.\n"); + promptBuilder.append("5. Return only a JSON array with objects in the following format:\n\n"); + + // 출력 형식 명시 + promptBuilder.append("```json\n"); + promptBuilder.append("[\n"); + promptBuilder.append(" {\n"); + promptBuilder.append("content: "); + promptBuilder.append("x: "); + promptBuilder.append("y: "); + promptBuilder.append("groups: "); + + // 콘텐츠 추가 + promptBuilder.append("### Input Content:\n"); for (String content : contents) { promptBuilder.append("- ").append(content).append("\n"); } @@ -133,53 +156,90 @@ private String buildPrompt(List contents) { } - // GPT 응답 파싱 및 Space 객체 생성 - private List parseGptResponse(String gptResponse, List spaces) { - System.out.println("GPT Response: " + gptResponse); + private String sanitizeResponse(String response) { try { - // 1. OpenAI 응답 JSON 파싱 - Map responseMap = objectMapper.readValue(gptResponse, Map.class); - List> choices = (List>) responseMap.get("choices"); + // 응답 문자열을 JSON 객체로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); + Map responseMap = objectMapper.readValue(response, new TypeReference>() {}); + // "choices" -> 첫 번째 "message" -> "content" 필드 추출 + List> choices = (List>) responseMap.get("choices"); if (choices == null || choices.isEmpty()) { - throw new RuntimeException("OpenAI API 응답에서 'choices'가 비어 있습니다."); + throw new RuntimeException("'choices' 필드가 비어있습니다."); } - // 2. 첫 번째 choice의 message에서 content 가져오기 Map message = (Map) choices.get(0).get("message"); if (message == null || !message.containsKey("content")) { - throw new RuntimeException("OpenAI API 응답에서 'message'가 비어 있거나 'content'가 없습니다."); + throw new RuntimeException("'message' 필드에 'content'가 없습니다."); } String content = (String) message.get("content"); - // 3. content를 다시 JSON으로 파싱 - List> groups = objectMapper.readValue(content, List.class); - - // 4. Space 객체 업데이트 - for (Map group : groups) { - String groupTitle = (String) group.get("group_title"); - List> groupSpaces = (List>) group.get("spaces"); - - for (Map groupSpace : groupSpaces) { - String spaceContent = (String) groupSpace.get("content"); - double x = (double) groupSpace.get("x"); - double y = (double) groupSpace.get("y"); - - spaces.stream() - .filter(space -> space.getContent().equals(spaceContent)) - .findFirst() - .ifPresent(space -> { - space.setX(x); - space.setY(y); - space.setGroups(Collections.singletonList(groupTitle)); - }); + // 백틱(```) 제거 및 JSON 배열로 파싱 가능하도록 정리 + if (content.startsWith("```json")) { + content = content.replace("```json", "").replace("```", "").trim(); + } + + // content가 유효한 JSON 배열인지 확인 + objectMapper.readTree(content); // JSON 파싱 시도 (유효성 확인용) + return content; + + } catch (Exception e) { + throw new RuntimeException("응답 정리 중 오류 발생: " + e.getMessage(), e); + } + } + + + + private List parseGptResponse(String gptResponse, List spaces) { + try { + // 1. 응답 정리 및 유효성 확인 + String sanitizedResponse = sanitizeResponse(gptResponse); + System.out.println("Sanitized GPT Response: " + sanitizedResponse); + + // 2. JSON 배열로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); + List> parsedData = objectMapper.readValue(sanitizedResponse, new TypeReference>>() {}); + + // 3. 각 항목 매핑 + for (Map item : parsedData) { + // 필수 필드 검증 + if (!item.containsKey("content") || !item.containsKey("x") || !item.containsKey("y") || !item.containsKey("groups")) { + System.out.println("Invalid item detected and skipped: " + item); + continue; } + + // 값 추출 + String content = (String) item.get("content"); + double x = ((Number) item.get("x")).doubleValue(); + double y = ((Number) item.get("y")).doubleValue(); + List groups = (List) item.get("groups"); + + // Space 리스트에 값 매핑 + spaces.stream() + .filter(space -> space.getContent().equals(content)) + .findFirst() + .ifPresent(space -> { + space.setX(x); + space.setY(y); + space.setGroups(groups.stream().map(String::valueOf).toList()); // Integer를 String으로 변환 + }); + } + + // 4. 매핑 결과 확인 로그 + System.out.println("=== 매핑 결과 ==="); + for (Space space : spaces) { + System.out.println("Content: " + space.getContent()); + System.out.println("x: " + space.getX()); + System.out.println("y: " + space.getY()); + System.out.println("Groups: " + space.getGroups()); + System.out.println("----------------"); } } catch (Exception e) { + System.out.println("GPT Response: " + gptResponse); throw new RuntimeException("GPT 응답 파싱 중 오류 발생: " + e.getMessage(), e); } return spaces; } -} +} \ No newline at end of file diff --git a/src/main/java/com/edison/project/global/config/GptConfig.java b/project/src/main/java/com/edison/project/global/config/GptConfig.java similarity index 100% rename from src/main/java/com/edison/project/global/config/GptConfig.java rename to project/src/main/java/com/edison/project/global/config/GptConfig.java diff --git a/project/src/main/resources/application.properties b/project/src/main/resources/application.properties index ae49aa8..c500929 100644 --- a/project/src/main/resources/application.properties +++ b/project/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=project # MySQL Database Connection -spring.datasource.url=jdbc:mysql://localhost:3306/edison?useSSL=false&serverTimezone=Asia/Seoul +spring.datasource.url=jdbc:mysql://localhost:3306/edison_db?useSSL=false&serverTimezone=Asia/Seoul spring.datasource.username=root spring.datasource.password=${DB_password} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 395e2db..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,35 +0,0 @@ -spring.application.name=project - -# MySQL Database Connection -spring.datasource.url=jdbc:mysql://localhost:3306/edison_db?useSSL=false&serverTimezone=Asia/Seoul -spring.datasource.username=root -spring.datasource.password=${DB_password} -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - -# Hibernate Configuration -spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect -spring.jpa.properties.hibernate.format_sql=true - - -# JPA Console Log -logging.level.org.hibernate.SQL=DEBUG -logging.level.org.hibernate.type.descriptor.sql=TRACE - -# Google OAuth2 -spring.security.oauth2.client.registration.google.client-id=${Client_ID} -spring.security.oauth2.client.registration.google.client-secret=${Client_Secret} -spring.security.oauth2.client.registration.google.scope=openid,email -spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google -spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com - -# JWT -jwt.secret=${jwt_secret} -jwt.access-token-expiration=${jwt_access_expiration:3600000} -jwt.refresh-token-expiration=${jwt_refresh_expiration:2592000000} - -# OPENAI -openai.model=gpt-3.5-turbo -openai.api.key=${openai_key} -openai.api.url= https://api.openai.com/v1/chat/completions \ No newline at end of file From 487275efc46218ad97f589395a9c519cf72f10ed Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Thu, 23 Jan 2025 01:59:07 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/domain/space/entity/Space.java | 13 +- .../space/service/SpaceServiceImpl.java | 160 ++++++++++-------- 2 files changed, 105 insertions(+), 68 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index 20a34aa..2628a33 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -1,21 +1,30 @@ package com.edison.project.domain.space.entity; - import java.util.List; public class Space { + private Long id; // Bubble의 ID private String content; // Space 내용 private double x; // x 좌표 private double y; // y 좌표 private List groups; // 속한 그룹 - public Space(String content, double x, double y, List groups) { + public Space(String content, double x, double y, List groups, Long id) { this.content = content; this.x = x; this.y = y; this.groups = groups; + this.id = id; } // Getter와 Setter + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public String getContent() { return content; } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index d3cdc33..53f5d05 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -56,7 +56,7 @@ private List createSpacesFromBubbles(List bubbles) { labels.isEmpty() ? "None" : labels ); - spaces.add(new Space(spaceContent, 0, 0, new ArrayList<>())); + spaces.add(new Space(spaceContent, 0., 0., new ArrayList<>(), bubble.getBubbleId())); } return spaces; @@ -121,48 +121,21 @@ private String callGPTForGrouping(List contents) { // GPT 요청 프롬프트 생성 - private String buildPrompt(List contents) { - StringBuilder promptBuilder = new StringBuilder(); - - // 프롬프트 설명 - promptBuilder.append("You are tasked with categorizing and positioning content items on a 2D grid. "); - promptBuilder.append("Each item should be assigned a unique `(x, y)` coordinate based on its category and relationships. "); - promptBuilder.append("Return only valid JSON output, with no additional text, comments, or explanations.\n\n"); - - // 규칙 추가 - promptBuilder.append("### Rules:\n"); - promptBuilder.append("1. Group similar items and assign `(x, y)` coordinates.\n"); - promptBuilder.append("2. Items in the same group should have closer coordinates.\n"); - promptBuilder.append("3. Items in different groups should be spaced farther apart.\n"); - promptBuilder.append("4. Ensure coordinates are unique and avoid clustering all items at `(0, 0)`.\n"); - promptBuilder.append("5. Return only a JSON array with objects in the following format:\n\n"); - - // 출력 형식 명시 - promptBuilder.append("```json\n"); - promptBuilder.append("[\n"); - promptBuilder.append(" {\n"); - promptBuilder.append("content: "); - promptBuilder.append("x: "); - promptBuilder.append("y: "); - promptBuilder.append("groups: "); - - // 콘텐츠 추가 - promptBuilder.append("### Input Content:\n"); - for (String content : contents) { - promptBuilder.append("- ").append(content).append("\n"); - } - - return promptBuilder.toString(); - } - - private String sanitizeResponse(String response) { try { - // 응답 문자열을 JSON 객체로 파싱 + if (response == null || response.isBlank()) { + throw new RuntimeException("GPT 응답이 비어 있습니다."); + } + ObjectMapper objectMapper = new ObjectMapper(); - Map responseMap = objectMapper.readValue(response, new TypeReference>() {}); + Map responseMap; + + try { + responseMap = objectMapper.readValue(response, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("GPT 응답이 JSON 형식이 아닙니다: " + response, e); + } - // "choices" -> 첫 번째 "message" -> "content" 필드 추출 List> choices = (List>) responseMap.get("choices"); if (choices == null || choices.isEmpty()) { throw new RuntimeException("'choices' 필드가 비어있습니다."); @@ -174,72 +147,127 @@ private String sanitizeResponse(String response) { } String content = (String) message.get("content"); - - // 백틱(```) 제거 및 JSON 배열로 파싱 가능하도록 정리 - if (content.startsWith("```json")) { - content = content.replace("```json", "").replace("```", "").trim(); + if (content == null || content.isBlank()) { + throw new RuntimeException("'content' 값이 비어 있습니다."); } - // content가 유효한 JSON 배열인지 확인 - objectMapper.readTree(content); // JSON 파싱 시도 (유효성 확인용) + content = content.replaceAll("```json", "").replaceAll("```", "").trim(); + objectMapper.readTree(content); + return content; } catch (Exception e) { + System.err.println("GPT 응답 처리 중 오류 발생: " + e.getMessage()); throw new RuntimeException("응답 정리 중 오류 발생: " + e.getMessage(), e); } } - - private List parseGptResponse(String gptResponse, List spaces) { try { - // 1. 응답 정리 및 유효성 확인 String sanitizedResponse = sanitizeResponse(gptResponse); System.out.println("Sanitized GPT Response: " + sanitizedResponse); - // 2. JSON 배열로 파싱 ObjectMapper objectMapper = new ObjectMapper(); List> parsedData = objectMapper.readValue(sanitizedResponse, new TypeReference>>() {}); - // 3. 각 항목 매핑 + int index = 0; for (Map item : parsedData) { - // 필수 필드 검증 - if (!item.containsKey("content") || !item.containsKey("x") || !item.containsKey("y") || !item.containsKey("groups")) { + if (!item.containsKey("content") || !item.containsKey("x") || !item.containsKey("y") || !item.containsKey("groups") || !item.containsKey("id")) { System.out.println("Invalid item detected and skipped: " + item); continue; } - // 값 추출 String content = (String) item.get("content"); + if (content == null || content.isBlank()) { + System.out.println("Skipping item with blank content: " + item); + continue; + } + double x = ((Number) item.get("x")).doubleValue(); double y = ((Number) item.get("y")).doubleValue(); - List groups = (List) item.get("groups"); - - // Space 리스트에 값 매핑 - spaces.stream() - .filter(space -> space.getContent().equals(content)) - .findFirst() - .ifPresent(space -> { - space.setX(x); - space.setY(y); - space.setGroups(groups.stream().map(String::valueOf).toList()); // Integer를 String으로 변환 - }); + Long id = ((Number) item.get("id")).longValue(); + + List rawGroups = (List) item.get("groups"); + List groups = new ArrayList<>(); + for (Object group : rawGroups) { + if (group instanceof Number) { + groups.add(((Number) group).intValue()); + } else if (group instanceof String) { + try { + groups.add(Integer.parseInt((String) group)); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid group format: " + group, e); + } + } else { + throw new RuntimeException("Invalid group format: " + group); + } + } + + if (index < spaces.size()) { + Space space = spaces.get(index); + space.setContent(content); + space.setX(x); + space.setY(y); + space.setId(id); + space.setGroups(groups.stream().map(String::valueOf).toList()); + System.out.println("Mapped Space: " + space.getContent()); + index++; + } else { + System.out.println("No more Space entities to map for item: " + item); + } } - // 4. 매핑 결과 확인 로그 System.out.println("=== 매핑 결과 ==="); for (Space space : spaces) { + System.out.println("ID: " + space.getId()); System.out.println("Content: " + space.getContent()); System.out.println("x: " + space.getX()); System.out.println("y: " + space.getY()); System.out.println("Groups: " + space.getGroups()); System.out.println("----------------"); } + } catch (Exception e) { - System.out.println("GPT Response: " + gptResponse); + System.err.println("GPT Response Parsing Error: " + e.getMessage()); throw new RuntimeException("GPT 응답 파싱 중 오류 발생: " + e.getMessage(), e); } return spaces; } + private String buildPrompt(List contents) { + StringBuilder promptBuilder = new StringBuilder(); + + promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); + promptBuilder.append("Each item should have the following attributes:\n"); + promptBuilder.append("- `id`: A unique identifier for the item (integer).\n"); + promptBuilder.append("- `content`: A string representing the item's content.\n"); + promptBuilder.append("- `x`: A unique floating-point number for the x-coordinate.\n"); + promptBuilder.append("- `y`: A unique floating-point number for the y-coordinate.\n"); + promptBuilder.append("- `groups`: A list of integers representing the item's group IDs.\n\n"); + promptBuilder.append("### Rules:\n"); + promptBuilder.append("1. Each item must have a unique `(x, y)` coordinate.\n"); + promptBuilder.append("2. Items with similar topics should have closer `(x, y)` coordinates.\n"); + promptBuilder.append("3. Items with different topics should have larger distances between their coordinates.\n"); + promptBuilder.append("4. Coordinates should include decimal values to express fine-grained similarity.\n"); + promptBuilder.append("5. Ensure `groups` contains only integers, and avoid any other data types.\n"); + promptBuilder.append("6. Return only valid JSON output in the following format:\n\n"); + promptBuilder.append("[\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"id\": ,\n"); + promptBuilder.append(" \"content\": \"\",\n"); + promptBuilder.append(" \"x\": ,\n"); + promptBuilder.append(" \"y\": ,\n"); + promptBuilder.append(" \"groups\": []\n"); + promptBuilder.append(" }\n"); + promptBuilder.append("]\n\n"); + + promptBuilder.append("### Input Content:\n"); + for (String content : contents) { + promptBuilder.append("- ").append(content).append("\n"); + } + + return promptBuilder.toString(); + } + + } \ No newline at end of file From bb6b04a1a0c1ab5ab8733eeae959bb9cf366e417 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Tue, 28 Jan 2025 01:48:03 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20space=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/service/SpaceServiceImpl.java | 249 ++++++++++-------- .../src/main/resources/application.properties | 26 +- 2 files changed, 149 insertions(+), 126 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 53f5d05..bc05529 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -7,9 +7,13 @@ import okhttp3.*; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; +import okhttp3.OkHttpClient; +import java.util.concurrent.TimeUnit; + import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; @Service public class SpaceServiceImpl implements SpaceService { @@ -24,50 +28,52 @@ public SpaceServiceImpl(BubbleRepository bubbleRepository) { @Override public List processSpaces() { - // 1. Bubble 테이블 전체 데이터 조회 + // 1. Bubble 데이터를 가져옵니다. List bubbles = bubbleRepository.findAll(); - // 2. Bubble 데이터를 Space 객체로 변환 - List spaces = createSpacesFromBubbles(bubbles); + // 2. Bubble 데이터를 content로 변환 + List> requestData = createRequestData(bubbles); + + // 3. requestData에서 content만 추출 + List contents = requestData.stream() + .map(data -> (String) data.get("content")) // "content" 필드만 추출 + .collect(Collectors.toList()); - // 3. GPT 호출 및 좌표/그룹 계산 - String gptResponse = callGPTForGrouping(spaces.stream().map(Space::getContent).toList()); - return parseGptResponse(gptResponse, spaces); + // 4. GPT 호출 + String gptResponse = callGPTForGrouping(contents); + + // 5. GPT 응답 파싱 및 매핑 + return parseGptResponse(gptResponse, bubbles); } - // Bubble 데이터를 Space로 변환 - private List createSpacesFromBubbles(List bubbles) { - List spaces = new ArrayList<>(); - for (Bubble bubble : bubbles) { - // Bubble과 연결된 Label 정보 가져오기 - List labelNames = bubble.getLabels().stream() - .map(bubbleLabel -> bubbleLabel.getLabel().getName()) - .toList(); + // Bubble 데이터를 content로 변환 + private List> createRequestData(List bubbles) { + List> requestData = new ArrayList<>(); - // Label 이름을 쉼표로 결합 - String labels = String.join(", ", labelNames); + for (Bubble bubble : bubbles) { + // Bubble의 Labels 병합 + String labels = bubble.getLabels().stream() + .map(label -> label.getLabel().getName()) + .collect(Collectors.joining(", ")); - // Space content 생성 - String spaceContent = String.format( + // Bubble의 제목, 내용, 라벨을 병합 + String content = String.format( "Title: %s\nContent: %s\nLabels: %s", bubble.getTitle(), bubble.getContent(), labels.isEmpty() ? "None" : labels ); - spaces.add(new Space(spaceContent, 0., 0., new ArrayList<>(), bubble.getBubbleId())); - } + // 요청 데이터 생성 + Map data = new HashMap<>(); + data.put("id", bubble.getBubbleId()); // 실제 Bubble ID + data.put("content", content); - return spaces; - } + requestData.add(data); + } - private OkHttpClient createHttpClientWithTimeout() { - return new OkHttpClient.Builder() - .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 연결 타임아웃 - .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 쓰기 타임아웃 - .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // 읽기 타임아웃 - .build(); + return requestData; } @@ -95,7 +101,6 @@ private String callGPTForGrouping(List contents) { } // API 요청 - OkHttpClient client = createHttpClientWithTimeout(); RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); Request request = new Request.Builder() @@ -119,104 +124,68 @@ private String callGPTForGrouping(List contents) { } } - - // GPT 요청 프롬프트 생성 - private String sanitizeResponse(String response) { + // GPT 응답 파싱 및 Space 매핑 + private List parseGptResponse(String gptResponse, List bubbles) { try { - if (response == null || response.isBlank()) { - throw new RuntimeException("GPT 응답이 비어 있습니다."); - } + String sanitizedResponse = sanitizeResponse(gptResponse); + System.out.println("Sanitized GPT Response: " + sanitizedResponse); ObjectMapper objectMapper = new ObjectMapper(); - Map responseMap; - - try { - responseMap = objectMapper.readValue(response, new TypeReference>() {}); - } catch (Exception e) { - throw new RuntimeException("GPT 응답이 JSON 형식이 아닙니다: " + response, e); - } + List> parsedData = objectMapper.readValue( + sanitizedResponse, new TypeReference>>() {}); - List> choices = (List>) responseMap.get("choices"); - if (choices == null || choices.isEmpty()) { - throw new RuntimeException("'choices' 필드가 비어있습니다."); - } + // 그룹화 결과를 저장할 리스트 + List spaces = new ArrayList<>(); - Map message = (Map) choices.get(0).get("message"); - if (message == null || !message.containsKey("content")) { - throw new RuntimeException("'message' 필드에 'content'가 없습니다."); - } + // 그룹화 로직: 같은 그룹끼리 묶음 + Map>> groupedById = parsedData.stream() + .collect(Collectors.groupingBy(item -> ((Number) item.get("id")).longValue())); - String content = (String) message.get("content"); - if (content == null || content.isBlank()) { - throw new RuntimeException("'content' 값이 비어 있습니다."); - } + for (Map.Entry>> entry : groupedById.entrySet()) { + Long id = entry.getKey(); + List> groupItems = entry.getValue(); - content = content.replaceAll("```json", "").replaceAll("```", "").trim(); - objectMapper.readTree(content); + // ID에 맞는 Bubble 찾기 + Optional optionalBubble = bubbles.stream() + .filter(bubble -> bubble.getBubbleId().equals(id)) + .findFirst(); - return content; - - } catch (Exception e) { - System.err.println("GPT 응답 처리 중 오류 발생: " + e.getMessage()); - throw new RuntimeException("응답 정리 중 오류 발생: " + e.getMessage(), e); - } - } - - private List parseGptResponse(String gptResponse, List spaces) { - try { - String sanitizedResponse = sanitizeResponse(gptResponse); - System.out.println("Sanitized GPT Response: " + sanitizedResponse); - - ObjectMapper objectMapper = new ObjectMapper(); - List> parsedData = objectMapper.readValue(sanitizedResponse, new TypeReference>>() {}); - - int index = 0; - for (Map item : parsedData) { - if (!item.containsKey("content") || !item.containsKey("x") || !item.containsKey("y") || !item.containsKey("groups") || !item.containsKey("id")) { - System.out.println("Invalid item detected and skipped: " + item); - continue; + if (optionalBubble.isEmpty()) { + System.err.println("Warning: Bubble not found for ID: " + id); + continue; // 매칭되지 않는 ID는 스킵 } - String content = (String) item.get("content"); - if (content == null || content.isBlank()) { - System.out.println("Skipping item with blank content: " + item); - continue; - } + Bubble bubble = optionalBubble.get(); - double x = ((Number) item.get("x")).doubleValue(); - double y = ((Number) item.get("y")).doubleValue(); - Long id = ((Number) item.get("id")).longValue(); - - List rawGroups = (List) item.get("groups"); - List groups = new ArrayList<>(); - for (Object group : rawGroups) { - if (group instanceof Number) { - groups.add(((Number) group).intValue()); - } else if (group instanceof String) { - try { - groups.add(Integer.parseInt((String) group)); - } catch (NumberFormatException e) { - throw new RuntimeException("Invalid group format: " + group, e); - } - } else { - throw new RuntimeException("Invalid group format: " + group); + // 그룹 내용을 결합하여 하나의 Space로 생성 + StringBuilder contentBuilder = new StringBuilder(); + for (Map groupItem : groupItems) { + String content = (String) groupItem.get("content"); + if (content != null && !content.isBlank()) { + contentBuilder.append(content).append("\n"); } } - if (index < spaces.size()) { - Space space = spaces.get(index); - space.setContent(content); - space.setX(x); - space.setY(y); - space.setId(id); - space.setGroups(groups.stream().map(String::valueOf).toList()); - System.out.println("Mapped Space: " + space.getContent()); - index++; - } else { - System.out.println("No more Space entities to map for item: " + item); - } + // 좌표 및 그룹 설정 (첫 번째 항목 기준) + double x = ((Number) groupItems.get(0).get("x")).doubleValue(); + double y = ((Number) groupItems.get(0).get("y")).doubleValue(); + List rawGroups = (List) groupItems.get(0).get("groups"); + List groups = rawGroups.stream() + .map(group -> group instanceof Number ? ((Number) group).intValue() : Integer.parseInt(group.toString())) + .collect(Collectors.toList()); + + // Space 객체 생성 + Space space = new Space( + contentBuilder.toString().trim(), // 그룹화된 content + x, + y, + groups.stream().map(String::valueOf).toList(), + bubble.getBubbleId() + ); + spaces.add(space); } + // 디버그 출력 System.out.println("=== 매핑 결과 ==="); for (Space space : spaces) { System.out.println("ID: " + space.getId()); @@ -227,11 +196,12 @@ private List parseGptResponse(String gptResponse, List spaces) { System.out.println("----------------"); } + return spaces; + } catch (Exception e) { System.err.println("GPT Response Parsing Error: " + e.getMessage()); throw new RuntimeException("GPT 응답 파싱 중 오류 발생: " + e.getMessage(), e); } - return spaces; } private String buildPrompt(List contents) { @@ -260,6 +230,7 @@ private String buildPrompt(List contents) { promptBuilder.append(" \"groups\": []\n"); promptBuilder.append(" }\n"); promptBuilder.append("]\n\n"); + promptBuilder.append("Return only valid JSON output without any additional text or explanation.\n"); promptBuilder.append("### Input Content:\n"); for (String content : contents) { @@ -270,4 +241,56 @@ private String buildPrompt(List contents) { } -} \ No newline at end of file + + // GPT 요청 프롬프트 생성 + private String sanitizeResponse(String response) { + try { + if (response == null || response.isBlank()) { + throw new RuntimeException("GPT 응답이 비어 있습니다."); + } + + ObjectMapper objectMapper = new ObjectMapper(); + Map responseMap; + + try { + responseMap = objectMapper.readValue(response, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("GPT 응답이 JSON 형식이 아닙니다: " + response, e); + } + + List> choices = (List>) responseMap.get("choices"); + if (choices == null || choices.isEmpty()) { + throw new RuntimeException("'choices' 필드가 비어있습니다."); + } + + Map message = (Map) choices.get(0).get("message"); + if (message == null || !message.containsKey("content")) { + throw new RuntimeException("'message' 필드에 'content'가 없습니다."); + } + + String content = (String) message.get("content"); + if (content == null || content.isBlank()) { + throw new RuntimeException("'content' 값이 비어 있습니다."); + } + + content = content.replaceAll("```json", "").replaceAll("```", "").trim(); + objectMapper.readTree(content); + + return content; + + } catch (Exception e) { + System.err.println("GPT 응답 처리 중 오류 발생: " + e.getMessage()); + throw new RuntimeException("응답 정리 중 오류 발생: " + e.getMessage(), e); + } + } + + private OkHttpClient createHttpClientWithTimeout() { + return new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) // 연결 타임아웃 설정 + .writeTimeout(30, TimeUnit.SECONDS) // 쓰기 타임아웃 설정 + .readTimeout(60, TimeUnit.SECONDS) // 읽기 타임아웃 설정 + .build(); + } + +} + diff --git a/project/src/main/resources/application.properties b/project/src/main/resources/application.properties index c500929..ab7451c 100644 --- a/project/src/main/resources/application.properties +++ b/project/src/main/resources/application.properties @@ -1,33 +1,33 @@ spring.application.name=project # MySQL Database Connection -spring.datasource.url=jdbc:mysql://localhost:3306/edison_db?useSSL=false&serverTimezone=Asia/Seoul -spring.datasource.username=root -spring.datasource.password=${DB_password} -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=${RDS_URL} +spring.datasource.username=${RDS_USERNAME} +spring.datasource.password=${RDS_PASSWORD} + # Hibernate Configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.format_sql=true - +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect # JPA Console Log logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql=TRACE # Google OAuth2 -spring.security.oauth2.client.registration.google.client-id=${Client_ID} -spring.security.oauth2.client.registration.google.client-secret=${Client_Secret} +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.client.registration.google.scope=openid,email spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google spring.security.oauth2.client.provider.google.issuer-uri=https://accounts.google.com # JWT -jwt.secret=${jwt_secret} -jwt.access-token-expiration=${jwt_access_expiration:3600000} -jwt.refresh-token-expiration=${jwt_refresh_expiration:2592000000} - - +jwt.secret=${JWT_SECRET} +jwt.access-token-expiration=${JWT_ACCESS_EXPIRATION} +jwt.refresh-token-expiration=${JWT_REFRESH_EXPIRATION} +# redis +spring.data.redis.host=localhost +spring.data.redis.port=6379 \ No newline at end of file From 692ddaef7c3857390f8b98b30eee3c15680cb358 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 29 Jan 2025 00:28:09 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/controller/SpaceController.java | 24 +- .../domain/space/dto/SpaceResponseDto.java | 64 ++++ .../domain/space/entity/MemberSpace.java | 53 +++ .../project/domain/space/entity/Space.java | 55 ++- .../repository/MemberSpaceRepository.java | 18 + .../space/repository/SpaceRepository.java | 10 + .../domain/space/service/SpaceService.java | 9 +- .../space/service/SpaceServiceImpl.java | 331 +++++++----------- 8 files changed, 326 insertions(+), 238 deletions(-) create mode 100644 project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java create mode 100644 project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java create mode 100644 project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java create mode 100644 project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java diff --git a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java index 00ba1ef..1428ffd 100644 --- a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java +++ b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -1,24 +1,32 @@ package com.edison.project.domain.space.controller; -import com.edison.project.domain.space.entity.Space; +import com.edison.project.common.response.ApiResponse; +import com.edison.project.common.status.SuccessStatus; +import com.edison.project.domain.space.dto.SpaceResponseDto; import com.edison.project.domain.space.service.SpaceService; -import org.springframework.beans.factory.annotation.Autowired; +import com.edison.project.global.security.CustomUserPrincipal; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/spaces") +@RequiredArgsConstructor public class SpaceController { - @Autowired - private SpaceService spaceService; + private final SpaceService spaceService; - // [POST] Space OPENAI API 이용하여 처리 + // 사용자 버블 데이터 기반 Space 생성 및 처리 @PostMapping("/process") - public ResponseEntity> processSpaces() { - List processedSpaces = spaceService.processSpaces(); - return ResponseEntity.ok(processedSpaces); + @PreAuthorize("isAuthenticated()") + public ResponseEntity processSpaces( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal + ) { + List spaces = spaceService.processSpaces(userPrincipal); + return ApiResponse.onSuccess(SuccessStatus._OK, spaces); } } diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java new file mode 100644 index 0000000..dc20cc7 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java @@ -0,0 +1,64 @@ +package com.edison.project.domain.space.dto; + +import java.util.List; + +public class SpaceResponseDto { + private Long id; + private String content; + private double x; + private double y; + private List groups; + + // 올바른 생성자 추가 + public SpaceResponseDto(Long id, String content, double x, double y, List groups) { + this.id = id; + this.content = content; + this.x = x; + this.y = y; + this.groups = groups; + } + + // 기본 생성자 (필요시 추가) + public SpaceResponseDto() {} + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } +} diff --git a/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java b/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java new file mode 100644 index 0000000..155c5c5 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java @@ -0,0 +1,53 @@ +package com.edison.project.domain.space.entity; + +import com.edison.project.domain.member.entity.Member; +import jakarta.persistence.*; + +@Entity +@Table(name = "member_space") +public class MemberSpace { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "space_id", nullable = false) + private Space space; + + public MemberSpace() {} + + public MemberSpace(Member member, Space space) { + this.member = member; + this.space = space; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Member getMember() { + return member; + } + + public void setMember(Member member) { + this.member = member; + } + + public Space getSpace() { + return space; + } + + public void setSpace(Space space) { + this.space = space; + } +} diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index 2628a33..85d2f0f 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -1,59 +1,56 @@ package com.edison.project.domain.space.entity; + +import jakarta.persistence.*; import java.util.List; +@Entity +@Table(name = "spaces") public class Space { - private Long id; // Bubble의 ID - private String content; // Space 내용 - private double x; // x 좌표 - private double y; // y 좌표 - private List groups; // 속한 그룹 - public Space(String content, double x, double y, List groups, Long id) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private double x; + private double y; + + @ElementCollection + @CollectionTable(name = "space_groups", joinColumns = @JoinColumn(name = "space_id")) + @Column(name = "`group_names`") // ✅ 예약어 문제 해결 + private List groupNames; // ✅ 필드명 변경 + + public Space() {} + + public Space(String content, double x, double y, List groupNames, Long bubbleId) { this.content = content; this.x = x; this.y = y; - this.groups = groups; - this.id = id; + this.groupNames = groupNames; } - // Getter와 Setter + // ✅ Getter & Setter 수정 public Long getId() { return id; } - public void setId(Long id) { - this.id = id; - } - public String getContent() { return content; } - public void setContent(String content) { - this.content = content; - } - public double getX() { return x; } - public void setX(double x) { - this.x = x; - } - public double getY() { return y; } - public void setY(double y) { - this.y = y; - } - - public List getGroups() { - return groups; + public List getGroupNames() { // ✅ 변경된 필드명 반영 + return groupNames; } - public void setGroups(List groups) { - this.groups = groups; + public void setGroupNames(List groupNames) { // ✅ Setter도 수정 + this.groupNames = groupNames; } } diff --git a/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java new file mode 100644 index 0000000..4b45493 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java @@ -0,0 +1,18 @@ +package com.edison.project.domain.space.repository; + +import com.edison.project.domain.space.entity.MemberSpace; +import com.edison.project.domain.space.entity.Space; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MemberSpaceRepository extends JpaRepository { + + // 특정 사용자가 소유한 Space 조회 + @Query("SELECT ms.space FROM MemberSpace ms WHERE ms.member.id = :memberId") + List findSpacesByMemberId(@Param("memberId") Long memberId); +} diff --git a/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java new file mode 100644 index 0000000..a399f97 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java @@ -0,0 +1,10 @@ +package com.edison.project.domain.space.repository; + +import com.edison.project.domain.space.entity.Space; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SpaceRepository extends JpaRepository { +} + diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java index ab357b3..18e1fcf 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java @@ -1,11 +1,10 @@ package com.edison.project.domain.space.service; -import com.edison.project.domain.bubble.entity.Bubble; -import com.edison.project.domain.space.entity.Space; - import java.util.List; -public interface SpaceService { - List processSpaces(); +import com.edison.project.domain.space.dto.SpaceResponseDto; +import com.edison.project.global.security.CustomUserPrincipal; +public interface SpaceService { + List processSpaces(CustomUserPrincipal userPrincipal); } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index bc05529..1761b22 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -1,212 +1,207 @@ package com.edison.project.domain.space.service; +import com.edison.project.common.exception.GeneralException; +import com.edison.project.common.status.ErrorStatus; +import com.edison.project.domain.member.entity.Member; +import com.edison.project.domain.member.repository.MemberRepository; +import com.edison.project.domain.space.dto.SpaceResponseDto; +import com.edison.project.domain.space.entity.MemberSpace; +import com.edison.project.domain.space.entity.Space; +import com.edison.project.domain.space.repository.MemberSpaceRepository; +import com.edison.project.domain.space.repository.SpaceRepository; import com.edison.project.domain.bubble.entity.Bubble; import com.edison.project.domain.bubble.repository.BubbleRepository; -import com.edison.project.domain.space.entity.Space; + +import com.edison.project.global.security.CustomUserPrincipal; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; -import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; -import okhttp3.OkHttpClient; -import java.util.concurrent.TimeUnit; - +import org.springframework.stereotype.Service; import java.io.IOException; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Service public class SpaceServiceImpl implements SpaceService { + private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final SpaceRepository spaceRepository; + private final MemberSpaceRepository memberSpaceRepository; private final BubbleRepository bubbleRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final MemberRepository memberRepository; - public SpaceServiceImpl(BubbleRepository bubbleRepository) { + public SpaceServiceImpl(SpaceRepository spaceRepository, + MemberSpaceRepository memberSpaceRepository, + BubbleRepository bubbleRepository, MemberRepository memberRepository) { + this.spaceRepository = spaceRepository; + this.memberSpaceRepository = memberSpaceRepository; this.bubbleRepository = bubbleRepository; + this.memberRepository = memberRepository; } @Override - public List processSpaces() { - // 1. Bubble 데이터를 가져옵니다. - List bubbles = bubbleRepository.findAll(); + public List processSpaces(CustomUserPrincipal userPrincipal) { + Long memberId = userPrincipal.getMemberId(); - // 2. Bubble 데이터를 content로 변환 - List> requestData = createRequestData(bubbles); + // ✅ 기존 사용자의 Space 가져오기 + List spaces = memberSpaceRepository.findSpacesByMemberId(memberId); - // 3. requestData에서 content만 추출 - List contents = requestData.stream() - .map(data -> (String) data.get("content")) // "content" 필드만 추출 - .collect(Collectors.toList()); + // ✅ 새로운 Bubble 데이터를 가져와 GPT로 변환 + List bubbles = bubbleRepository.findAll(); // TODO: 사용자의 Bubble만 가져오도록 수정 가능 + Map requestData = createRequestDataWithId(bubbles); - // 4. GPT 호출 - String gptResponse = callGPTForGrouping(contents); + String gptResponse = callGPTForGrouping(requestData); + List newSpaces = parseGptResponse(gptResponse, bubbles); - // 5. GPT 응답 파싱 및 매핑 - return parseGptResponse(gptResponse, bubbles); - } + // ✅ 새로운 Space를 저장하고 MemberSpace와 연결 + for (Space space : newSpaces) { + spaceRepository.save(space); + + MemberSpace memberSpace = new MemberSpace(); + + Member member = memberRepository.findById(userPrincipal.getMemberId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + memberSpace.setMember(member); + memberSpace.setSpace(space); + memberSpaceRepository.save(memberSpace); + } - // Bubble 데이터를 content로 변환 - private List> createRequestData(List bubbles) { - List> requestData = new ArrayList<>(); + // ✅ 기존 Space + 새로운 Space 반환 + spaces.addAll(newSpaces); + + return spaces.stream() + .map(space -> new SpaceResponseDto( + space.getId(), + space.getContent(), + space.getX(), + space.getY(), + space.getGroupNames() + )) + .collect(Collectors.toList()); + } + // ✅ Bubble 데이터를 GPT 요청 형식으로 변환 + private Map createRequestDataWithId(List bubbles) { + Map requestData = new HashMap<>(); for (Bubble bubble : bubbles) { - // Bubble의 Labels 병합 String labels = bubble.getLabels().stream() .map(label -> label.getLabel().getName()) .collect(Collectors.joining(", ")); - - // Bubble의 제목, 내용, 라벨을 병합 - String content = String.format( - "Title: %s\nContent: %s\nLabels: %s", - bubble.getTitle(), - bubble.getContent(), - labels.isEmpty() ? "None" : labels - ); - - // 요청 데이터 생성 - Map data = new HashMap<>(); - data.put("id", bubble.getBubbleId()); // 실제 Bubble ID - data.put("content", content); - - requestData.add(data); + String content = String.format("Title: %s\nContent: %s\nLabels: %s", + bubble.getTitle(), bubble.getContent(), labels.isEmpty() ? "None" : labels); + requestData.put(bubble.getBubbleId(), content); } - return requestData; } - - // GPT API 호출 - private String callGPTForGrouping(List contents) { + // ✅ GPT 호출하여 Space 좌표 변환 + private String callGPTForGrouping(Map requestData) { String openaiApiKey = System.getenv("openai_key"); if (openaiApiKey == null || openaiApiKey.isEmpty()) { throw new RuntimeException("OpenAI API 키가 환경변수에 설정되어 있지 않습니다."); } - if (contents == null || contents.isEmpty()) { - throw new IllegalArgumentException("요청 데이터가 비어 있습니다."); - } - - // JSON 요청 본문 생성 - ObjectMapper objectMapper = new ObjectMapper(); - Map message = Map.of("role", "system", "content", buildPrompt(contents)); + Map message = Map.of("role", "system", "content", buildPromptWithId(requestData)); Map requestBody = Map.of("model", "gpt-3.5-turbo", "messages", List.of(message)); - String jsonBody; try { - jsonBody = objectMapper.writeValueAsString(requestBody); - } catch (Exception e) { - throw new RuntimeException("JSON 생성 중 오류 발생: " + e.getMessage(), e); - } - - // API 요청 - OkHttpClient client = createHttpClientWithTimeout(); - RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); - Request request = new Request.Builder() - .url(OPENAI_API_URL) - .post(body) - .addHeader("Authorization", "Bearer " + openaiApiKey) - .addHeader("Content-Type", "application/json") - .build(); - - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) { - String responseBody = response.body() != null ? response.body().string() : "No response body"; - System.out.println("Response Code: " + response.code()); - System.out.println("Response Body: " + responseBody); - throw new RuntimeException("OpenAI API 호출 실패: " + response.code() + " - " + responseBody); + String jsonBody = objectMapper.writeValueAsString(requestBody); + OkHttpClient client = createHttpClientWithTimeout(); + RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); + Request request = new Request.Builder() + .url(OPENAI_API_URL) + .post(body) + .addHeader("Authorization", "Bearer " + openaiApiKey) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + String responseBody = response.body() != null ? response.body().string() : "No response body"; + throw new RuntimeException("OpenAI API 호출 실패: " + response.code() + " - " + responseBody); + } + return response.body().string(); } - - return response.body().string(); } catch (IOException e) { throw new RuntimeException("OpenAI API 호출 중 오류 발생: " + e.getMessage(), e); } } - // GPT 응답 파싱 및 Space 매핑 private List parseGptResponse(String gptResponse, List bubbles) { try { - String sanitizedResponse = sanitizeResponse(gptResponse); - System.out.println("Sanitized GPT Response: " + sanitizedResponse); - ObjectMapper objectMapper = new ObjectMapper(); - List> parsedData = objectMapper.readValue( - sanitizedResponse, new TypeReference>>() {}); - // 그룹화 결과를 저장할 리스트 - List spaces = new ArrayList<>(); + // ✅ 1. GPT 응답을 Map으로 변환 + Map responseMap = objectMapper.readValue(gptResponse, new TypeReference<>() {}); - // 그룹화 로직: 같은 그룹끼리 묶음 - Map>> groupedById = parsedData.stream() - .collect(Collectors.groupingBy(item -> ((Number) item.get("id")).longValue())); + // ✅ 2. 'choices' 내부 메시지 추출 + List> choices = (List>) responseMap.get("choices"); + if (choices == null || choices.isEmpty()) { + throw new RuntimeException("'choices' 필드가 비어 있음"); + } - for (Map.Entry>> entry : groupedById.entrySet()) { - Long id = entry.getKey(); - List> groupItems = entry.getValue(); + // ✅ 3. 'message' 내부 'content' 추출 (이 부분이 실제 JSON 데이터) + Map message = (Map) choices.get(0).get("message"); + if (message == null || !message.containsKey("content")) { + throw new RuntimeException("'message' 필드가 없거나 'content'가 없음"); + } + + // ✅ 4. 'content' 값(문자열 JSON)을 다시 ObjectMapper로 파싱 + String contentJson = (String) message.get("content"); + contentJson = contentJson.replaceAll("```json", "").replaceAll("```", "").trim(); // ✅ GPT가 코드 블록 감쌌을 경우 정리 + System.out.println("Sanitized Content JSON: " + contentJson); // 디버깅용 출력 + + // ✅ 5. 실제 Space 데이터를 List으로 변환 + List> parsedData = objectMapper.readValue( + contentJson, new TypeReference>>() {} + ); - // ID에 맞는 Bubble 찾기 + // ✅ 6. Space 엔티티로 변환 + List spaces = new ArrayList<>(); + for (Map item : parsedData) { + Long id = ((Number) item.get("id")).longValue(); Optional optionalBubble = bubbles.stream() .filter(bubble -> bubble.getBubbleId().equals(id)) .findFirst(); - - if (optionalBubble.isEmpty()) { - System.err.println("Warning: Bubble not found for ID: " + id); - continue; // 매칭되지 않는 ID는 스킵 - } + if (optionalBubble.isEmpty()) continue; Bubble bubble = optionalBubble.get(); - - // 그룹 내용을 결합하여 하나의 Space로 생성 - StringBuilder contentBuilder = new StringBuilder(); - for (Map groupItem : groupItems) { - String content = (String) groupItem.get("content"); - if (content != null && !content.isBlank()) { - contentBuilder.append(content).append("\n"); - } - } - - // 좌표 및 그룹 설정 (첫 번째 항목 기준) - double x = ((Number) groupItems.get(0).get("x")).doubleValue(); - double y = ((Number) groupItems.get(0).get("y")).doubleValue(); - List rawGroups = (List) groupItems.get(0).get("groups"); - List groups = rawGroups.stream() - .map(group -> group instanceof Number ? ((Number) group).intValue() : Integer.parseInt(group.toString())) + String content = (String) item.get("content"); + double x = ((Number) item.get("x")).doubleValue(); + double y = ((Number) item.get("y")).doubleValue(); + List groups = ((List) item.get("groups")).stream() + .map(Object::toString) .collect(Collectors.toList()); - // Space 객체 생성 - Space space = new Space( - contentBuilder.toString().trim(), // 그룹화된 content - x, - y, - groups.stream().map(String::valueOf).toList(), - bubble.getBubbleId() - ); - spaces.add(space); + spaces.add(new Space(content, x, y, groups, bubble.getBubbleId())); } - - // 디버그 출력 - System.out.println("=== 매핑 결과 ==="); - for (Space space : spaces) { - System.out.println("ID: " + space.getId()); - System.out.println("Content: " + space.getContent()); - System.out.println("x: " + space.getX()); - System.out.println("y: " + space.getY()); - System.out.println("Groups: " + space.getGroups()); - System.out.println("----------------"); - } - return spaces; } catch (Exception e) { - System.err.println("GPT Response Parsing Error: " + e.getMessage()); throw new RuntimeException("GPT 응답 파싱 중 오류 발생: " + e.getMessage(), e); } } - private String buildPrompt(List contents) { - StringBuilder promptBuilder = new StringBuilder(); + // ✅ GPT 응답 데이터 정리 + private String sanitizeResponse(String response) { + try { + if (response == null || response.isBlank()) throw new RuntimeException("GPT 응답이 비어 있습니다."); + objectMapper.readTree(response); + return response.trim(); + } catch (IOException e) { + throw new RuntimeException("GPT 응답 처리 중 오류 발생: " + e.getMessage(), e); + } + } + + // ✅ GPT 요청 프롬프트 생성 + private String buildPromptWithId(Map requestData) { + StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); promptBuilder.append("Each item should have the following attributes:\n"); promptBuilder.append("- `id`: A unique identifier for the item (integer).\n"); @@ -218,72 +213,18 @@ private String buildPrompt(List contents) { promptBuilder.append("1. Each item must have a unique `(x, y)` coordinate.\n"); promptBuilder.append("2. Items with similar topics should have closer `(x, y)` coordinates.\n"); promptBuilder.append("3. Items with different topics should have larger distances between their coordinates.\n"); - promptBuilder.append("4. Coordinates should include decimal values to express fine-grained similarity.\n"); + promptBuilder.append("4. The spread or distance for groups is up to you to decide.\n"); promptBuilder.append("5. Ensure `groups` contains only integers, and avoid any other data types.\n"); promptBuilder.append("6. Return only valid JSON output in the following format:\n\n"); - promptBuilder.append("[\n"); - promptBuilder.append(" {\n"); - promptBuilder.append(" \"id\": ,\n"); - promptBuilder.append(" \"content\": \"\",\n"); - promptBuilder.append(" \"x\": ,\n"); - promptBuilder.append(" \"y\": ,\n"); - promptBuilder.append(" \"groups\": []\n"); - promptBuilder.append(" }\n"); - promptBuilder.append("]\n\n"); - promptBuilder.append("Return only valid JSON output without any additional text or explanation.\n"); - - promptBuilder.append("### Input Content:\n"); - for (String content : contents) { - promptBuilder.append("- ").append(content).append("\n"); + + for (Map.Entry entry : requestData.entrySet()) { + promptBuilder.append("- ID: ").append(entry.getKey()).append("\n"); + promptBuilder.append(entry.getValue()).append("\n"); } return promptBuilder.toString(); } - - - // GPT 요청 프롬프트 생성 - private String sanitizeResponse(String response) { - try { - if (response == null || response.isBlank()) { - throw new RuntimeException("GPT 응답이 비어 있습니다."); - } - - ObjectMapper objectMapper = new ObjectMapper(); - Map responseMap; - - try { - responseMap = objectMapper.readValue(response, new TypeReference>() {}); - } catch (Exception e) { - throw new RuntimeException("GPT 응답이 JSON 형식이 아닙니다: " + response, e); - } - - List> choices = (List>) responseMap.get("choices"); - if (choices == null || choices.isEmpty()) { - throw new RuntimeException("'choices' 필드가 비어있습니다."); - } - - Map message = (Map) choices.get(0).get("message"); - if (message == null || !message.containsKey("content")) { - throw new RuntimeException("'message' 필드에 'content'가 없습니다."); - } - - String content = (String) message.get("content"); - if (content == null || content.isBlank()) { - throw new RuntimeException("'content' 값이 비어 있습니다."); - } - - content = content.replaceAll("```json", "").replaceAll("```", "").trim(); - objectMapper.readTree(content); - - return content; - - } catch (Exception e) { - System.err.println("GPT 응답 처리 중 오류 발생: " + e.getMessage()); - throw new RuntimeException("응답 정리 중 오류 발생: " + e.getMessage(), e); - } - } - private OkHttpClient createHttpClientWithTimeout() { return new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) // 연결 타임아웃 설정 @@ -291,6 +232,4 @@ private OkHttpClient createHttpClientWithTimeout() { .readTimeout(60, TimeUnit.SECONDS) // 읽기 타임아웃 설정 .build(); } - } - From 72a11df2f061b38eb1b746e26e0329b5a1986b99 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 29 Jan 2025 19:49:25 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84=20api=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/common/status/ErrorStatus.java | 5 +- .../bubble/repository/BubbleRepository.java | 2 + .../space/controller/SpaceController.java | 25 +-- .../project/domain/space/entity/Space.java | 21 ++- .../domain/space/service/SpaceService.java | 8 +- .../space/service/SpaceServiceImpl.java | 178 ++++++++++++------ 6 files changed, 163 insertions(+), 76 deletions(-) diff --git a/project/src/main/java/com/edison/project/common/status/ErrorStatus.java b/project/src/main/java/com/edison/project/common/status/ErrorStatus.java index 6443468..df0db3a 100644 --- a/project/src/main/java/com/edison/project/common/status/ErrorStatus.java +++ b/project/src/main/java/com/edison/project/common/status/ErrorStatus.java @@ -62,7 +62,10 @@ public enum ErrorStatus { LETTERS_NOT_FOUND(HttpStatus.BAD_REQUEST, "LETTER4011", "아트레터를 찾을 수 없습니다."), // 검색 관련 에러 - INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "SEARCH4001", "검색어는 공백일 수 없습니다."); + INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "SEARCH4001", "검색어는 공백일 수 없습니다."), + + // 스페이스 관련 에러 + NO_BUBBLES_FOUND(HttpStatus.BAD_REQUEST,"SPACE4001", "작성된 버블이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java index eb509a9..97ef521 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java +++ b/project/src/main/java/com/edison/project/domain/bubble/repository/BubbleRepository.java @@ -23,7 +23,9 @@ public interface BubbleRepository extends JpaRepository { // 휴지통에 있는 Bubble만 조회 Optional findByBubbleIdAndIsDeletedTrue(Long bubbleId); + @Query("SELECT b FROM Bubble b WHERE b.member.memberId = :memberId AND b.isDeleted = false") Page findByMember_MemberIdAndIsDeletedFalse(Long memberId, Pageable pageable); + Page findByMember_MemberIdAndIsDeletedTrue(Long memberId, Pageable pageable); // 7일 이내 버블 목록 diff --git a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java index 1428ffd..e6c9f43 100644 --- a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java +++ b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -5,28 +5,31 @@ import com.edison.project.domain.space.dto.SpaceResponseDto; import com.edison.project.domain.space.service.SpaceService; import com.edison.project.global.security.CustomUserPrincipal; -import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/spaces") -@RequiredArgsConstructor public class SpaceController { private final SpaceService spaceService; - // 사용자 버블 데이터 기반 Space 생성 및 처리 - @PostMapping("/process") - @PreAuthorize("isAuthenticated()") - public ResponseEntity processSpaces( - @AuthenticationPrincipal CustomUserPrincipal userPrincipal - ) { - List spaces = spaceService.processSpaces(userPrincipal); + public SpaceController(SpaceService spaceService) { + this.spaceService = spaceService; + } + + @GetMapping + public ResponseEntity getSpaces( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal, Pageable pageable) { + ResponseEntity response = spaceService.processSpaces(userPrincipal, pageable); + List spaces = (List) response.getBody().getResult(); return ApiResponse.onSuccess(SuccessStatus._OK, spaces); } } diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index 85d2f0f..2238125 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -1,5 +1,6 @@ package com.edison.project.domain.space.entity; +import com.edison.project.domain.bubble.entity.Bubble; import jakarta.persistence.*; import java.util.List; @@ -15,18 +16,24 @@ public class Space { private double x; private double y; + // ✅ Bubble과의 관계 설정 (ManyToOne) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "bubble_id", nullable = false) // 🚨 `NOT NULL` 적용 + private Bubble bubble; + @ElementCollection @CollectionTable(name = "space_groups", joinColumns = @JoinColumn(name = "space_id")) - @Column(name = "`group_names`") // ✅ 예약어 문제 해결 - private List groupNames; // ✅ 필드명 변경 + @Column(name = "group_names") // ✅ 예약어 문제 해결 (`groups` → `group_names`) + private List groupNames; public Space() {} - public Space(String content, double x, double y, List groupNames, Long bubbleId) { + public Space(String content, double x, double y, List groupNames, Bubble bubble) { this.content = content; this.x = x; this.y = y; this.groupNames = groupNames; + this.bubble = bubble; // ✅ `bubble_id` 설정 } // ✅ Getter & Setter 수정 @@ -46,6 +53,14 @@ public double getY() { return y; } + public Bubble getBubble() { // ✅ Bubble 관련 Getter 추가 + return bubble; + } + + public void setBubble(Bubble bubble) { // ✅ Bubble 관련 Setter 추가 + this.bubble = bubble; + } + public List getGroupNames() { // ✅ 변경된 필드명 반영 return groupNames; } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java index 18e1fcf..00a58b7 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java @@ -1,10 +1,10 @@ package com.edison.project.domain.space.service; -import java.util.List; - -import com.edison.project.domain.space.dto.SpaceResponseDto; +import com.edison.project.common.response.ApiResponse; import com.edison.project.global.security.CustomUserPrincipal; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; public interface SpaceService { - List processSpaces(CustomUserPrincipal userPrincipal); + ResponseEntity processSpaces(CustomUserPrincipal userPrincipal, Pageable pageable); } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 1761b22..aa0d5c0 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -1,7 +1,10 @@ package com.edison.project.domain.space.service; import com.edison.project.common.exception.GeneralException; +import com.edison.project.common.response.ApiResponse; +import com.edison.project.common.response.PageInfo; import com.edison.project.common.status.ErrorStatus; +import com.edison.project.common.status.SuccessStatus; import com.edison.project.domain.member.entity.Member; import com.edison.project.domain.member.repository.MemberRepository; import com.edison.project.domain.space.dto.SpaceResponseDto; @@ -11,19 +14,27 @@ import com.edison.project.domain.space.repository.SpaceRepository; import com.edison.project.domain.bubble.entity.Bubble; import com.edison.project.domain.bubble.repository.BubbleRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import com.edison.project.global.security.CustomUserPrincipal; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; import com.fasterxml.jackson.core.type.TypeReference; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import okhttp3.*; +import java.util.*; + @Service +@Transactional public class SpaceServiceImpl implements SpaceService { private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; @@ -44,37 +55,56 @@ public SpaceServiceImpl(SpaceRepository spaceRepository, } @Override - public List processSpaces(CustomUserPrincipal userPrincipal) { + @Transactional + public ResponseEntity processSpaces(CustomUserPrincipal userPrincipal, Pageable pageable) { Long memberId = userPrincipal.getMemberId(); + System.out.println("🔍 [Process Spaces] 실행 - 사용자 ID: " + memberId); + // ✅ 기존 사용자의 Space 가져오기 List spaces = memberSpaceRepository.findSpacesByMemberId(memberId); + System.out.println("📌 기존 사용자의 Space 개수: " + spaces.size()); + + // ✅ 사용자의 삭제되지 않은 Bubble 페이징 처리 + Page bubblePage = bubbleRepository.findByMember_MemberIdAndIsDeletedFalse(memberId, pageable); + + // ✅ Page 정보 설정 + PageInfo pageInfo = new PageInfo( + bubblePage.getNumber(), + bubblePage.getSize(), + bubblePage.hasNext(), + bubblePage.getTotalElements(), + bubblePage.getTotalPages() + ); + + // ✅ Bubble 데이터 변환 + List bubbles = bubblePage.getContent(); + System.out.println("🫧 사용자의 Bubble 개수: " + bubbles.size()); + + // ✅ 버블이 없을 경우 -> "작성된 버블이 없습니다." 메시지 반환 + if (bubbles.isEmpty()) { + System.out.println("⚠️ 사용자에게 등록된 버블이 없습니다."); + return ApiResponse.onFailure(ErrorStatus.NO_BUBBLES_FOUND); + } - // ✅ 새로운 Bubble 데이터를 가져와 GPT로 변환 - List bubbles = bubbleRepository.findAll(); // TODO: 사용자의 Bubble만 가져오도록 수정 가능 Map requestData = createRequestDataWithId(bubbles); + // ✅ GPT 호출하여 Space 좌표 변환 String gptResponse = callGPTForGrouping(requestData); + System.out.println("🛠 GPT 응답: " + gptResponse); + List newSpaces = parseGptResponse(gptResponse, bubbles); + System.out.println("✅ 변환된 Space 개수: " + newSpaces.size()); // ✅ 새로운 Space를 저장하고 MemberSpace와 연결 for (Space space : newSpaces) { - spaceRepository.save(space); - - MemberSpace memberSpace = new MemberSpace(); - - Member member = memberRepository.findById(userPrincipal.getMemberId()) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - memberSpace.setMember(member); - - memberSpace.setSpace(space); - memberSpaceRepository.save(memberSpace); + saveSpaceAndMemberSpace(space, memberId); } // ✅ 기존 Space + 새로운 Space 반환 spaces.addAll(newSpaces); - return spaces.stream() + List spaceDtos = spaces.stream() .map(space -> new SpaceResponseDto( space.getId(), space.getContent(), @@ -83,20 +113,40 @@ public List processSpaces(CustomUserPrincipal userPrincipal) { space.getGroupNames() )) .collect(Collectors.toList()); + + return ApiResponse.onSuccess(SuccessStatus._OK, pageInfo, spaceDtos); + } + + // ✅ Space와 MemberSpace 저장 및 즉시 반영 + @Transactional + public void saveSpaceAndMemberSpace(Space space, Long memberId) { + spaceRepository.save(space); + spaceRepository.flush(); // ✅ 즉시 반영 + System.out.println("💾 저장된 Space ID: " + space.getId()); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + + MemberSpace memberSpace = new MemberSpace(); + memberSpace.setMember(member); + memberSpace.setSpace(space); + memberSpaceRepository.save(memberSpace); + memberSpaceRepository.flush(); // ✅ 즉시 반영 + System.out.println("🔗 MemberSpace 연결됨: " + memberSpace.getMember().getMemberId() + " -> " + memberSpace.getSpace().getId()); } // ✅ Bubble 데이터를 GPT 요청 형식으로 변환 private Map createRequestDataWithId(List bubbles) { - Map requestData = new HashMap<>(); - for (Bubble bubble : bubbles) { - String labels = bubble.getLabels().stream() - .map(label -> label.getLabel().getName()) - .collect(Collectors.joining(", ")); - String content = String.format("Title: %s\nContent: %s\nLabels: %s", - bubble.getTitle(), bubble.getContent(), labels.isEmpty() ? "None" : labels); - requestData.put(bubble.getBubbleId(), content); - } - return requestData; + return bubbles.stream().collect(Collectors.toMap( + Bubble::getBubbleId, + bubble -> String.format("Title: %s\nContent: %s\nLabels: %s", + bubble.getTitle(), + bubble.getContent(), + bubble.getLabels().stream() + .map(label -> label.getLabel().getName()) + .collect(Collectors.joining(", ")) + ) + )); } // ✅ GPT 호출하여 Space 좌표 변환 @@ -111,7 +161,12 @@ private String callGPTForGrouping(Map requestData) { try { String jsonBody = objectMapper.writeValueAsString(requestBody); - OkHttpClient client = createHttpClientWithTimeout(); + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build(); + RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); Request request = new Request.Builder() .url(OPENAI_API_URL) @@ -122,8 +177,7 @@ private String callGPTForGrouping(Map requestData) { try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { - String responseBody = response.body() != null ? response.body().string() : "No response body"; - throw new RuntimeException("OpenAI API 호출 실패: " + response.code() + " - " + responseBody); + throw new RuntimeException("OpenAI API 호출 실패: " + response.code()); } return response.body().string(); } @@ -131,43 +185,61 @@ private String callGPTForGrouping(Map requestData) { throw new RuntimeException("OpenAI API 호출 중 오류 발생: " + e.getMessage(), e); } } + // ✅ GPT 응답 데이터 정리 + private String sanitizeResponse(String response) { + try { + if (response == null || response.isBlank()) throw new RuntimeException("GPT 응답이 비어 있습니다."); + objectMapper.readTree(response); + return response.trim(); + } catch (IOException e) { + throw new RuntimeException("GPT 응답 처리 중 오류 발생: " + e.getMessage(), e); + } + } private List parseGptResponse(String gptResponse, List bubbles) { try { ObjectMapper objectMapper = new ObjectMapper(); // ✅ 1. GPT 응답을 Map으로 변환 - Map responseMap = objectMapper.readValue(gptResponse, new TypeReference<>() {}); + Map responseMap = objectMapper.readValue(gptResponse, new TypeReference>() {}); - // ✅ 2. 'choices' 내부 메시지 추출 + // ✅ 2. "choices" 필드 확인 List> choices = (List>) responseMap.get("choices"); if (choices == null || choices.isEmpty()) { throw new RuntimeException("'choices' 필드가 비어 있음"); } - // ✅ 3. 'message' 내부 'content' 추출 (이 부분이 실제 JSON 데이터) + // ✅ 3. "message" 내부 "content" 확인 Map message = (Map) choices.get(0).get("message"); if (message == null || !message.containsKey("content")) { throw new RuntimeException("'message' 필드가 없거나 'content'가 없음"); } - // ✅ 4. 'content' 값(문자열 JSON)을 다시 ObjectMapper로 파싱 + // ✅ 4. "content" 값에서 JSON 문자열 추출 후 다시 변환 String contentJson = (String) message.get("content"); - contentJson = contentJson.replaceAll("```json", "").replaceAll("```", "").trim(); // ✅ GPT가 코드 블록 감쌌을 경우 정리 - System.out.println("Sanitized Content JSON: " + contentJson); // 디버깅용 출력 - // ✅ 5. 실제 Space 데이터를 List으로 변환 - List> parsedData = objectMapper.readValue( - contentJson, new TypeReference>>() {} - ); + // ✅ JSON이 ```json ... ``` 형태일 경우 제거 + contentJson = contentJson.replaceAll("```json", "").replaceAll("```", "").trim(); + + // ✅ 5. 문자열을 다시 Map으로 변환 + Map parsedContent = objectMapper.readValue(contentJson, new TypeReference>() {}); - // ✅ 6. Space 엔티티로 변환 + // ✅ 6. "items" 필드 확인 및 리스트 변환 + List> parsedData = (List>) parsedContent.get("items"); + if (parsedData == null || parsedData.isEmpty()) { + throw new RuntimeException("'items' 필드가 비어 있음"); + } + + System.out.println("✅ 변환된 Space 데이터: " + parsedData); + + // ✅ 7. Space 엔티티로 변환 List spaces = new ArrayList<>(); for (Map item : parsedData) { Long id = ((Number) item.get("id")).longValue(); Optional optionalBubble = bubbles.stream() .filter(bubble -> bubble.getBubbleId().equals(id)) .findFirst(); + if (optionalBubble.isEmpty()) continue; Bubble bubble = optionalBubble.get(); @@ -178,7 +250,7 @@ private List parseGptResponse(String gptResponse, List bubbles) { .map(Object::toString) .collect(Collectors.toList()); - spaces.add(new Space(content, x, y, groups, bubble.getBubbleId())); + spaces.add(new Space(content, x, y, groups, bubble)); } return spaces; @@ -188,33 +260,23 @@ private List parseGptResponse(String gptResponse, List bubbles) { } - // ✅ GPT 응답 데이터 정리 - private String sanitizeResponse(String response) { - try { - if (response == null || response.isBlank()) throw new RuntimeException("GPT 응답이 비어 있습니다."); - objectMapper.readTree(response); - return response.trim(); - } catch (IOException e) { - throw new RuntimeException("GPT 응답 처리 중 오류 발생: " + e.getMessage(), e); - } - } // ✅ GPT 요청 프롬프트 생성 private String buildPromptWithId(Map requestData) { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); promptBuilder.append("Each item should have the following attributes:\n"); - promptBuilder.append("- `id`: A unique identifier for the item (integer).\n"); - promptBuilder.append("- `content`: A string representing the item's content.\n"); - promptBuilder.append("- `x`: A unique floating-point number for the x-coordinate.\n"); - promptBuilder.append("- `y`: A unique floating-point number for the y-coordinate.\n"); - promptBuilder.append("- `groups`: A list of integers representing the item's group IDs.\n\n"); + promptBuilder.append("- id: A unique identifier for the item (integer).\n"); + promptBuilder.append("- content: A string representing the item's content.\n"); + promptBuilder.append("- x: A unique floating-point number for the x-coordinate.\n"); + promptBuilder.append("- y: A unique floating-point number for the y-coordinate.\n"); + promptBuilder.append("- groups: A list of integers representing the item's group IDs.\n\n"); promptBuilder.append("### Rules:\n"); - promptBuilder.append("1. Each item must have a unique `(x, y)` coordinate.\n"); - promptBuilder.append("2. Items with similar topics should have closer `(x, y)` coordinates.\n"); + promptBuilder.append("1. Each item must have a unique (x, y) coordinate.\n"); + promptBuilder.append("2. Items with similar topics should have closer (x, y) coordinates.\n"); promptBuilder.append("3. Items with different topics should have larger distances between their coordinates.\n"); promptBuilder.append("4. The spread or distance for groups is up to you to decide.\n"); - promptBuilder.append("5. Ensure `groups` contains only integers, and avoid any other data types.\n"); + promptBuilder.append("5. Ensure groups contains only integers, and avoid any other data types.\n"); promptBuilder.append("6. Return only valid JSON output in the following format:\n\n"); for (Map.Entry entry : requestData.entrySet()) { @@ -233,3 +295,5 @@ private OkHttpClient createHttpClientWithTimeout() { .build(); } } + + From 944ae6a4e3a9fbcdd44ec07636575d6179a12e00 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Sat, 1 Feb 2025 21:20:28 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/domain/space/entity/Space.java | 32 +++++++++++++++++-- .../space/service/SpaceServiceImpl.java | 24 ++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index 2238125..a68e6a7 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -26,17 +26,23 @@ public class Space { @Column(name = "group_names") // ✅ 예약어 문제 해결 (`groups` → `group_names`) private List groupNames; + @Column(nullable = false) // member_id 추가 + private Long memberId; + + // ✅ 기본 생성자 (JPA 필수) public Space() {} - public Space(String content, double x, double y, List groupNames, Bubble bubble) { + // ✅ memberId와 Bubble 포함한 생성자 + public Space(String content, double x, double y, List groupNames, Bubble bubble, Long memberId) { this.content = content; this.x = x; this.y = y; this.groupNames = groupNames; this.bubble = bubble; // ✅ `bubble_id` 설정 + this.memberId = memberId; } - // ✅ Getter & Setter 수정 + // ✅ Getter & Setter public Long getId() { return id; } @@ -45,14 +51,26 @@ public String getContent() { return content; } + public void setContent(String content) { + this.content = content; + } + public double getX() { return x; } + public void setX(double x) { + this.x = x; + } + public double getY() { return y; } + public void setY(double y) { + this.y = y; + } + public Bubble getBubble() { // ✅ Bubble 관련 Getter 추가 return bubble; } @@ -65,7 +83,15 @@ public List getGroupNames() { // ✅ 변경된 필드명 반영 return groupNames; } - public void setGroupNames(List groupNames) { // ✅ Setter도 수정 + public void setGroupNames(List groupNames) { // ✅ Setter 추가 this.groupNames = groupNames; } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index aa0d5c0..ed903c2 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -93,7 +93,7 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip String gptResponse = callGPTForGrouping(requestData); System.out.println("🛠 GPT 응답: " + gptResponse); - List newSpaces = parseGptResponse(gptResponse, bubbles); + List newSpaces = parseGptResponse(gptResponse, bubbles, memberId); System.out.println("✅ 변환된 Space 개수: " + newSpaces.size()); // ✅ 새로운 Space를 저장하고 MemberSpace와 연결 @@ -196,10 +196,12 @@ private String sanitizeResponse(String response) { } } - private List parseGptResponse(String gptResponse, List bubbles) { + private List parseGptResponse(String gptResponse, List bubbles, Long memberId) { try { ObjectMapper objectMapper = new ObjectMapper(); + System.out.println("🔍 Raw GPT Response (Before Parsing): " + gptResponse); + // ✅ 1. GPT 응답을 Map으로 변환 Map responseMap = objectMapper.readValue(gptResponse, new TypeReference>() {}); @@ -250,7 +252,7 @@ private List parseGptResponse(String gptResponse, List bubbles) { .map(Object::toString) .collect(Collectors.toList()); - spaces.add(new Space(content, x, y, groups, bubble)); + spaces.add(new Space(content, x, y, groups, bubble, memberId)); } return spaces; @@ -267,17 +269,21 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); promptBuilder.append("Each item should have the following attributes:\n"); promptBuilder.append("- id: A unique identifier for the item (integer).\n"); - promptBuilder.append("- content: A string representing the item's content.\n"); + promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); promptBuilder.append("- x: A unique floating-point number for the x-coordinate.\n"); promptBuilder.append("- y: A unique floating-point number for the y-coordinate.\n"); promptBuilder.append("- groups: A list of integers representing the item's group IDs.\n\n"); + promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate.\n"); - promptBuilder.append("2. Items with similar topics should have closer (x, y) coordinates.\n"); - promptBuilder.append("3. Items with different topics should have larger distances between their coordinates.\n"); - promptBuilder.append("4. The spread or distance for groups is up to you to decide.\n"); - promptBuilder.append("5. Ensure groups contains only integers, and avoid any other data types.\n"); - promptBuilder.append("6. Return only valid JSON output in the following format:\n\n"); + promptBuilder.append("2. Items with similar topics should be clustered like a firework explosion, forming visually distinct groups.\n"); + promptBuilder.append("3. Groups should be separated from each other while maintaining internal coherence.\n"); + promptBuilder.append("4. The spread or distance for groups is up to you to decide, but they should appear like bursts from a central point.\n"); + promptBuilder.append("5. Ensure groups contain only integers, and avoid any other data types.\n"); + promptBuilder.append("6. X and Y coordinates do not need to follow a uniform increase; they can be randomly distributed while maintaining the clustering structure.\n"); + promptBuilder.append("7. Return only valid JSON output in the following format:\n\n"); + promptBuilder.append("8. Clusters can be separate, but items with similar themes should be placed near each other, even if they belong to different clusters.\n"); + promptBuilder.append("9. Content should be reduced to its **core meaning**: extract only **one or two essential words** that best describe it.\n"); for (Map.Entry entry : requestData.entrySet()) { promptBuilder.append("- ID: ").append(entry.getKey()).append("\n"); From a33a0fab01bebafc5567c7511767a1ccdcf6bf3f Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Sat, 1 Feb 2025 22:23:17 +0900 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20id=20null=EA=B0=92=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/space/dto/SpaceResponseDto.java | 11 ++- .../repository/MemberSpaceRepository.java | 5 +- .../space/repository/SpaceRepository.java | 6 ++ .../domain/space/service/SpaceService.java | 1 + .../space/service/SpaceServiceImpl.java | 95 ++++++++++++++----- 5 files changed, 92 insertions(+), 26 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java index dc20cc7..8de7f30 100644 --- a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java @@ -1,7 +1,12 @@ package com.edison.project.domain.space.dto; +import com.edison.project.domain.bubble.entity.Bubble; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.ArrayList; import java.util.List; +@JsonIgnoreProperties ({"hibernateLazyInitializer", "handler"}) public class SpaceResponseDto { private Long id; private String content; @@ -10,12 +15,12 @@ public class SpaceResponseDto { private List groups; // 올바른 생성자 추가 - public SpaceResponseDto(Long id, String content, double x, double y, List groups) { - this.id = id; + public SpaceResponseDto(Bubble bubble, String content, double x, double y, List groups) { + this.id = bubble.getBubbleId(); this.content = content; this.x = x; this.y = y; - this.groups = groups; + this.groups = new ArrayList<>(groups);; } // 기본 생성자 (필요시 추가) diff --git a/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java index 4b45493..e319f05 100644 --- a/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java +++ b/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java @@ -8,11 +8,14 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface MemberSpaceRepository extends JpaRepository { // 특정 사용자가 소유한 Space 조회 - @Query("SELECT ms.space FROM MemberSpace ms WHERE ms.member.id = :memberId") + @Query("SELECT ms.space FROM MemberSpace ms WHERE ms.member.memberId = :memberId") List findSpacesByMemberId(@Param("memberId") Long memberId); + + Optional findByMember_MemberIdAndSpace_Id(Long memberId, Long spaceId); } diff --git a/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java index a399f97..9c13053 100644 --- a/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java +++ b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java @@ -2,9 +2,15 @@ import com.edison.project.domain.space.entity.Space; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface SpaceRepository extends JpaRepository { + @Query("SELECT s FROM Space s JOIN FETCH s.bubble WHERE s.bubble.bubbleId = :bubbleId AND s.memberId = :memberId") + List findByBubble_BubbleIdAndMemberId(@Param("bubbleId") Long bubbleId, @Param("memberId") Long memberId); } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java index 00a58b7..347c251 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java @@ -6,5 +6,6 @@ import org.springframework.http.ResponseEntity; public interface SpaceService { + ResponseEntity processSpaces(CustomUserPrincipal userPrincipal, Pageable pageable); } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index ed903c2..62f67ce 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -30,9 +30,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import okhttp3.*; -import java.util.*; - @Service @Transactional public class SpaceServiceImpl implements SpaceService { @@ -96,17 +93,18 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip List newSpaces = parseGptResponse(gptResponse, bubbles, memberId); System.out.println("✅ 변환된 Space 개수: " + newSpaces.size()); - // ✅ 새로운 Space를 저장하고 MemberSpace와 연결 + // ✅ 새로운 Space를 저장하고 MemberSpace도 업데이트 for (Space space : newSpaces) { - saveSpaceAndMemberSpace(space, memberId); + saveOrUpdateSpaceWithMemberSpace(space); } + // ✅ 기존 Space + 새로운 Space 반환 spaces.addAll(newSpaces); List spaceDtos = spaces.stream() .map(space -> new SpaceResponseDto( - space.getId(), + space.getBubble(), // ✅ Bubble 객체 전달 space.getContent(), space.getX(), space.getY(), @@ -114,16 +112,47 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip )) .collect(Collectors.toList()); + return ApiResponse.onSuccess(SuccessStatus._OK, pageInfo, spaceDtos); } - // ✅ Space와 MemberSpace 저장 및 즉시 반영 @Transactional - public void saveSpaceAndMemberSpace(Space space, Long memberId) { - spaceRepository.save(space); - spaceRepository.flush(); // ✅ 즉시 반영 - System.out.println("💾 저장된 Space ID: " + space.getId()); + public void saveOrUpdateSpaceWithMemberSpace(Space newSpace) { + List existingSpaces = spaceRepository.findByBubble_BubbleIdAndMemberId( + newSpace.getBubble().getBubbleId(), newSpace.getMemberId()); + + if (existingSpaces.isEmpty()) { + // ✅ 새로운 Space 저장 + spaceRepository.save(newSpace); + spaceRepository.flush(); + System.out.println("🆕 새로운 Space 추가! ID: " + newSpace.getId()); + + // ✅ MemberSpace 추가 + saveMemberSpace(newSpace.getMemberId(), newSpace); + } else { + // ✅ 여러 개의 Space가 존재할 경우, 가장 오래된 데이터만 남기고 나머지는 삭제 + Space spaceToUpdate = existingSpaces.get(0); // 첫 번째 요소 사용 + for (int i = 1; i < existingSpaces.size(); i++) { + spaceRepository.delete(existingSpaces.get(i)); // 나머지 삭제 + } + // ✅ 기존 Space 업데이트 + spaceToUpdate.setX(newSpace.getX()); + spaceToUpdate.setY(newSpace.getY()); + spaceToUpdate.setGroupNames(newSpace.getGroupNames()); + spaceToUpdate.setContent(newSpace.getContent()); + spaceRepository.save(spaceToUpdate); + spaceRepository.flush(); + System.out.println("🔄 기존 Space 업데이트 완료! ID: " + spaceToUpdate.getId()); + + // ✅ MemberSpace 업데이트 (기존 연결 유지) + updateMemberSpace(newSpace.getMemberId(), spaceToUpdate); + } + } + + + // ✅ MemberSpace 저장 + private void saveMemberSpace(Long memberId, Space space) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); @@ -131,10 +160,21 @@ public void saveSpaceAndMemberSpace(Space space, Long memberId) { memberSpace.setMember(member); memberSpace.setSpace(space); memberSpaceRepository.save(memberSpace); - memberSpaceRepository.flush(); // ✅ 즉시 반영 - System.out.println("🔗 MemberSpace 연결됨: " + memberSpace.getMember().getMemberId() + " -> " + memberSpace.getSpace().getId()); + memberSpaceRepository.flush(); + System.out.println("🔗 MemberSpace 연결됨: " + memberId + " -> " + space.getId()); } + // ✅ MemberSpace 업데이트 + private void updateMemberSpace(Long memberId, Space space) { + Optional existingMemberSpace = memberSpaceRepository.findByMember_MemberIdAndSpace_Id(memberId, space.getId()); + + if (existingMemberSpace.isEmpty()) { + saveMemberSpace(memberId, space); + } + } + + + // ✅ Bubble 데이터를 GPT 요청 형식으로 변환 private Map createRequestDataWithId(List bubbles) { return bubbles.stream().collect(Collectors.toMap( @@ -223,10 +263,14 @@ private List parseGptResponse(String gptResponse, List bubbles, L // ✅ JSON이 ```json ... ``` 형태일 경우 제거 contentJson = contentJson.replaceAll("```json", "").replaceAll("```", "").trim(); - // ✅ 5. 문자열을 다시 Map으로 변환 + // ✅ 5. 문자열을 Map으로 변환 Map parsedContent = objectMapper.readValue(contentJson, new TypeReference>() {}); - // ✅ 6. "items" 필드 확인 및 리스트 변환 + // ✅ 6. "items" 필드 확인 후 리스트로 변환 + if (!parsedContent.containsKey("items")) { + throw new RuntimeException("'items' 필드가 존재하지 않습니다."); + } + List> parsedData = (List>) parsedContent.get("items"); if (parsedData == null || parsedData.isEmpty()) { throw new RuntimeException("'items' 필드가 비어 있음"); @@ -262,11 +306,24 @@ private List parseGptResponse(String gptResponse, List bubbles, L } + private String extractKeywords(String content) { + if (content == null || content.isEmpty()) return "N/A"; + + // 공백으로 단어 분리 + String[] words = content.split("\\s+"); + + // 1~2개 핵심 키워드만 추출 + int keywordCount = Math.min(words.length, 2); + return String.join(" ", Arrays.copyOfRange(words, 0, keywordCount)); + } + + // ✅ GPT 요청 프롬프트 생성 private String buildPromptWithId(Map requestData) { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); + promptBuilder.append("Ensure that ALL provided bubbles are assigned unique coordinates."); promptBuilder.append("Each item should have the following attributes:\n"); promptBuilder.append("- id: A unique identifier for the item (integer).\n"); promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); @@ -293,13 +350,7 @@ private String buildPromptWithId(Map requestData) { return promptBuilder.toString(); } - private OkHttpClient createHttpClientWithTimeout() { - return new OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) // 연결 타임아웃 설정 - .writeTimeout(30, TimeUnit.SECONDS) // 쓰기 타임아웃 설정 - .readTimeout(60, TimeUnit.SECONDS) // 읽기 타임아웃 설정 - .build(); - } + } From 8e22b8c42aa91452572433f06db20dcc8f551ff4 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Sat, 1 Feb 2025 22:42:42 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20db=20=EC=8B=B1=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/service/SpaceServiceImpl.java | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 62f67ce..a69177d 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -118,41 +118,39 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip @Transactional public void saveOrUpdateSpaceWithMemberSpace(Space newSpace) { + // 🔍 기존 Space 조회 List existingSpaces = spaceRepository.findByBubble_BubbleIdAndMemberId( - newSpace.getBubble().getBubbleId(), newSpace.getMemberId()); + newSpace.getBubble().getBubbleId(), newSpace.getMemberId() + ); - if (existingSpaces.isEmpty()) { - // ✅ 새로운 Space 저장 - spaceRepository.save(newSpace); - spaceRepository.flush(); - System.out.println("🆕 새로운 Space 추가! ID: " + newSpace.getId()); - - // ✅ MemberSpace 추가 - saveMemberSpace(newSpace.getMemberId(), newSpace); - } else { - // ✅ 여러 개의 Space가 존재할 경우, 가장 오래된 데이터만 남기고 나머지는 삭제 - Space spaceToUpdate = existingSpaces.get(0); // 첫 번째 요소 사용 - for (int i = 1; i < existingSpaces.size(); i++) { - spaceRepository.delete(existingSpaces.get(i)); // 나머지 삭제 - } + Optional existingSpace = existingSpaces.stream().findFirst(); // ✅ 첫 번째 항목 가져오기 + if (existingSpace.isPresent()) { // ✅ 기존 Space 업데이트 + Space spaceToUpdate = existingSpace.get(); spaceToUpdate.setX(newSpace.getX()); spaceToUpdate.setY(newSpace.getY()); - spaceToUpdate.setGroupNames(newSpace.getGroupNames()); spaceToUpdate.setContent(newSpace.getContent()); - spaceRepository.save(spaceToUpdate); - spaceRepository.flush(); + spaceRepository.save(spaceToUpdate); // UPDATE 수행 + System.out.println("🔄 기존 Space 업데이트 완료! ID: " + spaceToUpdate.getId()); // ✅ MemberSpace 업데이트 (기존 연결 유지) updateMemberSpace(newSpace.getMemberId(), spaceToUpdate); + } else { + // ✅ 새로운 Space 저장 + spaceRepository.save(newSpace); + spaceRepository.flush(); + System.out.println("🆕 새로운 Space 추가! ID: " + newSpace.getId()); + + // ✅ MemberSpace 추가 + saveMemberSpace(newSpace.getMemberId(), newSpace); } } - // ✅ MemberSpace 저장 - private void saveMemberSpace(Long memberId, Space space) { + @Transactional + public void saveMemberSpace(Long memberId, Space space) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); @@ -160,19 +158,23 @@ private void saveMemberSpace(Long memberId, Space space) { memberSpace.setMember(member); memberSpace.setSpace(space); memberSpaceRepository.save(memberSpace); - memberSpaceRepository.flush(); - System.out.println("🔗 MemberSpace 연결됨: " + memberId + " -> " + space.getId()); + memberSpaceRepository.flush(); // 즉시 반영 + + System.out.println("🔗 MemberSpace 저장 완료: Member ID " + memberId + " -> Space ID " + space.getId()); } - // ✅ MemberSpace 업데이트 - private void updateMemberSpace(Long memberId, Space space) { - Optional existingMemberSpace = memberSpaceRepository.findByMember_MemberIdAndSpace_Id(memberId, space.getId()); + @Transactional + public void updateMemberSpace(Long memberId, Space space) { + Optional optionalMemberSpace = memberSpaceRepository.findByMember_MemberIdAndSpace_Id(memberId, space.getId()); - if (existingMemberSpace.isEmpty()) { - saveMemberSpace(memberId, space); + if (optionalMemberSpace.isPresent()) { + System.out.println("✅ MemberSpace는 이미 존재함: Member ID " + memberId + " -> Space ID " + space.getId()); + return; // 이미 연결이 존재하므로 추가 처리 필요 없음 } - } + // 새로운 MemberSpace 저장 + saveMemberSpace(memberId, space); + } // ✅ Bubble 데이터를 GPT 요청 형식으로 변환 From e2272eba3342a3107a52bb229b3fcb2924165ad7 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 5 Feb 2025 10:17:46 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95,=20=EB=B2=84=EB=B8=94=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=B2=98=EB=A6=AC=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EC=B5=9C=EB=8C=80=20=EA=B0=9C=EC=88=98=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/service/SpaceServiceImpl.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index a69177d..54328c0 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -15,6 +15,7 @@ import com.edison.project.domain.bubble.entity.Bubble; import com.edison.project.domain.bubble.repository.BubbleRepository; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import com.edison.project.global.security.CustomUserPrincipal; @@ -63,7 +64,9 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip System.out.println("📌 기존 사용자의 Space 개수: " + spaces.size()); // ✅ 사용자의 삭제되지 않은 Bubble 페이징 처리 - Page bubblePage = bubbleRepository.findByMember_MemberIdAndIsDeletedFalse(memberId, pageable); + Pageable unlimitedPageable = PageRequest.of(0, Integer.MAX_VALUE); // 최대 개수 가져오기 + Page bubblePage = bubbleRepository.findByMember_MemberIdAndIsDeletedFalse(memberId, unlimitedPageable); + // 페이징 처리 삭제, 가져올 수 있는 최대 개수만큼 반환하는 코드 추가 // ✅ Page 정보 설정 PageInfo pageInfo = new PageInfo( @@ -324,25 +327,27 @@ private String extractKeywords(String content) { // ✅ GPT 요청 프롬프트 생성 private String buildPromptWithId(Map requestData) { StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append("You are tasked with categorizing content items and positioning them on a 2D grid.\n"); - promptBuilder.append("Ensure that ALL provided bubbles are assigned unique coordinates."); + promptBuilder.append("Ensure that ALL provided bubbles are assigned unique coordinates, distributed evenly across four quadrants centered at (0,0).\n"); promptBuilder.append("Each item should have the following attributes:\n"); promptBuilder.append("- id: A unique identifier for the item (integer).\n"); promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); - promptBuilder.append("- x: A unique floating-point number for the x-coordinate.\n"); - promptBuilder.append("- y: A unique floating-point number for the y-coordinate.\n"); + promptBuilder.append("- x: A unique floating-point number for the x-coordinate (spread across four quadrants).\n"); + promptBuilder.append("- y: A unique floating-point number for the y-coordinate (spread across four quadrants).\n"); promptBuilder.append("- groups: A list of integers representing the item's group IDs.\n\n"); promptBuilder.append("### Rules:\n"); - promptBuilder.append("1. Each item must have a unique (x, y) coordinate.\n"); - promptBuilder.append("2. Items with similar topics should be clustered like a firework explosion, forming visually distinct groups.\n"); - promptBuilder.append("3. Groups should be separated from each other while maintaining internal coherence.\n"); - promptBuilder.append("4. The spread or distance for groups is up to you to decide, but they should appear like bursts from a central point.\n"); - promptBuilder.append("5. Ensure groups contain only integers, and avoid any other data types.\n"); - promptBuilder.append("6. X and Y coordinates do not need to follow a uniform increase; they can be randomly distributed while maintaining the clustering structure.\n"); - promptBuilder.append("7. Return only valid JSON output in the following format:\n\n"); - promptBuilder.append("8. Clusters can be separate, but items with similar themes should be placed near each other, even if they belong to different clusters.\n"); - promptBuilder.append("9. Content should be reduced to its **core meaning**: extract only **one or two essential words** that best describe it.\n"); + promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); + promptBuilder.append("2. Items with similar topics should form visually distinct clusters, appearing as bursts from a central point.\n"); + promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); + promptBuilder.append("4. Each cluster should contain **5 to 8 items**, and **no cluster should have more than 10 items**.\n"); + promptBuilder.append("5. The number of clusters should be minimized, ideally around **1/4 of the total number of items**.\n"); + promptBuilder.append("6. Items that do not naturally fit into a cluster should remain ungrouped, keeping their original coordinates.\n"); + promptBuilder.append("7. X and Y coordinates should be distributed across all four quadrants for better visualization.\n"); + promptBuilder.append("8. Similar items across different clusters should still be positioned near each other where possible.\n"); + promptBuilder.append("9. Extract the **core meaning** of each content item, reducing it to **1 or 2 essential words**.\n"); + promptBuilder.append("10. The output must be strictly in JSON format as shown below:\n\n"); for (Map.Entry entry : requestData.entrySet()) { promptBuilder.append("- ID: ").append(entry.getKey()).append("\n"); From c8185299d0b1652cf7d2d614a7f303f4798936ed Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 5 Feb 2025 10:25:14 +0900 Subject: [PATCH 11/19] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/service/SpaceServiceImpl.java | 26 +------------------ .../src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 54328c0..f202566 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -230,16 +230,6 @@ private String callGPTForGrouping(Map requestData) { throw new RuntimeException("OpenAI API 호출 중 오류 발생: " + e.getMessage(), e); } } - // ✅ GPT 응답 데이터 정리 - private String sanitizeResponse(String response) { - try { - if (response == null || response.isBlank()) throw new RuntimeException("GPT 응답이 비어 있습니다."); - objectMapper.readTree(response); - return response.trim(); - } catch (IOException e) { - throw new RuntimeException("GPT 응답 처리 중 오류 발생: " + e.getMessage(), e); - } - } private List parseGptResponse(String gptResponse, List bubbles, Long memberId) { try { @@ -310,20 +300,6 @@ private List parseGptResponse(String gptResponse, List bubbles, L } } - - private String extractKeywords(String content) { - if (content == null || content.isEmpty()) return "N/A"; - - // 공백으로 단어 분리 - String[] words = content.split("\\s+"); - - // 1~2개 핵심 키워드만 추출 - int keywordCount = Math.min(words.length, 2); - return String.join(" ", Arrays.copyOfRange(words, 0, keywordCount)); - } - - - // ✅ GPT 요청 프롬프트 생성 private String buildPromptWithId(Map requestData) { StringBuilder promptBuilder = new StringBuilder(); @@ -340,7 +316,7 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); promptBuilder.append("2. Items with similar topics should form visually distinct clusters, appearing as bursts from a central point.\n"); - promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); + promptBuilder.append("3. Groups = Clusters, should be well-separated from each other but internally cohesive.\n"); promptBuilder.append("4. Each cluster should contain **5 to 8 items**, and **no cluster should have more than 10 items**.\n"); promptBuilder.append("5. The number of clusters should be minimized, ideally around **1/4 of the total number of items**.\n"); promptBuilder.append("6. Items that do not naturally fit into a cluster should remain ungrouped, keeping their original coordinates.\n"); diff --git a/project/src/main/resources/application.properties b/project/src/main/resources/application.properties index ab7451c..d811fbe 100644 --- a/project/src/main/resources/application.properties +++ b/project/src/main/resources/application.properties @@ -13,7 +13,7 @@ spring.jpa.properties.hibernate.format_sql=true spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect # JPA Console Log -logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.SQL=INFO logging.level.org.hibernate.type.descriptor.sql=TRACE # Google OAuth2 From 56c4910ad46863d6d3dbf99676a647ac1cd1ee91 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 5 Feb 2025 10:52:36 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20is=5Fdeleted=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/edison/project/domain/bubble/entity/Bubble.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java b/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java index 7c349ee..5791250 100644 --- a/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java +++ b/project/src/main/java/com/edison/project/domain/bubble/entity/Bubble.java @@ -36,6 +36,9 @@ public class Bubble { @Column(name = "main_img", length = 2083) private String mainImg; + @Column(name = "is_deleted", nullable = false) //휴지통에도 없는! + private boolean isDeleted = false; + @Column(name = "is_trashed", nullable = false) //휴지통에 있는 지(soft_delete) private boolean isTrashed = false; From 4a2ec117e85f0b5438ddd3de0e1231c69d14365a Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 5 Feb 2025 11:28:02 +0900 Subject: [PATCH 13/19] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0-memverspace=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/controller/SpaceController.java | 10 ++- .../domain/space/entity/MemberSpace.java | 53 ------------ .../project/domain/space/entity/Space.java | 2 +- .../repository/MemberSpaceRepository.java | 21 ----- .../space/repository/SpaceRepository.java | 4 + .../space/service/SpaceServiceImpl.java | 82 ++++++------------- 6 files changed, 36 insertions(+), 136 deletions(-) delete mode 100644 project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java delete mode 100644 project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java diff --git a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java index e6c9f43..e883377 100644 --- a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java +++ b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -10,7 +10,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -25,11 +24,16 @@ public SpaceController(SpaceService spaceService) { this.spaceService = spaceService; } - @GetMapping - public ResponseEntity getSpaces( + @GetMapping("/convert") // 스페이스로 변환 + public ResponseEntity convertSpaces( @AuthenticationPrincipal CustomUserPrincipal userPrincipal, Pageable pageable) { ResponseEntity response = spaceService.processSpaces(userPrincipal, pageable); List spaces = (List) response.getBody().getResult(); return ApiResponse.onSuccess(SuccessStatus._OK, spaces); } + + @GetMapping("/cluster") // 클러스터의 중심, 반지름 반환 + public ResponseEntity clusterSpaces(){ + return ApiResponse.onSuccess(SuccessStatus._OK); + } } diff --git a/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java b/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java deleted file mode 100644 index 155c5c5..0000000 --- a/project/src/main/java/com/edison/project/domain/space/entity/MemberSpace.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.edison.project.domain.space.entity; - -import com.edison.project.domain.member.entity.Member; -import jakarta.persistence.*; - -@Entity -@Table(name = "member_space") -public class MemberSpace { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "space_id", nullable = false) - private Space space; - - public MemberSpace() {} - - public MemberSpace(Member member, Space space) { - this.member = member; - this.space = space; - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Member getMember() { - return member; - } - - public void setMember(Member member) { - this.member = member; - } - - public Space getSpace() { - return space; - } - - public void setSpace(Space space) { - this.space = space; - } -} diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index a68e6a7..dc8911d 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -22,7 +22,7 @@ public class Space { private Bubble bubble; @ElementCollection - @CollectionTable(name = "space_groups", joinColumns = @JoinColumn(name = "space_id")) + // @CollectionTable(name = "space_groups", joinColumns = @JoinColumn(name = "space_id")) @Column(name = "group_names") // ✅ 예약어 문제 해결 (`groups` → `group_names`) private List groupNames; diff --git a/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java deleted file mode 100644 index e319f05..0000000 --- a/project/src/main/java/com/edison/project/domain/space/repository/MemberSpaceRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.edison.project.domain.space.repository; - -import com.edison.project.domain.space.entity.MemberSpace; -import com.edison.project.domain.space.entity.Space; -import io.lettuce.core.dynamic.annotation.Param; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface MemberSpaceRepository extends JpaRepository { - - // 특정 사용자가 소유한 Space 조회 - @Query("SELECT ms.space FROM MemberSpace ms WHERE ms.member.memberId = :memberId") - List findSpacesByMemberId(@Param("memberId") Long memberId); - - Optional findByMember_MemberIdAndSpace_Id(Long memberId, Long spaceId); -} diff --git a/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java index 9c13053..bcfab9a 100644 --- a/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java +++ b/project/src/main/java/com/edison/project/domain/space/repository/SpaceRepository.java @@ -12,5 +12,9 @@ public interface SpaceRepository extends JpaRepository { @Query("SELECT s FROM Space s JOIN FETCH s.bubble WHERE s.bubble.bubbleId = :bubbleId AND s.memberId = :memberId") List findByBubble_BubbleIdAndMemberId(@Param("bubbleId") Long bubbleId, @Param("memberId") Long memberId); + + @Query("SELECT s FROM Space s WHERE s.memberId = :memberId") + List findByMemberId(@Param("memberId") Long memberId); + } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 65e81af..c9b1092 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -1,16 +1,12 @@ package com.edison.project.domain.space.service; -import com.edison.project.common.exception.GeneralException; import com.edison.project.common.response.ApiResponse; import com.edison.project.common.response.PageInfo; import com.edison.project.common.status.ErrorStatus; import com.edison.project.common.status.SuccessStatus; -import com.edison.project.domain.member.entity.Member; import com.edison.project.domain.member.repository.MemberRepository; import com.edison.project.domain.space.dto.SpaceResponseDto; -import com.edison.project.domain.space.entity.MemberSpace; import com.edison.project.domain.space.entity.Space; -import com.edison.project.domain.space.repository.MemberSpaceRepository; import com.edison.project.domain.space.repository.SpaceRepository; import com.edison.project.domain.bubble.entity.Bubble; import com.edison.project.domain.bubble.repository.BubbleRepository; @@ -38,16 +34,13 @@ public class SpaceServiceImpl implements SpaceService { private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; private final SpaceRepository spaceRepository; - private final MemberSpaceRepository memberSpaceRepository; private final BubbleRepository bubbleRepository; private final ObjectMapper objectMapper = new ObjectMapper(); private final MemberRepository memberRepository; public SpaceServiceImpl(SpaceRepository spaceRepository, - MemberSpaceRepository memberSpaceRepository, BubbleRepository bubbleRepository, MemberRepository memberRepository) { this.spaceRepository = spaceRepository; - this.memberSpaceRepository = memberSpaceRepository; this.bubbleRepository = bubbleRepository; this.memberRepository = memberRepository; } @@ -60,7 +53,7 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip System.out.println("🔍 [Process Spaces] 실행 - 사용자 ID: " + memberId); // ✅ 기존 사용자의 Space 가져오기 - List spaces = memberSpaceRepository.findSpacesByMemberId(memberId); + List spaces = spaceRepository.findByMemberId(memberId); System.out.println("📌 기존 사용자의 Space 개수: " + spaces.size()); // ✅ 사용자의 삭제되지 않은 Bubble 페이징 처리 @@ -96,9 +89,9 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip List newSpaces = parseGptResponse(gptResponse, bubbles, memberId); System.out.println("✅ 변환된 Space 개수: " + newSpaces.size()); - // ✅ 새로운 Space를 저장하고 MemberSpace도 업데이트 + // ✅ 새로운 Space 업데이트 for (Space space : newSpaces) { - saveOrUpdateSpaceWithMemberSpace(space); + saveOrUpdateSpace(space); } @@ -120,65 +113,28 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip } @Transactional - public void saveOrUpdateSpaceWithMemberSpace(Space newSpace) { - // 🔍 기존 Space 조회 + public void saveOrUpdateSpace(Space newSpace) { List existingSpaces = spaceRepository.findByBubble_BubbleIdAndMemberId( newSpace.getBubble().getBubbleId(), newSpace.getMemberId() ); - Optional existingSpace = existingSpaces.stream().findFirst(); // ✅ 첫 번째 항목 가져오기 + Optional existingSpace = existingSpaces.stream().findFirst(); if (existingSpace.isPresent()) { - // ✅ 기존 Space 업데이트 Space spaceToUpdate = existingSpace.get(); spaceToUpdate.setX(newSpace.getX()); spaceToUpdate.setY(newSpace.getY()); spaceToUpdate.setContent(newSpace.getContent()); - spaceRepository.save(spaceToUpdate); // UPDATE 수행 - + spaceRepository.save(spaceToUpdate); System.out.println("🔄 기존 Space 업데이트 완료! ID: " + spaceToUpdate.getId()); - - // ✅ MemberSpace 업데이트 (기존 연결 유지) - updateMemberSpace(newSpace.getMemberId(), spaceToUpdate); } else { - // ✅ 새로운 Space 저장 spaceRepository.save(newSpace); spaceRepository.flush(); System.out.println("🆕 새로운 Space 추가! ID: " + newSpace.getId()); - - // ✅ MemberSpace 추가 - saveMemberSpace(newSpace.getMemberId(), newSpace); } } - @Transactional - public void saveMemberSpace(Long memberId, Space space) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - - MemberSpace memberSpace = new MemberSpace(); - memberSpace.setMember(member); - memberSpace.setSpace(space); - memberSpaceRepository.save(memberSpace); - memberSpaceRepository.flush(); // 즉시 반영 - - System.out.println("🔗 MemberSpace 저장 완료: Member ID " + memberId + " -> Space ID " + space.getId()); - } - - @Transactional - public void updateMemberSpace(Long memberId, Space space) { - Optional optionalMemberSpace = memberSpaceRepository.findByMember_MemberIdAndSpace_Id(memberId, space.getId()); - - if (optionalMemberSpace.isPresent()) { - System.out.println("✅ MemberSpace는 이미 존재함: Member ID " + memberId + " -> Space ID " + space.getId()); - return; // 이미 연결이 존재하므로 추가 처리 필요 없음 - } - - // 새로운 MemberSpace 저장 - saveMemberSpace(memberId, space); - } - // ✅ Bubble 데이터를 GPT 요청 형식으로 변환 private Map createRequestDataWithId(List bubbles) { @@ -287,9 +243,19 @@ private List parseGptResponse(String gptResponse, List bubbles, L String content = (String) item.get("content"); double x = ((Number) item.get("x")).doubleValue(); double y = ((Number) item.get("y")).doubleValue(); - List groups = ((List) item.get("groups")).stream() + + List groups = item.containsKey("groups") && item.get("groups") != null + ? ((List) item.get("groups")).stream() .map(Object::toString) - .collect(Collectors.toList()); + .collect(Collectors.toList()) + : new ArrayList<>(); // ✅ 빈 리스트 할당 + + // ✅ 여기서 추가적으로 체크: 만약 groups가 비어 있다면 "UNASSIGNED" 추가 + if (groups.isEmpty()) { + groups.add("UNASSIGNED"); + } + + spaces.add(new Space(content, x, y, groups, bubble, memberId)); } @@ -316,16 +282,16 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); promptBuilder.append("2. Items with similar topics should form visually distinct clusters, appearing as bursts from a central point.\n"); - promptBuilder.append("3. Groups = Clusters, should be well-separated from each other but internally cohesive.\n"); - promptBuilder.append("4. Each cluster should contain **5 to 8 items**, and **no cluster should have more than 10 items**.\n"); - promptBuilder.append("5. The number of clusters should be minimized, ideally around **1/4 of the total number of items**.\n"); + promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); + promptBuilder.append("4. Each group should contain **5 to 8 items**, and **no group should have more than 10 items**.\n"); + promptBuilder.append("5. The number of group should be minimized, ideally around **1/4 of the total number of items**.\n"); promptBuilder.append("6. Items that do not naturally fit into a cluster should remain ungrouped, keeping their original coordinates.\n"); promptBuilder.append("7. X and Y coordinates should be distributed across all four quadrants for better visualization.\n"); promptBuilder.append("8. Similar items across different clusters should still be positioned near each other where possible.\n"); promptBuilder.append("9. Extract the **core meaning** of each content item, reducing it to **1 or 2 essential words**.\n"); - promptBuilder.append("10. The output must be strictly in JSON format as shown below:\n\n"); - promptBuilder.append("- groups: A list of **integer group IDs** representing the item's cluster (must not be empty).\n\n"); - + promptBuilder.append("10. Each item **MUST belong to at least one group**. If an item does not fit into any existing group, create a new unique group ID for it.\n"); + promptBuilder.append("11. The output must strictly include the `groups` field for all items, even if they belong to a single group.\n"); + promptBuilder.append("12. The output must be strictly in JSON format as shown below:\n\n"); for (Map.Entry entry : requestData.entrySet()) { promptBuilder.append("- ID: ").append(entry.getKey()).append("\n"); From 87f101664baaf9a8ac02f119262e339243c41023 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 12 Feb 2025 22:51:24 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20space=20cluster=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B0=98=ED=99=98=20api=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/common/status/ErrorStatus.java | 3 +- .../space/controller/SpaceController.java | 7 ++- .../space/dto/SpaceInfoResponseDto.java | 29 ++++++++++ .../domain/space/dto/SpaceResponseDto.java | 20 +++---- .../project/domain/space/entity/Space.java | 18 +++---- .../domain/space/service/SpaceService.java | 1 + .../space/service/SpaceServiceImpl.java | 54 +++++++++++++------ .../project/global/config/GptConfig.java | 4 +- 8 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java diff --git a/project/src/main/java/com/edison/project/common/status/ErrorStatus.java b/project/src/main/java/com/edison/project/common/status/ErrorStatus.java index e03bb22..ae6ed9b 100644 --- a/project/src/main/java/com/edison/project/common/status/ErrorStatus.java +++ b/project/src/main/java/com/edison/project/common/status/ErrorStatus.java @@ -76,7 +76,8 @@ public enum ErrorStatus { INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "SEARCH4001", "검색어는 공백일 수 없습니다."), // 스페이스 관련 에러 - NO_BUBBLES_FOUND(HttpStatus.BAD_REQUEST,"SPACE4001", "작성된 버블이 없습니다."); + NO_BUBBLES_FOUND(HttpStatus.BAD_REQUEST,"SPACE4001", "작성된 버블이 없습니다."), + NO_SPACES_FOUND(HttpStatus.BAD_REQUEST, "SPACE4002", "스페이스를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java index e883377..9978119 100644 --- a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java +++ b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -2,6 +2,7 @@ import com.edison.project.common.response.ApiResponse; import com.edison.project.common.status.SuccessStatus; +import com.edison.project.domain.space.dto.SpaceInfoResponseDto; import com.edison.project.domain.space.dto.SpaceResponseDto; import com.edison.project.domain.space.service.SpaceService; import com.edison.project.global.security.CustomUserPrincipal; @@ -33,7 +34,9 @@ public ResponseEntity convertSpaces( } @GetMapping("/cluster") // 클러스터의 중심, 반지름 반환 - public ResponseEntity clusterSpaces(){ - return ApiResponse.onSuccess(SuccessStatus._OK); + public ResponseEntity clusterSpaces() { + ResponseEntity response = spaceService.getSpaceInfo(); + SpaceInfoResponseDto spaceInfo = (SpaceInfoResponseDto) response.getBody().getResult(); + return ApiResponse.onSuccess(SuccessStatus._OK, spaceInfo); } } diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java new file mode 100644 index 0000000..667f397 --- /dev/null +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java @@ -0,0 +1,29 @@ +package com.edison.project.domain.space.dto; + +public class SpaceInfoResponseDto { + private double centerX; + private double centerY; + private double radius; + + // 기본 생성자 추가 + public SpaceInfoResponseDto() { + } + + public SpaceInfoResponseDto(double centerX, double centerY, double radius) { + this.centerX = centerX; + this.centerY = centerY; + this.radius = radius; + } + + public double getCenterX() { + return centerX; + } + + public double getCenterY() { + return centerY; + } + + public double getRadius() { + return radius; + } +} diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java index 09ab83a..7e7067e 100644 --- a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java @@ -2,10 +2,6 @@ import com.edison.project.domain.bubble.entity.Bubble; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.ElementCollection; - -import java.util.ArrayList; -import java.util.List; @JsonIgnoreProperties ({"hibernateLazyInitializer", "handler"}) public class SpaceResponseDto { @@ -13,17 +9,15 @@ public class SpaceResponseDto { private String content; private double x; private double y; - - @ElementCollection // JPA 리스트 관리용 - private List groups; + private Integer group = 0; // 올바른 생성자 추가 - public SpaceResponseDto(Bubble bubble, String content, double x, double y, List groups) { + public SpaceResponseDto(Bubble bubble, String content, double x, double y, Integer group) { this.id = bubble.getBubbleId(); this.content = content; this.x = x; this.y = y; - this.groups = new ArrayList<>(groups);; + this.group = group; } // Getters and Setters @@ -59,11 +53,11 @@ public void setY(double y) { this.y = y; } - public List getGroups() { - return groups; + public int getGroup() { + return this.group != null ? this.group : 0; // ✅ null이면 0 반환 } - public void setGroups(List groups) { - this.groups = groups; + public void setGroup(int group) { + this.group = group; } } diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index dc8911d..d78a78a 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -21,10 +21,8 @@ public class Space { @JoinColumn(name = "bubble_id", nullable = false) // 🚨 `NOT NULL` 적용 private Bubble bubble; - @ElementCollection - // @CollectionTable(name = "space_groups", joinColumns = @JoinColumn(name = "space_id")) - @Column(name = "group_names") // ✅ 예약어 문제 해결 (`groups` → `group_names`) - private List groupNames; + @Column(name = "`group`") // ✅ 예약어 처리 + private Integer group; @Column(nullable = false) // member_id 추가 private Long memberId; @@ -33,11 +31,11 @@ public class Space { public Space() {} // ✅ memberId와 Bubble 포함한 생성자 - public Space(String content, double x, double y, List groupNames, Bubble bubble, Long memberId) { + public Space(String content, double x, double y, int group, Bubble bubble, Long memberId) { this.content = content; this.x = x; this.y = y; - this.groupNames = groupNames; + this.group = group; this.bubble = bubble; // ✅ `bubble_id` 설정 this.memberId = memberId; } @@ -79,12 +77,12 @@ public void setBubble(Bubble bubble) { // ✅ Bubble 관련 Setter 추가 this.bubble = bubble; } - public List getGroupNames() { // ✅ 변경된 필드명 반영 - return groupNames; + public int getGroup() { + return this.group != null ? this.group : 0; // ✅ null이면 0 반환 } - public void setGroupNames(List groupNames) { // ✅ Setter 추가 - this.groupNames = groupNames; + public void setGroup(int group) { // ✅ 변경된 필드명 반영 + this.group = group; } public Long getMemberId() { diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java index 347c251..6041e8f 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceService.java @@ -8,4 +8,5 @@ public interface SpaceService { ResponseEntity processSpaces(CustomUserPrincipal userPrincipal, Pageable pageable); + ResponseEntity getSpaceInfo(); } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index c9b1092..87adc95 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -5,6 +5,7 @@ import com.edison.project.common.status.ErrorStatus; import com.edison.project.common.status.SuccessStatus; import com.edison.project.domain.member.repository.MemberRepository; +import com.edison.project.domain.space.dto.SpaceInfoResponseDto; import com.edison.project.domain.space.dto.SpaceResponseDto; import com.edison.project.domain.space.entity.Space; import com.edison.project.domain.space.repository.SpaceRepository; @@ -104,7 +105,7 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip space.getContent(), space.getX(), space.getY(), - space.getGroupNames() + space.getGroup() )) .collect(Collectors.toList()); @@ -243,21 +244,9 @@ private List parseGptResponse(String gptResponse, List bubbles, L String content = (String) item.get("content"); double x = ((Number) item.get("x")).doubleValue(); double y = ((Number) item.get("y")).doubleValue(); + int group = item.get("group") != null ? ((Number) item.get("group")).intValue() : 0; - List groups = item.containsKey("groups") && item.get("groups") != null - ? ((List) item.get("groups")).stream() - .map(Object::toString) - .collect(Collectors.toList()) - : new ArrayList<>(); // ✅ 빈 리스트 할당 - - // ✅ 여기서 추가적으로 체크: 만약 groups가 비어 있다면 "UNASSIGNED" 추가 - if (groups.isEmpty()) { - groups.add("UNASSIGNED"); - } - - - - spaces.add(new Space(content, x, y, groups, bubble, memberId)); + spaces.add(new Space(content, x, y, group, bubble, memberId)); } return spaces; @@ -277,13 +266,13 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); promptBuilder.append("- x: A unique floating-point number for the x-coordinate (spread across four quadrants).\n"); promptBuilder.append("- y: A unique floating-point number for the y-coordinate (spread across four quadrants).\n"); - promptBuilder.append("- groups: A list of integers representing the item's group IDs.\n\n"); + promptBuilder.append("- group: A integer representing the item's group ID.\n\n"); promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); promptBuilder.append("2. Items with similar topics should form visually distinct clusters, appearing as bursts from a central point.\n"); promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); - promptBuilder.append("4. Each group should contain **5 to 8 items**, and **no group should have more than 10 items**.\n"); + promptBuilder.append("4. Each group should contain **5 to 8 items**, and **no group should have more than 10 items**, also group always starts with number 1.\n"); promptBuilder.append("5. The number of group should be minimized, ideally around **1/4 of the total number of items**.\n"); promptBuilder.append("6. Items that do not naturally fit into a cluster should remain ungrouped, keeping their original coordinates.\n"); promptBuilder.append("7. X and Y coordinates should be distributed across all four quadrants for better visualization.\n"); @@ -292,16 +281,47 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("10. Each item **MUST belong to at least one group**. If an item does not fit into any existing group, create a new unique group ID for it.\n"); promptBuilder.append("11. The output must strictly include the `groups` field for all items, even if they belong to a single group.\n"); promptBuilder.append("12. The output must be strictly in JSON format as shown below:\n\n"); + promptBuilder.append("13. **The last provided bubble MUST be positioned at coordinates (0, 0) without exception.**\n"); + promptBuilder.append("14. **The coordinates of all other items MUST be determined considering that the last item is fixed at (0, 0), ensuring proper spacing and distribution.**\n\n"); + + Long lastKey = null; + for (Long key : requestData.keySet()) { + lastKey = key; + } for (Map.Entry entry : requestData.entrySet()) { promptBuilder.append("- ID: ").append(entry.getKey()).append("\n"); promptBuilder.append(entry.getValue()).append("\n"); + + if (entry.getKey().equals(lastKey)) { + promptBuilder.append("(This item MUST be placed at coordinates (0, 0))\n"); + } } return promptBuilder.toString(); } + public ResponseEntity getSpaceInfo() { + List spaces = spaceRepository.findAll(); + + if (spaces.isEmpty()) { + return ApiResponse.onFailure(ErrorStatus.NO_SPACES_FOUND); + } + + double minX = spaces.stream().mapToDouble(Space::getX).min().orElse(0); + double maxX = spaces.stream().mapToDouble(Space::getX).max().orElse(0); + double minY = spaces.stream().mapToDouble(Space::getY).min().orElse(0); + double maxY = spaces.stream().mapToDouble(Space::getY).max().orElse(0); + double centerX = (minX + maxX) / 2; + double centerY = (minY + maxY) / 2; + + double radius = Math.sqrt(Math.pow(maxX - centerX, 2) + Math.pow(maxY - centerY, 2)); + + SpaceInfoResponseDto response = new SpaceInfoResponseDto(centerX, centerY, radius); + + return ApiResponse.onSuccess(SuccessStatus._OK, response); + } } diff --git a/project/src/main/java/com/edison/project/global/config/GptConfig.java b/project/src/main/java/com/edison/project/global/config/GptConfig.java index 0d4e1d3..883746d 100644 --- a/project/src/main/java/com/edison/project/global/config/GptConfig.java +++ b/project/src/main/java/com/edison/project/global/config/GptConfig.java @@ -23,8 +23,8 @@ public String getModel() { return model; } - @Bean - public RestTemplate restTemplate() { + @Bean(name = "gptRestTemplate") + public RestTemplate gptRestTemplate() { return new RestTemplate(); } From 584e60bb8fcf448ce4ea7679805d550fe58c3ac3 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 12 Feb 2025 22:58:03 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20group=20=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=20=EC=A4=91=EC=8B=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=98=EC=A7=80=EB=A6=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/dto/SpaceInfoResponseDto.java | 10 +++++-- .../space/service/SpaceServiceImpl.java | 28 +++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java index 667f397..899d969 100644 --- a/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceInfoResponseDto.java @@ -1,6 +1,7 @@ package com.edison.project.domain.space.dto; public class SpaceInfoResponseDto { + private int groupId; private double centerX; private double centerY; private double radius; @@ -9,12 +10,17 @@ public class SpaceInfoResponseDto { public SpaceInfoResponseDto() { } - public SpaceInfoResponseDto(double centerX, double centerY, double radius) { + public SpaceInfoResponseDto(int groupId, double centerX, double centerY, double radius) { + this.groupId = groupId; this.centerX = centerX; this.centerY = centerY; this.radius = radius; } + public int getGroupId() { + return groupId; + } + public double getCenterX() { return centerX; } @@ -26,4 +32,4 @@ public double getCenterY() { public double getRadius() { return radius; } -} +} \ No newline at end of file diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 87adc95..f929aa4 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -308,19 +308,29 @@ public ResponseEntity getSpaceInfo() { return ApiResponse.onFailure(ErrorStatus.NO_SPACES_FOUND); } - double minX = spaces.stream().mapToDouble(Space::getX).min().orElse(0); - double maxX = spaces.stream().mapToDouble(Space::getX).max().orElse(0); - double minY = spaces.stream().mapToDouble(Space::getY).min().orElse(0); - double maxY = spaces.stream().mapToDouble(Space::getY).max().orElse(0); + Map> groupedSpaces = spaces.stream() + .collect(Collectors.groupingBy(Space::getGroup)); - double centerX = (minX + maxX) / 2; - double centerY = (minY + maxY) / 2; + List responseList = new ArrayList<>(); - double radius = Math.sqrt(Math.pow(maxX - centerX, 2) + Math.pow(maxY - centerY, 2)); + for (Map.Entry> entry : groupedSpaces.entrySet()) { + int groupId = entry.getKey(); + List groupSpaces = entry.getValue(); - SpaceInfoResponseDto response = new SpaceInfoResponseDto(centerX, centerY, radius); + double minX = groupSpaces.stream().mapToDouble(Space::getX).min().orElse(0); + double maxX = groupSpaces.stream().mapToDouble(Space::getX).max().orElse(0); + double minY = groupSpaces.stream().mapToDouble(Space::getY).min().orElse(0); + double maxY = groupSpaces.stream().mapToDouble(Space::getY).max().orElse(0); - return ApiResponse.onSuccess(SuccessStatus._OK, response); + double centerX = (minX + maxX) / 2; + double centerY = (minY + maxY) / 2; + + double radius = Math.sqrt(Math.pow(maxX - centerX, 2) + Math.pow(maxY - centerY, 2)); + + responseList.add(new SpaceInfoResponseDto(groupId, centerX, centerY, radius)); + } + + return ApiResponse.onSuccess(SuccessStatus._OK, responseList); } } From bf21e0acce204c059e5828012b7fd52040bdbf99 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Wed, 12 Feb 2025 23:18:48 +0900 Subject: [PATCH 16/19] =?UTF-8?q?feat:=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/controller/SpaceController.java | 5 +++-- .../domain/space/service/SpaceServiceImpl.java | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java index 9978119..57a931f 100644 --- a/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java +++ b/project/src/main/java/com/edison/project/domain/space/controller/SpaceController.java @@ -36,7 +36,8 @@ public ResponseEntity convertSpaces( @GetMapping("/cluster") // 클러스터의 중심, 반지름 반환 public ResponseEntity clusterSpaces() { ResponseEntity response = spaceService.getSpaceInfo(); - SpaceInfoResponseDto spaceInfo = (SpaceInfoResponseDto) response.getBody().getResult(); - return ApiResponse.onSuccess(SuccessStatus._OK, spaceInfo); + List spaceInfoList = (List) response.getBody().getResult(); + return ApiResponse.onSuccess(SuccessStatus._OK, spaceInfoList); } + } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index f929aa4..106840b 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -303,6 +303,7 @@ private String buildPromptWithId(Map requestData) { public ResponseEntity getSpaceInfo() { List spaces = spaceRepository.findAll(); + System.out.println("Fetched Spaces: " + spaces.size()); if (spaces.isEmpty()) { return ApiResponse.onFailure(ErrorStatus.NO_SPACES_FOUND); @@ -310,12 +311,20 @@ public ResponseEntity getSpaceInfo() { Map> groupedSpaces = spaces.stream() .collect(Collectors.groupingBy(Space::getGroup)); + System.out.println("Grouped Spaces: " + groupedSpaces.keySet()); + + int maxGroupId = groupedSpaces.keySet().stream().max(Integer::compareTo).orElse(0); + System.out.println("Max Group ID: " + maxGroupId); List responseList = new ArrayList<>(); - for (Map.Entry> entry : groupedSpaces.entrySet()) { - int groupId = entry.getKey(); - List groupSpaces = entry.getValue(); + for (int groupId = 1; groupId <= maxGroupId; groupId++) { + List groupSpaces = groupedSpaces.getOrDefault(groupId, new ArrayList<>()); + System.out.println("Processing Group ID: " + groupId + ", Spaces: " + groupSpaces.size()); + + if (groupSpaces.isEmpty()) { + continue; + } double minX = groupSpaces.stream().mapToDouble(Space::getX).min().orElse(0); double maxX = groupSpaces.stream().mapToDouble(Space::getX).max().orElse(0); @@ -330,8 +339,10 @@ public ResponseEntity getSpaceInfo() { responseList.add(new SpaceInfoResponseDto(groupId, centerX, centerY, radius)); } + System.out.println("Response List Size: " + responseList.size()); return ApiResponse.onSuccess(SuccessStatus._OK, responseList); } + } From 286a35a388191e4ae191b940108a55784b39b06a Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Thu, 13 Feb 2025 00:15:48 +0900 Subject: [PATCH 17/19] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/domain/space/entity/Space.java | 2 +- .../space/service/SpaceServiceImpl.java | 70 +++++++++---------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/entity/Space.java b/project/src/main/java/com/edison/project/domain/space/entity/Space.java index d78a78a..8f34db0 100644 --- a/project/src/main/java/com/edison/project/domain/space/entity/Space.java +++ b/project/src/main/java/com/edison/project/domain/space/entity/Space.java @@ -21,7 +21,7 @@ public class Space { @JoinColumn(name = "bubble_id", nullable = false) // 🚨 `NOT NULL` 적용 private Bubble bubble; - @Column(name = "`group`") // ✅ 예약어 처리 + @Column(name = "group_id") private Integer group; @Column(nullable = false) // member_id 추가 diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 106840b..c004789 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -126,6 +126,7 @@ public void saveOrUpdateSpace(Space newSpace) { spaceToUpdate.setX(newSpace.getX()); spaceToUpdate.setY(newSpace.getY()); spaceToUpdate.setContent(newSpace.getContent()); + spaceToUpdate.setGroup(newSpace.getGroup()); spaceRepository.save(spaceToUpdate); System.out.println("🔄 기존 Space 업데이트 완료! ID: " + spaceToUpdate.getId()); } else { @@ -191,46 +192,28 @@ private String callGPTForGrouping(Map requestData) { private List parseGptResponse(String gptResponse, List bubbles, Long memberId) { try { ObjectMapper objectMapper = new ObjectMapper(); - System.out.println("🔍 Raw GPT Response (Before Parsing): " + gptResponse); - // ✅ 1. GPT 응답을 Map으로 변환 + // ✅ GPT 응답에서 JSON 추출 Map responseMap = objectMapper.readValue(gptResponse, new TypeReference>() {}); - - // ✅ 2. "choices" 필드 확인 List> choices = (List>) responseMap.get("choices"); + if (choices == null || choices.isEmpty()) { throw new RuntimeException("'choices' 필드가 비어 있음"); } - // ✅ 3. "message" 내부 "content" 확인 Map message = (Map) choices.get(0).get("message"); if (message == null || !message.containsKey("content")) { throw new RuntimeException("'message' 필드가 없거나 'content'가 없음"); } - // ✅ 4. "content" 값에서 JSON 문자열 추출 후 다시 변환 String contentJson = (String) message.get("content"); - - // ✅ JSON이 ```json ... ``` 형태일 경우 제거 contentJson = contentJson.replaceAll("```json", "").replaceAll("```", "").trim(); - // ✅ 5. 문자열을 Map으로 변환 - Map parsedContent = objectMapper.readValue(contentJson, new TypeReference>() {}); - - // ✅ 6. "items" 필드 확인 후 리스트로 변환 - if (!parsedContent.containsKey("items")) { - throw new RuntimeException("'items' 필드가 존재하지 않습니다."); - } - - List> parsedData = (List>) parsedContent.get("items"); - if (parsedData == null || parsedData.isEmpty()) { - throw new RuntimeException("'items' 필드가 비어 있음"); - } - + // ✅ JSON 배열 파싱 + List> parsedData = objectMapper.readValue(contentJson, new TypeReference>>() {}); System.out.println("✅ 변환된 Space 데이터: " + parsedData); - // ✅ 7. Space 엔티티로 변환 List spaces = new ArrayList<>(); for (Map item : parsedData) { Long id = ((Number) item.get("id")).longValue(); @@ -244,7 +227,7 @@ private List parseGptResponse(String gptResponse, List bubbles, L String content = (String) item.get("content"); double x = ((Number) item.get("x")).doubleValue(); double y = ((Number) item.get("y")).doubleValue(); - int group = item.get("group") != null ? ((Number) item.get("group")).intValue() : 0; + int group = item.get("group") != null ? ((Number) item.get("group")).intValue() : 1; // ✅ 기본 그룹 1 spaces.add(new Space(content, x, y, group, bubble, memberId)); } @@ -266,23 +249,36 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); promptBuilder.append("- x: A unique floating-point number for the x-coordinate (spread across four quadrants).\n"); promptBuilder.append("- y: A unique floating-point number for the y-coordinate (spread across four quadrants).\n"); - promptBuilder.append("- group: A integer representing the item's group ID.\n\n"); + promptBuilder.append("- group: An integer representing the item's group ID, starting from 1.\n\n"); promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); - promptBuilder.append("2. Items with similar topics should form visually distinct clusters, appearing as bursts from a central point.\n"); - promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); - promptBuilder.append("4. Each group should contain **5 to 8 items**, and **no group should have more than 10 items**, also group always starts with number 1.\n"); - promptBuilder.append("5. The number of group should be minimized, ideally around **1/4 of the total number of items**.\n"); - promptBuilder.append("6. Items that do not naturally fit into a cluster should remain ungrouped, keeping their original coordinates.\n"); - promptBuilder.append("7. X and Y coordinates should be distributed across all four quadrants for better visualization.\n"); - promptBuilder.append("8. Similar items across different clusters should still be positioned near each other where possible.\n"); - promptBuilder.append("9. Extract the **core meaning** of each content item, reducing it to **1 or 2 essential words**.\n"); - promptBuilder.append("10. Each item **MUST belong to at least one group**. If an item does not fit into any existing group, create a new unique group ID for it.\n"); - promptBuilder.append("11. The output must strictly include the `groups` field for all items, even if they belong to a single group.\n"); - promptBuilder.append("12. The output must be strictly in JSON format as shown below:\n\n"); - promptBuilder.append("13. **The last provided bubble MUST be positioned at coordinates (0, 0) without exception.**\n"); - promptBuilder.append("14. **The coordinates of all other items MUST be determined considering that the last item is fixed at (0, 0), ensuring proper spacing and distribution.**\n\n"); + promptBuilder.append("2. Items with similar topics should form visually distinct clusters.\n"); + promptBuilder.append("3. Absolutely !!! Each group should contain **5 to 8 items**, and **no group should have more than 10 items**.\n"); + promptBuilder.append("4. Groups always start with number 1 and increment sequentially (1, 2, 3, ...).\n"); + promptBuilder.append("5. The output **MUST** be in valid JSON format as shown below:\n\n"); + promptBuilder.append("6. Not all items MUST belong to a group. If an item is difficult to group with others, leave it blank. However, if items that are difficult to group can be grouped together, create an additional group for them."); + + // ✅ Specify JSON format + promptBuilder.append("### Response Format:\n"); + promptBuilder.append("[\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"id\": 1,\n"); + promptBuilder.append(" \"content\": \"Keyword\",\n"); + promptBuilder.append(" \"x\": 1.5,\n"); + promptBuilder.append(" \"y\": -0.5,\n"); + promptBuilder.append(" \"group\": 1\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" {\n"); + promptBuilder.append(" \"id\": 2,\n"); + promptBuilder.append(" \"content\": \"Topic\",\n"); + promptBuilder.append(" \"x\": -1.0,\n"); + promptBuilder.append(" \"y\": 0.8,\n"); + promptBuilder.append(" \"group\": 2\n"); + promptBuilder.append(" }\n"); + promptBuilder.append("]\n\n"); + + promptBuilder.append("DO NOT include any explanations or additional text. Respond ONLY with the JSON array.\n"); Long lastKey = null; for (Long key : requestData.keySet()) { From 99a9847380a7533a92a1a97f6f6404f2afafaf2c Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Thu, 13 Feb 2025 00:32:36 +0900 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../space/service/SpaceServiceImpl.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index c004789..6d933d7 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -249,17 +249,22 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("- content: A short keyword or phrase (1-2 words) representing the item's content.\n"); promptBuilder.append("- x: A unique floating-point number for the x-coordinate (spread across four quadrants).\n"); promptBuilder.append("- y: A unique floating-point number for the y-coordinate (spread across four quadrants).\n"); - promptBuilder.append("- group: An integer representing the item's group ID, starting from 1.\n\n"); + promptBuilder.append("- group: An integer representing the item's group ID, starting from 1. If an item does not belong to any group, set this to `null`.\n\n"); promptBuilder.append("### Rules:\n"); promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); promptBuilder.append("2. Items with similar topics should form visually distinct clusters.\n"); - promptBuilder.append("3. Absolutely !!! Each group should contain **5 to 8 items**, and **no group should have more than 10 items**.\n"); - promptBuilder.append("4. Groups always start with number 1 and increment sequentially (1, 2, 3, ...).\n"); - promptBuilder.append("5. The output **MUST** be in valid JSON format as shown below:\n\n"); - promptBuilder.append("6. Not all items MUST belong to a group. If an item is difficult to group with others, leave it blank. However, if items that are difficult to group can be grouped together, create an additional group for them."); + promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); + promptBuilder.append("4. **❗ Each group MUST contain between 5 and 8 items. This is MANDATORY. ❗**\n"); + promptBuilder.append("5. **If any group contains fewer than 5 or more than 8 items, YOU MUST re-cluster that group into smaller sub-groups, each containing 5-8 items.**\n"); + promptBuilder.append("6. **Continue re-clustering until ALL groups satisfy the 5-8 item rule. This process must be repeated as many times as necessary.**\n"); + promptBuilder.append("7. Groups must start with number 1 and increment sequentially (1, 2, 3, ...).\n"); + promptBuilder.append("8. The number of groups should be minimized, ideally around 1/4 of the total number of items.\n"); + promptBuilder.append("9. **Items do NOT have to belong to a group. However, if possible, items should be grouped based on topic similarity.**\n"); + promptBuilder.append("10. Extract the core meaning of each content item, reducing it to 1 or 2 essential words.\n"); + promptBuilder.append("11. The output MUST strictly include the `group` field for ALL items, even if it's `null`.\n"); + promptBuilder.append("12. The output MUST be valid JSON format as shown below, with NO explanations or extra text.\n\n"); - // ✅ Specify JSON format promptBuilder.append("### Response Format:\n"); promptBuilder.append("[\n"); promptBuilder.append(" {\n"); @@ -274,11 +279,11 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append(" \"content\": \"Topic\",\n"); promptBuilder.append(" \"x\": -1.0,\n"); promptBuilder.append(" \"y\": 0.8,\n"); - promptBuilder.append(" \"group\": 2\n"); + promptBuilder.append(" \"group\": null // ✅ Example of an ungrouped item\n"); promptBuilder.append(" }\n"); promptBuilder.append("]\n\n"); - promptBuilder.append("DO NOT include any explanations or additional text. Respond ONLY with the JSON array.\n"); + promptBuilder.append("⚠️ **DO NOT** include any explanations, comments, or extra formatting. Respond ONLY with the JSON array. ⚠️\n"); Long lastKey = null; for (Long key : requestData.keySet()) { From 0992e81f2dca8b12f9d7d37e7f34605d24d24841 Mon Sep 17 00:00:00 2001 From: Yoonji Lee Date: Thu, 13 Feb 2025 11:02:11 +0900 Subject: [PATCH 19/19] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/domain/space/dto/SpaceResponseDto.java | 11 +---------- .../domain/space/service/SpaceServiceImpl.java | 3 +-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java index 7e7067e..19f41dc 100644 --- a/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java +++ b/project/src/main/java/com/edison/project/domain/space/dto/SpaceResponseDto.java @@ -12,9 +12,8 @@ public class SpaceResponseDto { private Integer group = 0; // 올바른 생성자 추가 - public SpaceResponseDto(Bubble bubble, String content, double x, double y, Integer group) { + public SpaceResponseDto(Bubble bubble, double x, double y, Integer group) { this.id = bubble.getBubbleId(); - this.content = content; this.x = x; this.y = y; this.group = group; @@ -29,14 +28,6 @@ public void setId(Long id) { this.id = id; } - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - public double getX() { return x; } diff --git a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java index 6d933d7..f3cec39 100644 --- a/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java +++ b/project/src/main/java/com/edison/project/domain/space/service/SpaceServiceImpl.java @@ -102,7 +102,6 @@ public ResponseEntity processSpaces(CustomUserPrincipal userPrincip List spaceDtos = spaces.stream() .map(space -> new SpaceResponseDto( space.getBubble(), // ✅ Bubble 객체 전달 - space.getContent(), space.getX(), space.getY(), space.getGroup() @@ -252,7 +251,7 @@ private String buildPromptWithId(Map requestData) { promptBuilder.append("- group: An integer representing the item's group ID, starting from 1. If an item does not belong to any group, set this to `null`.\n\n"); promptBuilder.append("### Rules:\n"); - promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum spacing of 0.5.\n"); + promptBuilder.append("1. Each item must have a unique (x, y) coordinate, with a minimum Euclidean distance of 0.5 between any two items.\n"); promptBuilder.append("2. Items with similar topics should form visually distinct clusters.\n"); promptBuilder.append("3. Clusters should be well-separated from each other but internally cohesive.\n"); promptBuilder.append("4. **❗ Each group MUST contain between 5 and 8 items. This is MANDATORY. ❗**\n");