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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions demo-api/src/main/java/team9/demo/RoomGenieApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan("team9.demo.external.config.properties")
public class RoomGenieApplication {
public static void main(String[] args) {
SpringApplication.run(RoomGenieApplication.class, args);
Expand Down
10 changes: 6 additions & 4 deletions demo-api/src/main/java/team9/demo/config/OpenAiConfig.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package team9.demo.config;


import org.springframework.beans.factory.annotation.Value;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import team9.demo.external.config.properties.OpenAiProperties;

@Configuration
@RequiredArgsConstructor
public class OpenAiConfig {
@Value("${openai.api.key}")
private String openAiApiKey;

private final OpenAiProperties openAiProperties;

@Bean
public RestTemplate template() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add((request, body, execution) -> {
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
request.getHeaders().set("Authorization", "Bearer " + openAiApiKey);
request.getHeaders().set("Authorization", "Bearer " + openAiProperties.apiKey());
return execution.execute(request, body);
});
return restTemplate;
Expand Down
9 changes: 3 additions & 6 deletions demo-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
spring.config.import=db.yml, optional:file:.env[.properties]

openai.api.key=${OPENAI_API_KEY}
openai.model1=${OPENAI_MODEL1}
openai.model2=${OPENAI_MODEL2}
openai.api.url=${OPENAI_API_URL}
openai.api.url2=${OPENAI_API_URL2}
openai.api.url.image=${OPENAI_API_URL_IMAGE}
openai.api-key=${OPENAI_API_KEY}
openai.model=${OPENAI_MODEL1}
openai.chat-endpoint=${OPENAI_API_URL2}

spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=100MB
Expand Down
9 changes: 3 additions & 6 deletions demo-api/src/test/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
spring.config.import=db.yml, optional:file:.env[.properties]
server.port=8080

openai.api.key=${OPENAI_API_KEY:test}
openai.model1=${OPENAI_MODEL1:test}
openai.model2=${OPENAI_MODEL2:test}
openai.api.url=${OPENAI_API_URL:http://localhost}
openai.api.url2=${OPENAI_API_URL2:http://localhost}
openai.api.url.image=${OPENAI_API_URL_IMAGE:http://localhost}
openai.api-key=${OPENAI_API_KEY:test}
openai.model=${OPENAI_MODEL1:test}
openai.chat-endpoint=${OPENAI_API_URL2:http://localhost}

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import team9.demo.error.ConflictException;
import team9.demo.error.ErrorCode;
import team9.demo.external.config.properties.AwsS3Properties;
import team9.demo.model.media.FileData;
import team9.demo.model.media.Media;

Expand All @@ -16,9 +16,7 @@
public class ExternalFileClientImpl implements ExternalFileClient {

private final AmazonS3 amazonS3;

@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AwsS3Properties s3Properties;

@Override
public void uploadFile(FileData file, Media media) {
Expand All @@ -27,7 +25,7 @@ public void uploadFile(FileData file, Media media) {
metadata.setContentLength(file.getSize());
metadata.setContentType(media.getType().value());

amazonS3.putObject(bucket, media.getPath(), file.getInputStream(), metadata);
amazonS3.putObject(s3Properties.bucket(), media.getPath(), file.getInputStream(), metadata);
} catch (Exception e) {
throw new ConflictException(ErrorCode.FILE_UPLOAD_FAILED);
}
Expand All @@ -36,13 +34,13 @@ public void uploadFile(FileData file, Media media) {
@Override
public void removeFile(Media media) {
try {
amazonS3.deleteObject(bucket, media.getPath());
amazonS3.deleteObject(s3Properties.bucket(), media.getPath());
} catch (Exception e) {
throw new ConflictException(ErrorCode.FILE_DELETE_FAILED);
}
}

public String getPublicUrl(Media media) {
return amazonS3.getUrl(bucket, media.getPath()).toString();
return amazonS3.getUrl(s3Properties.bucket(), media.getPath()).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
Expand All @@ -12,6 +11,7 @@
import team9.demo.dto.ChatGPTRequest;
import team9.demo.error.AiException;
import team9.demo.error.ErrorCode;
import team9.demo.external.config.properties.OpenAiProperties;
import team9.demo.model.ai.analysis.ChatResponse;
import team9.demo.model.ai.analysis.Choice;
import team9.demo.model.ai.analysis.TextMessage;
Expand All @@ -32,23 +32,18 @@
@RequiredArgsConstructor
public class GptVisionClient {

@Value("${openai.api.url2}")
private String chatEndpoint;

@Value("${openai.model1}")
private String model;

private final RestTemplate restTemplate;
private final S3ImageStore s3ImageStore;
private final OpenAiProperties openAiProperties;

/** 단일 이미지 분석 — 정리 가이드 텍스트 응답을 반환한다. */
public ChatResponse requestImageAnalysis(String imageUrl, String requestText) {
try {
String dataUrl = "data:image/jpeg;base64," + s3ImageStore.encodeBase64(imageUrl);
ChatGPTRequest requestDto = ChatGPTRequest.of(model, requestText, dataUrl, 500);
ChatGPTRequest requestDto = ChatGPTRequest.of(openAiProperties.model(), requestText, dataUrl, 500);

ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
chatEndpoint, HttpMethod.POST, new HttpEntity<>(requestDto), getMapType()
openAiProperties.chatEndpoint(), HttpMethod.POST, new HttpEntity<>(requestDto), getMapType()
);
return parseChatResponse(response);

Expand All @@ -67,7 +62,7 @@ public ChatResponse requestCompareAnalysis(String beforeUrl, String afterUrl, St
String afterBase64 = s3ImageStore.encodeBase64(afterUrl);

Map<String, Object> requestBody = Map.of(
"model", model,
"model", openAiProperties.model(),
"messages", List.of(Map.of(
"role", "user",
"content", List.of(
Expand All @@ -80,7 +75,7 @@ public ChatResponse requestCompareAnalysis(String beforeUrl, String afterUrl, St
);

ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
chatEndpoint, HttpMethod.POST, new HttpEntity<>(requestBody), getMapType()
openAiProperties.chatEndpoint(), HttpMethod.POST, new HttpEntity<>(requestBody), getMapType()
);
return parseChatResponse(response);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
Expand All @@ -18,6 +17,7 @@
import org.springframework.web.client.RestTemplate;
import team9.demo.error.AiException;
import team9.demo.error.ErrorCode;
import team9.demo.external.config.properties.LamaProperties;
import team9.demo.model.user.UserId;

import java.util.List;
Expand All @@ -34,10 +34,8 @@
@RequiredArgsConstructor
public class LamaInpaintClient {

@Value("${ai.lama.url:http://localhost:7870}")
private String lamaServerUrl;

private final S3ImageStore s3ImageStore;
private final LamaProperties lamaProperties;

private final RestTemplate lamaClient = new RestTemplate(List.of(
new FormHttpMessageConverter(),
Expand All @@ -57,7 +55,7 @@ public String editImage(byte[] imageBytes, byte[] maskBytes, String prompt, User
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

ResponseEntity<byte[]> response = lamaClient.exchange(
lamaServerUrl + "/inpaint",
lamaProperties.url() + "/inpaint",
HttpMethod.POST,
new HttpEntity<>(body, headers),
byte[].class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import com.amazonaws.services.s3.model.S3Object;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import team9.demo.error.AiException;
import team9.demo.error.ErrorCode;
import team9.demo.external.config.properties.AwsS3Properties;
import team9.demo.model.user.UserId;

import java.io.ByteArrayInputStream;
Expand All @@ -32,14 +32,12 @@
public class S3ImageStore {

private final AmazonS3 amazonS3;

@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AwsS3Properties s3Properties;

/** S3 오브젝트를 다운로드해 byte 배열로 반환한다. */
public byte[] download(String imageUrl) throws IOException {
String key = extractKey(imageUrl);
S3Object object = amazonS3.getObject(bucket, key);
S3Object object = amazonS3.getObject(s3Properties.bucket(), key);
try (InputStream inputStream = object.getObjectContent()) {
return inputStream.readAllBytes();
}
Expand All @@ -59,20 +57,21 @@ public String uploadCleanedImage(byte[] image, UserId userId) {
metadata.setContentType("image/png");

try (InputStream inputStream = new ByteArrayInputStream(image)) {
amazonS3.putObject(bucket, key, inputStream, metadata);
amazonS3.putObject(s3Properties.bucket(), key, inputStream, metadata);
} catch (IOException e) {
log.error("S3 업로드 실패: {}", e.getMessage(), e);
throw new AiException(ErrorCode.AI_S3_UPLOAD_FAILED);
}

return amazonS3.getUrl(bucket, key).toString();
return amazonS3.getUrl(s3Properties.bucket(), key).toString();
}

/** S3 URL(path-style / virtual-hosted style)에서 object key를 추출한다. */
private String extractKey(String imageUrl) throws IOException {
URL url = new URL(imageUrl);
String host = url.getHost();
String path = url.getPath();
String bucket = s3Properties.bucket();

if (path.startsWith("/" + bucket + "/")) {
return path.substring(("/" + bucket + "/").length());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
Expand All @@ -12,6 +11,7 @@
import org.springframework.web.client.RestTemplate;
import team9.demo.error.AiException;
import team9.demo.error.ErrorCode;
import team9.demo.external.config.properties.YoloProperties;
import team9.demo.model.ai.mask.Box;

import java.util.Collections;
Expand All @@ -28,18 +28,16 @@
@RequiredArgsConstructor
public class YoloDetectionClient {

@Value("${ai.yolo.url:http://localhost:5000}")
private String yoloServerUrl;

private final RestTemplate restTemplate;
private final YoloProperties yoloProperties;

public List<Box> detectClutterBoxes(byte[] imageBytes) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);

ResponseEntity<List> response = restTemplate.exchange(
yoloServerUrl + "/yolo",
yoloProperties.url() + "/yolo",
HttpMethod.POST,
new HttpEntity<>(imageBytes, headers),
List.class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package team9.demo.external.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* AWS S3 버킷 설정.
*
* @param bucket S3 버킷 이름 (application.properties: cloud.aws.s3.bucket)
*/
@ConfigurationProperties(prefix = "cloud.aws.s3")
public record AwsS3Properties(
String bucket
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package team9.demo.external.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

/**
* 자체 호스팅 LAMA 인페인팅 서버 설정.
*
* @param url LAMA 서버 base URL (application.properties: ai.lama.url)
*/
@ConfigurationProperties(prefix = "ai.lama")
public record LamaProperties(
@DefaultValue("http://localhost:7870") String url
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package team9.demo.external.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* OpenAI 호출에 필요한 설정.
* <p>
* Spring Boot relaxed binding으로 application.properties의 다음 키와 매핑된다:
* <pre>
* openai.api-key → apiKey
* openai.chat-endpoint → chatEndpoint
* openai.model → model
* </pre>
*
* @param apiKey Bearer 토큰 (RestTemplate interceptor에서 사용)
* @param chatEndpoint chat/completions 엔드포인트
* @param model 사용할 모델 식별자 (예: gpt-4-vision-preview)
*/
@ConfigurationProperties(prefix = "openai")
public record OpenAiProperties(
String apiKey,
String chatEndpoint,
String model
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package team9.demo.external.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

/**
* 자체 호스팅 YOLO 감지 서버 설정.
*
* @param url YOLO 서버 base URL (application.properties: ai.yolo.url)
*/
@ConfigurationProperties(prefix = "ai.yolo")
public record YoloProperties(
@DefaultValue("http://localhost:5000") String url
) {
}
Loading