diff --git a/README.md b/README.md
index 23e63cf..2cfefe1 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,6 @@
-# ๐ Retrip - ์ฌํ์ ์์ฝํด์ฃผ๋ ์๋ก์ด ๋ฐฉ์์ SNS
-- Retrip์ ์ฌํ์ ์ข์ํ๋ ์ฌ๋๋ค์ ์ํ ์ด๋ฏธ์ง ๊ธฐ๋ฐ ์ฌํ ์์ฝ SNS์
๋๋ค. ์ฌ์ฉ์๊ฐ ์
๋ก๋ํ ์ฌํ ์ฌ์ง ์ ๋ฉํ๋ฐ์ดํฐ์ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก, ์ฌํ์ ์ ์ฒด์ ์ธ ๋ถ์๊ธฐ์ ์ ๋ณด๋ฅผ ํ๋์ ์ด๋ฏธ์ง๋ก ์์ฝํด์ฃผ๋ ํน๋ณํ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
+# ๐ ReTrip - ์ฌํ์ ์์ฝํด์ฃผ๋ ์๋ก์ด ๋ฐฉ์์ SNS
-![readme_mockup2]()
-
-- ๋ฐฐํฌ URL :
-- Test ID :
-- Test PW :
-
-
+Retrip์ ์ฌํ์ ์ข์ํ๋ ์ฌ๋๋ค์ ์ํ ์ด๋ฏธ์ง ๊ธฐ๋ฐ ์ฌํ ์์ฝ SNS์
๋๋ค. ์ฌ์ฉ์๊ฐ ์
๋ก๋ํ ์ฌํ ์ฌ์ง ์ ๋ฉํ๋ฐ์ดํฐ์ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก, ์ฌํ์ ์ ์ฒด์ ์ธ ๋ถ์๊ธฐ์ ์ ๋ณด๋ฅผ ํ๋์ ์ด๋ฏธ์ง๋ก ์์ฝํด์ฃผ๋ ํน๋ณํ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
## ํ๋ก์ ํธ ์๊ฐ
@@ -31,30 +24,105 @@
## 1. ๊ฐ๋ฐ ํ๊ฒฝ
-- Front : HTML, Vue3
-- Back-end : ์ ๊ณต๋ API ํ์ฉ
-- ๋ฒ์ ๋ฐ ์ด์๊ด๋ฆฌ : Github, Github Issues, Github Project
-- ํ์
ํด : Discord, Notion
-- ์๋น์ค ๋ฐฐํฌ ํ๊ฒฝ :
+- **Front**: HTML, Vue3, Vuetify3
+- **Back-end**: Spring Boot 3.4.5, Java 21, Spring Data JPA, Spring Security
+- **Database**: MySQL, Redis
+- **AI/ML**: OpenAI GPT-4 Vision API
+- **์ธํ๋ผ**: Docker, Nginx, AWS S3
+- **๋ฒ์ ๋ฐ ์ด์๊ด๋ฆฌ**: Github, Github Issues, Github Project
+- **ํ์
ํด**: Discord, Notion
+- **์๋น์ค ๋ฐฐํฌ ํ๊ฒฝ**: AWS EC2, Docker Compose
## 2. ์ฑํํ ๊ฐ๋ฐ ๊ธฐ์ ๊ณผ ๋ธ๋์น ์ ๋ต
+### ๊ธฐ์ ์คํ ์์ธ
+
+#### Backend Framework
+- **Spring Boot 3.4.5** (Java 21) - ์ต์ LTS ๋ฒ์ ์ฌ์ฉ์ผ๋ก ์์ ์ฑ ํ๋ณด
+- **Spring Web MVC** - RESTful API ๊ตฌํ
+- **Spring Data JPA** - ORM์ ํตํ ํจ์จ์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ
+- **Spring Security** - OAuth2 ๊ธฐ๋ฐ ์์
๋ก๊ทธ์ธ (Kakao)
+
+#### AI & ์ด๋ฏธ์ง ์ฒ๋ฆฌ
+- **OpenAI GPT-4 Vision API** - ์ด๋ฏธ์ง ๋ถ์ ๋ฐ ์ฌํ ํจํด ์ธ์
+- **metadata-extractor 2.18.0** - EXIF ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ (GPS, ์๊ฐ ์ ๋ณด)
+- **TwelveMonkeys ImageIO** - HEIF, HEIC ๋ฑ ๋ค์ํ ์ด๋ฏธ์ง ํฌ๋งท ์ง์
+- **Thumbnailator** - ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ๋ฐ ์ต์ ํ
+
+#### ๋ชจ๋ํฐ๋ง
+- **Spring Actuator** - ์ ํ๋ฆฌ์ผ์ด์
์ํ ๋ชจ๋ํฐ๋ง
+- **Prometheus & Micrometer** - ๋ฉํธ๋ฆญ ์์ง ๋ฐ ๋ชจ๋ํฐ๋ง
+
+### ๋ธ๋์น ์ ๋ต
+- **main**: ํ๋ก๋์
๋ฐฐํฌ ๋ธ๋์น
+- **dev**: ๊ฐ๋ฐ ํตํฉ ๋ธ๋์น
+- **feature/๊ธฐ๋ฅ๋ช
**: ๊ธฐ๋ฅ ๊ฐ๋ฐ ๋ธ๋์น
+- **refactor/๋ฆฌํฉํ ๋ง๋ช
**: ๋ฆฌํฉํ ๋ง ๋ธ๋์น
## 3. ํ๋ก์ ํธ ๊ตฌ์กฐ
+```
+retrip-api/
+โโโ src/main/java/ssafy/retrip/
+โ โโโ api/
+โ โ โโโ controller/ # REST API ์๋ํฌ์ธํธ
+โ โ โ โโโ retrip/
+โ โ โ โโโ RetripController.java
+โ โ โ โโโ response/ # API ์๋ต DTO
+โ โ โโโ service/
+โ โ โโโ openai/ # OpenAI GPT ์ฐ๋
+โ โ โ โโโ GptImageAnalysisService.java
+โ โ โ โโโ OpenAiClient.java
+โ โ โ โโโ response/ # GPT ์๋ต ๋ชจ๋ธ
+โ โ โโโ retrip/ # ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง
+โ โ โโโ RetripService.java
+โ โ โโโ ImageConverter.java
+โ โ โโโ info/ # ๋๋ฉ์ธ ์ ๋ณด ๊ฐ์ฒด
+โ โโโ config/ # ์ค์ ํด๋์ค
+โ โ โโโ SecurityConfig.java # Spring Security ์ค์
+โ โ โโโ OpenAiConfig.java # OpenAI ํด๋ผ์ด์ธํธ ์ค์
+โ โ โโโ RedisConfig.java # Redis ์ค์
+โ โ โโโ WebConfig.java # CORS ์ค์
+โ โโโ domain/ # JPA ์ํฐํฐ
+โ โ โโโ retrip/
+โ โ โ โโโ Retrip.java # ์ฌํ ์์ฝ ์ํฐํฐ
+โ โ โ โโโ TimeSlot.java # ์๊ฐ๋ Enum
+โ โ โ โโโ RetripRepository.java
+โ โ โโโ place/
+โ โ โโโ RecommendationPlace.java # ์ถ์ฒ ์ฅ์ ์ํฐํฐ
+โ โโโ utils/ # ์ ํธ๋ฆฌํฐ ํด๋์ค
+โ โโโ ImageMetaDataUtil.java # ์ด๋ฏธ์ง ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ
+โ โโโ CoordinateUtil.java # GPS ์ขํ ์ฒ๋ฆฌ
+โ โโโ DistanceUtil.java # ๊ฑฐ๋ฆฌ ๊ณ์ฐ
+โโโ docker/ # Docker ์ค์
+โโโ nginx/ # Nginx ์ค์ (SSL, ๋ฆฌ๋ฒ์ค ํ๋ก์)
+โโโ scripts/ # ๋ฐฐํฌ ์คํฌ๋ฆฝํธ
+โโโ src/main/resources/
+ โโโ application.yml # ์ ํ๋ฆฌ์ผ์ด์
์ค์
+ โโโ analysis.prompt # GPT ๋ถ์ ํ๋กฌํํธ
+```
+
## 4. ์ญํ ๋ถ๋ด
### ๐๐ปโโ๏ธ ๊น์ฉ๋ฒ
+- AI ๋ชจ๋ธ ์ฐ๋ ๋ฐ ํ๋กฌํํธ ์์ง๋์ด๋ง
+- ์ด๋ฏธ์ง ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ ๋ฐ ์ฒ๋ฆฌ
+- GPS ๊ธฐ๋ฐ ์์น ๋ถ์ ๋ก์ง ๊ตฌํ
+- Docker ๋ฐ ์ธํ๋ผ ๊ตฌ์ฑ
### ๐๐ปโโ๏ธ ์ค์ผ์ฐ
+- Spring Boot ๋ฐฑ์๋ ์ํคํ
์ฒ ์ค๊ณ
+- RESTful API ๊ฐ๋ฐ
+- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ๋ฐ JPA ๊ตฌํ
+- Spring Security ๋ฐ OAuth2 ์ธ์ฆ ๊ตฌํ
@@ -63,37 +131,146 @@
### ๊ฐ๋ฐ ๊ธฐ๊ฐ
- ์ ์ฒด ๊ฐ๋ฐ ๊ธฐ๊ฐ : 2025-04-28 ~ ing
-- UI ๊ตฌํ : 2025-05-15 ~ ing
-- ๊ธฐ๋ฅ ๊ตฌํ : 2022-05-12 ~ ing
+- ๋ฐฑ์๋ API ๊ฐ๋ฐ : 2025-04-28 ~ 2025-05-10
+- AI ๋ชจ๋ธ ์ฐ๋ : 2025-05-11 ~ 2025-05-20
+- ์ธํ๋ผ ๊ตฌ์ถ : 2025-05-21 ~ 2025-05-25
+- ํ
์คํธ ๋ฐ ์ต์ ํ : 2025-05-26 ~ ing
### ์์
๊ด๋ฆฌ
+- Github Projects๋ฅผ ํ์ฉํ ์นธ๋ฐ ๋ณด๋ ์ด์
+- ์ฃผ 2ํ ์ ๊ธฐ ๋ฏธํ
์ ํตํ ์งํ์ํฉ ๊ณต์
+- Discord๋ฅผ ํตํ ์ค์๊ฐ ์ปค๋ฎค๋์ผ์ด์
## 6. ์ ๊ฒฝ ์ด ๋ถ๋ถ
+### ๐ฏ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์ต์ ํ
+- ๋์ฉ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ธ ๋ฆฌ์ฌ์ด์ง
+- HEIF/HEIC ๋ฑ ์ต์ ์ด๋ฏธ์ง ํฌ๋งท ์ง์
+- ๋ฉํ๋ฐ์ดํฐ ๋ณด์กดํ๋ฉด์ ์ด๋ฏธ์ง ํฌ๊ธฐ ์ต์ ํ
+
+### ๐ค AI ๋ถ์ ์ ํ๋
+- GPT-4 Vision ํ๋กฌํํธ ์ต์ ํ๋ก ๋ถ์ ์ ํ๋ ํฅ์
+- ์ด๋ฏธ์ง ๋ฉํ๋ฐ์ดํฐ์ AI ๋ถ์ ๊ฒฐ๊ณผ์ ๊ต์ฐจ ๊ฒ์ฆ
+- ์์น ์ ๋ณด ๊ธฐ๋ฐ ๋ง์ถคํ ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ
+
+### ๐ ๋ณด์ ๋ฐ ์์ ์ฑ
+- Spring Security๋ฅผ ํตํ ์ธ์ฆ/์ธ๊ฐ ์ฒ๋ฆฌ
+- ์ด๋ฏธ์ง ์
๋ก๋ ์ ์
์ฑ ํ์ผ ๊ฒ์ฆ
+- Rate Limiting์ผ๋ก API ๋จ์ฉ ๋ฐฉ์ง
+
+### ๐ ๋ชจ๋ํฐ๋ง
+- Prometheus + Grafana๋ฅผ ํตํ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง
+- ์ฃผ์ ๋น์ฆ๋์ค ๋ฉํธ๋ฆญ ์ถ์
+- ์๋ฌ ๋ก๊น
๋ฐ ์๋ฆผ ์์คํ
+
## 7. ํ์ด์ง๋ณ ๊ธฐ๋ฅ
+### ๐ธ ์ด๋ฏธ์ง ์
๋ก๋ ๋ฐ ๋ถ์
+- ๋ค์ค ์ด๋ฏธ์ง ์
๋ก๋ (5~20์ฅ)
+- ์ค์๊ฐ ์
๋ก๋ ์งํ๋ฅ ํ์
+- ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ ์์ ์กฐ์
+
+### ๐บ๏ธ ์ฌํ ์์ฝ ๊ฒฐ๊ณผ
+- AI ๊ธฐ๋ฐ ์ฌํ ์คํ์ผ ๋ถ์ (MBTI, ์ฌํ ์ฑํฅ)
+- ์๊ฐ์ ์ฌํ ์์ฝ ์นด๋ ์์ฑ
+- ์์น ๊ธฐ๋ฐ ์ถ์ฒ ์ฅ์ ์ ๊ณต
+- ์ฌํ ํต๊ณ (์ด๋ ๊ฑฐ๋ฆฌ, ์ฃผ์ ํ๋ ์๊ฐ๋ ๋ฑ)
+
+### ๐ฑ SNS ๊ณต์
+- ์์ฑ๋ ์์ฝ ์ด๋ฏธ์ง ๋ค์ด๋ก๋
+- ์ฃผ์ SNS ํ๋ซํผ ๊ณต์ ๊ธฐ๋ฅ
+- ๊ณ ์ URL์ ํตํ ๊ฒฐ๊ณผ ๊ณต์
+
## 8. ํธ๋ฌ๋ธ ์ํ
+### ๐ง ๋์ฉ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ฉ๋ชจ๋ฆฌ ์ด์
+- **๋ฌธ์ **: ๊ณ ํด์๋ ์ด๋ฏธ์ง ๋ค๋ ์
๋ก๋ ์ OutOfMemoryError ๋ฐ์
+- **ํด๊ฒฐ**:
+ - ImageIO ๋์ TwelveMonkeys ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
+ - ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์์ผ๋ก ์ด๋ฏธ์ง ์ฒ๋ฆฌ
+ - ์ ์ ํ JVM ํ ๋ฉ๋ชจ๋ฆฌ ์ค์
+
+### ๐ CORS ์ด์
+- **๋ฌธ์ **: ํ๋ก ํธ์๋-๋ฐฑ์๋ ๊ฐ CORS ์ ์ฑ
์๋ฐ
+- **ํด๊ฒฐ**:
+ - WebConfig์์ ๋ช
์์ CORS ์ค์
+ - ํ๊ฒฝ๋ณ ํ์ฉ Origin ๋ถ๋ฆฌ ๊ด๋ฆฌ
+
+### โก GPT API ์๋ต ์๊ฐ ์ต์ ํ
+- **๋ฌธ์ **: GPT-4 Vision API ์๋ต ์๊ฐ์ด ๊ธธ์ด UX ์ ํ
+- **ํด๊ฒฐ**:
+ - ์ด๋ฏธ์ง ์ฌ์ ์์ถ์ผ๋ก ์์ฒญ ํฌ๊ธฐ ๊ฐ์
+ - ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฐ ์งํ ์ํ ํ์
+ - ์๋ต ์บ์ฑ์ผ๋ก ์ฌ์์ฒญ ์ ์ฑ๋ฅ ํฅ์
+
## 9. ๊ฐ์ ๋ชฉํ
+- ๐ ์ค์๊ฐ ๋ถ์ ์งํ๋ฅ ํ์ (WebSocket)
+- ๐ ๋ค๊ตญ์ด ์ง์ ํ๋
+- ๐ ๋ ์ ๊ตํ ์ฌํ ํจํด ๋ถ์
+- ๐จ ๋ค์ํ ์์ฝ ํ
ํ๋ฆฟ ์ ๊ณต
+- ๐ฅ ์์
๊ธฐ๋ฅ ๊ฐํ (ํ๋ก์ฐ, ์ข์์, ๋๊ธ)
+- ๐ฑ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ
+
## 10. ํ๋ก์ ํธ ํ๊ธฐ
-### ๐๐ปโโ๏ธย ๊น์ฉ๋ฒ
+### ๐๐ปโโ๏ธ ๊น์ฉ๋ฒ
+AI ๊ธฐ์ ์ ์ค์ ์๋น์ค์ ์ ์ฉํ๋ฉด์ ํ๋กฌํํธ ์์ง๋์ด๋ง์ ์ค์์ฑ์ ๊นจ๋ฌ์์ต๋๋ค. ํนํ ์ด๋ฏธ์ง ๋ฉํ๋ฐ์ดํฐ์ AI ๋ถ์์ ๊ฒฐํฉํ์ฌ ๋ ์ ํํ ๊ฒฐ๊ณผ๋ฅผ ๋์ถํ๋ ๊ณผ์ ์ด ํฅ๋ฏธ๋ก์ ์ต๋๋ค. Docker์ ์ธํ๋ผ ๊ตฌ์ฑ์ ํตํด DevOps ์ญ๋๋ ํฅ์์ํฌ ์ ์์์ต๋๋ค.
+
+
+### ๐๐ปโโ๏ธ ์ค์ผ์ฐ
+Spring Boot์ JPA๋ฅผ ํ์ฉํ ๋ฐฑ์๋ ๊ฐ๋ฐ ๊ฒฝํ์ ์์ ์ ์์๊ณ , ํนํ ๋์ฉ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ์ ์ฑ๋ฅ ์ต์ ํ ๊ณผ์ ์์ ๋ง์ ๊ฒ์ ๋ฐฐ์ ์ต๋๋ค. OAuth2 ์ธ์ฆ๊ณผ ๋ณด์ ์ค์ ์ ๊ตฌํํ๋ฉด์ ์ค๋ฌด์์ ํ์ํ ๋ณด์ ์ง์๋ ์ต๋ํ ์ ์์์ต๋๋ค.
-### ๐๐ปโโ๏ธย ์ค์ผ์ฐ
+---
+
+## ๐ Quick Start
+
+### ์๊ตฌ์ฌํญ
+- Java 21+
+- MySQL 8.0+
+- Redis 7.0+
+- Docker & Docker Compose (์ ํ์ฌํญ)
+
+### ๋ก์ปฌ ํ๊ฒฝ ์คํ
+```bash
+# 1. ํ๋ก์ ํธ ํด๋ก
+git clone https://github.com/ReTrip-Dev/ReTrip-api.git
+cd ReTrip-api
+
+# 2. application-secret.yml ์ค์
+# src/main/resources/application-secret.yml ํ์ผ ์์ฑ ํ ํ์ํ ์ค์ ์ถ๊ฐ
+
+# 3. ์ ํ๋ฆฌ์ผ์ด์
์คํ
+./gradlew bootRun
+```
+
+### API ํ
์คํธ
+```bash
+# ์ด๋ฏธ์ง ์
๋ก๋ ๋ฐ ๋ถ์
+curl -X POST http://localhost:8080/api/images/uploads \
+ -F "images=@photo1.jpg" \
+ -F "images=@photo2.jpg" \
+ -F "images=@photo3.jpg" \
+ -F "images=@photo4.jpg" \
+ -F "images=@photo5.jpg"
+```
+
+## ๐ ๋ฌธ์
+
+ํ๋ก์ ํธ์ ๋ํ ๋ฌธ์์ฌํญ์ [Issues](https://github.com/ReTrip-Dev/ReTrip-api/issues)๋ฅผ ํตํด ๋จ๊ฒจ์ฃผ์ธ์.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 40937b5..86ec98c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -63,23 +63,18 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
- // AWS - ์ค๋ณต๋ ์์กด์ฑ ์ ๊ฑฐ ๋ฐ ๊ฐ์ํ
- implementation platform('software.amazon.awssdk:bom:2.20.56')
- implementation 'software.amazon.awssdk:s3'
- implementation 'software.amazon.awssdk:url-connection-client'
-
- // EXIF/๋ฉํ๋ฐ์ดํฐ - ์์ ์ ์ธ ๋ฒ์ ์ฌ์ฉ
+ // EXIF/๋ฉํ๋ฐ์ดํฐ
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
- // TwelveMonkeys ImageIO - ์์ ์ ์ธ ๋ฒ์ ์ฌ์ฉ
+ // TwelveMonkeys ImageIO
implementation 'com.twelvemonkeys.imageio:imageio-core:3.9.4'
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.9.4'
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.9.4'
- // Apache Commons Imaging (์คํ์ ์์กด์ฑ ์ ๊ฑฐ)
+ // Apache Commons Imaging
implementation 'org.apache.commons:commons-imaging:1.0-alpha3'
- // ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ - ์์ ์ ์ธ ๋ฒ์ ์ผ๋ก ๋ณ๊ฒฝ
+ // ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation 'net.coobird:thumbnailator:0.4.19'
// Monitoring
@@ -87,8 +82,8 @@ dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'
// OpenAI ์ฐ๋
- implementation("com.openai:openai-java:2.8.1") // ๊ณต์ SDK
- implementation("org.springframework.boot:spring-boot-starter-webflux") // SSE ์คํธ๋ฆผ ์ ์ก
+ implementation 'com.openai:openai-java:2.8.1'
+
}
diff --git a/src/main/java/ssafy/retrip/api/controller/retrip/RetripController.java b/src/main/java/ssafy/retrip/api/controller/retrip/RetripController.java
index c6838da..8d02784 100644
--- a/src/main/java/ssafy/retrip/api/controller/retrip/RetripController.java
+++ b/src/main/java/ssafy/retrip/api/controller/retrip/RetripController.java
@@ -3,7 +3,9 @@
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
@@ -14,6 +16,7 @@
import ssafy.retrip.api.controller.retrip.response.TravelAnalysisResponseDto;
import ssafy.retrip.api.service.retrip.RetripService;
+@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/images")
@@ -22,14 +25,18 @@ public class RetripController {
private final RetripService retripService;
@PostMapping("/uploads")
- public ResponseEntity uploadMultipleImages(HttpServletRequest request,
+ public CompletableFuture> uploadMultipleImages(HttpServletRequest request,
@RequestParam("images") List images) throws IOException {
- try {
- return ResponseEntity.ok(retripService.createRetripFromImages(images));
- } catch (Exception e) {
- e.printStackTrace();
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
- }
+ return retripService.createRetripFromImages(images)
+ .thenApply(result -> {
+ log.info("์ฌํ ๋ถ์ ์๋ฃ: retripId={}", result.getRetripId());
+ return ResponseEntity.ok(result);
+ })
+ .exceptionally(ex -> {
+ log.error("์ฌํ ๋ถ์ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์", ex);
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(null);
+ });
}
}
diff --git a/src/main/java/ssafy/retrip/api/service/openai/GptImageAnalysisService.java b/src/main/java/ssafy/retrip/api/service/openai/GptImageAnalysisService.java
index 015ff56..7f757a1 100644
--- a/src/main/java/ssafy/retrip/api/service/openai/GptImageAnalysisService.java
+++ b/src/main/java/ssafy/retrip/api/service/openai/GptImageAnalysisService.java
@@ -1,9 +1,11 @@
package ssafy.retrip.api.service.openai;
import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
@@ -35,7 +37,7 @@ public class GptImageAnalysisService {
*
* @throws RuntimeException ํ๋กฌํํธ ํ์ผ์ ์ฐพ์ ์ ์๊ฑฐ๋ ์ฝ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
*/
- @jakarta.annotation.PostConstruct
+ @PostConstruct
private void init() {
try {
Resource resource = resourceLoader.getResource("classpath:analysis.prompt");
@@ -46,34 +48,20 @@ private void init() {
}
}
- /**
- * ์ฃผ์ด์ง ์ด๋ฏธ์ง ๋ฐ์ดํฐ์ ์์น ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ GPT์ ๋ถ์์ ์์ฒญํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํ์ฑํ์ฌ DTO๋ก ๋ฐํํฉ๋๋ค.
- *
- * @param imageDataUrls ๋ถ์ํ ์ด๋ฏธ์ง์ ๋ฐ์ดํฐ URL ๋ชฉ๋ก (Base64 ์ธ์ฝ๋ฉ)
- * @param latitude ์ฃผ์ ์์น์ ์๋
- * @param longitude ์ฃผ์ ์์น์ ๊ฒฝ๋
- * @return GPT๊ฐ ๋ฐํํ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ด์ {@link AnalysisResponse} ๊ฐ์ฒด
- * @throws IllegalStateException GPT ์๋ต JSON์ ํ์ฑํ๋ ๋ฐ ์คํจํ ๊ฒฝ์ฐ
- */
- public AnalysisResponse analyze(List imageDataUrls, double latitude, double longitude) {
- // ํ๋กฌํํธ ํ
ํ๋ฆฟ์ ์๋, ๊ฒฝ๋ ๊ฐ ์ฝ์
+ public CompletableFuture analyze(List imageDataUrls, double latitude, double longitude) {
String prompt = promptTemplate
.replace(LATITUDE_PLACEHOLDER, String.format("%.6f", latitude))
.replace(LONGITUDE_PLACEHOLDER, String.format("%.6f", longitude));
- // OpenAI ํด๋ผ์ด์ธํธ๋ฅผ ํตํด ๋ถ์ ์์ฒญ
- String gptAnalysisJson = openAiClient.analyzeImages(prompt, imageDataUrls);
-
- // GPT๋ก๋ถํฐ ๋ฐ์ ์๋ณธ JSON ์๋ต์ ๋ก๊ทธ๋ก ๊ธฐ๋ก (๋๋ฒ๊น
์ฉ)
- log.info("GPT-4o๋ก๋ถํฐ ๋ฐ์ ๋ถ์ ๊ฒฐ๊ณผ (JSON): {}", gptAnalysisJson);
-
- try {
- // ๋ฐํ๋ JSON ๋ฌธ์์ด์ AnalysisResponse ๊ฐ์ฒด๋ก ๋ณํ
- return objectMapper.readValue(gptAnalysisJson, AnalysisResponse.class);
- } catch (Exception e) {
- // JSON ํ์ฑ ์คํจ ์, ์๋ณธ ์๋ต๊ณผ ํจ๊ป ์๋ฌ ๋ก๊ทธ ๊ธฐ๋ก
- log.error("GPT ์๋ต JSON ํ์ฑ ์ค ์ค๋ฅ ๋ฐ์. Response: {}", gptAnalysisJson, e);
- throw new IllegalStateException("GPT ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ํ์ฑํ๋ ๋ฐ ์คํจํ์ต๋๋ค.", e);
- }
+ return openAiClient.analyzeImages(prompt, imageDataUrls)
+ .thenApply(gptAnalysisJson -> {
+ log.info("GPT-4o๋ก๋ถํฐ ๋ฐ์ ๋ถ์ ๊ฒฐ๊ณผ (JSON): {}", gptAnalysisJson);
+ try {
+ return objectMapper.readValue(gptAnalysisJson, AnalysisResponse.class);
+ } catch (Exception e) {
+ log.error("GPT ์๋ต JSON ํ์ฑ ์ค ์ค๋ฅ ๋ฐ์. Response: {}", gptAnalysisJson, e);
+ throw new IllegalStateException("GPT ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ํ์ฑํ๋ ๋ฐ ์คํจํ์ต๋๋ค.", e);
+ }
+ });
}
}
\ No newline at end of file
diff --git a/src/main/java/ssafy/retrip/api/service/openai/OpenAiClient.java b/src/main/java/ssafy/retrip/api/service/openai/OpenAiClient.java
index dd2e9eb..f948fb0 100644
--- a/src/main/java/ssafy/retrip/api/service/openai/OpenAiClient.java
+++ b/src/main/java/ssafy/retrip/api/service/openai/OpenAiClient.java
@@ -2,13 +2,16 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
+import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@@ -29,18 +32,20 @@ public class OpenAiClient {
@Value("${openai.model}")
private String model;
- /**
- * GPT-4o์ ์ด๋ฏธ์ง์ ํ๋กฌํํธ๋ฅผ ์ ์กํ์ฌ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ JSON ํํ๋ก ๋ฐํ
- */
- public String analyzeImages(String prompt, List imageDataUrls) {
+ @Async("openAiExecutor")
+ public CompletableFuture analyzeImages(String prompt, List imageDataUrls) {
+ long startTime = System.currentTimeMillis();
+ String threadName = Thread.currentThread().getName();
+
+ log.info("OpenAI API ์์ฒญ ์์ - Thread: {}, ์์์๊ฐ: {}",
+ threadName, LocalDateTime.now());
+
try {
- // ๋ฉ์์ง ๊ตฌ์ฑ
Map message = Map.of(
"role", "user",
"content", buildContent(prompt, imageDataUrls)
);
- // API ์์ฒญ ๋ณธ๋ฌธ ๊ตฌ์ฑ
Map requestBody = Map.of(
"model", model,
"messages", List.of(message),
@@ -48,7 +53,6 @@ public String analyzeImages(String prompt, List imageDataUrls) {
"response_format", Map.of("type", "json_object")
);
- // API ํธ์ถ
String response = restClient.post()
.uri(apiUrl + "/chat/completions")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
@@ -57,26 +61,32 @@ public String analyzeImages(String prompt, List imageDataUrls) {
.retrieve()
.body(String.class);
- // ์๋ต์์ ์ฝํ
์ธ ์ถ์ถ
JsonNode jsonResponse = objectMapper.readTree(response);
- return jsonResponse.path("choices").path(0).path("message").path("content").asText();
+ long endTime = System.currentTimeMillis();
+ long executionTime = endTime - startTime;
+
+ log.info("OpenAI API ์์ฒญ ์๋ฃ - Thread: {}, ์ข
๋ฃ์๊ฐ: {}, ์คํ์๊ฐ: {}ms",
+ threadName, LocalDateTime.now(), executionTime);
+
+ String result = jsonResponse.path("choices").path(0).path("message").path("content").asText();
+
+ return CompletableFuture.completedFuture(result);
+
} catch (Exception e) {
- log.error("OpenAI API ํธ์ถ ์ค ์ค๋ฅ ๋ฐ์", e);
- throw new RuntimeException("์ด๋ฏธ์ง ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: " + e.getMessage());
+ log.error("[{}] GPT ํธ์ถ ์คํจ", threadName, e);
+ return CompletableFuture.failedFuture(
+ new RuntimeException("GPT API ํธ์ถ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: " + e.getMessage(), e)
+ );
}
}
- /**
- * OpenAI API ์์ฒญ์ ํ์ํ ์ฝํ
์ธ ๊ตฌ์ฑ
- */
+
private List