diff --git a/README.md b/README.md
new file mode 100644
index 00000000..9df7777e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,230 @@
+# ๐น Ssoul - ์นตํ
์ผ ๋ ์ํผ ๊ณต์ ํ๋ซํผ
+
+## ๐ ์๋น์ค ๋งํฌ
+**ํํ์ด์ง**: [https://ssoul.life](https://ssoul.life)
+
+## ๐ ๊ฐ์
+๋ณธ ์์คํ
์ ์ฌ์ฉ์๊ฐ ์นตํ
์ผ ๋ ์ํผ๋ฅผ ๊ณต์ ํ๊ณ , AI ๋ฐํ
๋ '์ค๋ฆฌ'๋ฅผ ํตํด ๋ง์ถคํ ์นตํ
์ผ์ ์ถ์ฒ๋ฐ์ผ๋ฉฐ, ์นตํ
์ผ ๋ฌธํ๋ฅผ ์ฆ๊ธธ ์ ์๋ ์น ์๋น์ค์
๋๋ค.
+์นตํ
์ผ ์
๋ฌธ์๋ถํฐ ์ ํธ๊ฐ๊น์ง ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ๋์์ผ๋ก, ๋จ์ํ ๋ ์ํผ ์ ๊ณต์ ๋์ด AI ์ฑ๋ด์ ํตํ ์ธํฐ๋ํฐ๋ธํ ์นตํ
์ผ ์ถ์ฒ๊ณผ ์ปค๋ฎค๋ํฐ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
+๋ํ ์ฌ์ฉ์ ํ๋ ๊ธฐ๋ฐ ๋ฑ๊ธ ์์คํ
(ABV ๋์)๊ณผ MyBar(ํต) ๊ธฐ๋ฅ์ ํตํด ๊พธ์คํ ์ฐธ์ฌ๋ฅผ ์ ๋ํ๋๋ก ์ค๊ณํ์ต๋๋ค.
+Spring Boot, Spring AI, OAuth2, SSE, AWS S3 ๋ฑ์ ํตํฉํด ์ด์ํ๋ฉฐ,
+์นตํ
์ผ ๋ฌธํ ํ์ฐ๊ณผ ์ฌ์ฉ์ ๊ฐ ๋ ์ํผ ๊ณต์ ๋ฅผ ํตํ ์ปค๋ฎค๋ํฐ ํ์ฑํ๋ฅผ ๋ชฉํ๋ก ํฉ๋๋ค.
+
+---
+
+## ๐ฅ ํ์ ๋ฐ ์ญํ
+
+| ํ์ | ์ญํ | ๋ด๋น ์
๋ฌด |
+|------|------|-----------|
+| ์ ์ฉ์ง | Backend(PM) | AI ์ฑ๋ด '์ค๋ฆฌ', ๋จ๊ณ๋ณ ์ถ์ฒ ์์คํ
|
+| ์ด๊ด์ | Backend(ํ์ฅ) / Frontend | ์นตํ
์ผ ๋๋ฉ์ธ, ๊ฒ์/ํํฐ๋ง, ์์ธ ์กฐํ |
+| ์๊ทผํธ | Backend / Frontend | ์ปค๋ฎค๋ํฐ(๊ฒ์ํ/๋๊ธ), S3, ํ์ผ ์
๋ก๋ |
+| ์ต์น์ฑ | Backend / Frontend | ์ธ์ฆ/์ธ๊ฐ, ์์
๋ก๊ทธ์ธ(OAuth2), JWT, ํ
๋ผํผ |
+| ํ๋ฏผ์ | Backend | MyBar(ํต) ๊ธฐ๋ฅ, ์๋ฆผ ์์คํ
(SSE) |
+
+---
+
+## ๐ ๊ธฐ์ ์คํ
+- **Backend**: Java 21, Spring Boot 3.5.5
+- **Database**: MySQL 8.x / JPA / H2 (๊ฐ๋ฐ)
+- **AI ์ฐ๋**: Spring AI, Gemini API
+- **์ธ์ฆ**: Spring Security, OAuth2 (Kakao, Google, Naver), JWT
+- **ํ์ผ ์ ์ฅ**: AWS S3
+- **์ค์๊ฐ ํต์ **: SSE (Server-Sent Events)
+- **์บ์ฑ**: Redis
+- **API ๋ฌธ์**: Swagger (SpringDoc OpenAPI)
+
+---
+
+## ๐ ํต์ฌ ๊ธฐ๋ฅ ํ๋ก์ธ์ค
+
+### 1๏ธโฃ AI ์ฑ๋ด '์ค๋ฆฌ' ์นตํ
์ผ ์ถ์ฒ ํ๋ก์ธ์ค
+
+```mermaid
+sequenceDiagram
+ participant User as ์ฌ์ฉ์
+ participant Controller as ChatbotController
+ participant Service as ChatbotService
+ participant AI as Spring AI (Gemini)
+ participant DB as Database
+ participant Cocktail as CocktailRepository
+
+ User->>Controller: ๋ํ ์์/๋ฉ์์ง ์ ์ก
+ Controller->>Service: ๋ฉ์์ง ์ฒ๋ฆฌ ์์ฒญ
+
+ alt ๋จ๊ณ๋ณ ์ถ์ฒ ๋ชจ๋
+ Service->>Service: ์ถ์ฒ ๋จ๊ณ ํ์ธ
+ Service->>Cocktail: ํํฐ ์กฐ๊ฑด๋ณ ์นตํ
์ผ ์กฐํ
+ Cocktail-->>Service: ์ถ์ฒ ์นตํ
์ผ ๋ชฉ๋ก
+ Service->>DB: ๋ํ ์ด๋ ฅ ์ ์ฅ
+ else ์ผ๋ฐ ๋ํ ๋ชจ๋
+ Service->>DB: ์ต๊ทผ ๋ํ ์ด๋ ฅ ์กฐํ (5๊ฑด)
+ DB-->>Service: ๋ํ ์ปจํ
์คํธ
+ Service->>AI: ํ๋กฌํํธ + ์ปจํ
์คํธ ์ ์ก
+ AI-->>Service: AI ์๋ต ์์ฑ
+ Service->>DB: ์๋ต ์ ์ฅ
+ end
+
+ Service-->>Controller: ์๋ต DTO
+ Controller-->>User: ์นตํ
์ผ ์ถ์ฒ/์ ๋ณด ์ ๊ณต
+```
+
+### 2๏ธโฃ MyBar (ํต) ๊ธฐ๋ฅ ํ๋ก์ธ์ค
+
+```mermaid
+sequenceDiagram
+ participant User as ์ฌ์ฉ์
+ participant Controller as MyBarController
+ participant Service as MyBarService
+ participant ABV as AbvScoreService
+ participant DB as MyBarRepository
+
+ User->>Controller: ์นตํ
์ผ ํต ์์ฒญ
+ Controller->>Service: keep(userId, cocktailId)
+
+ Service->>DB: ๊ธฐ์กด ํต ํ์ธ
+ DB-->>Service: ํต ์ํ ๋ฐํ
+
+ alt ์ ๊ท ํต
+ Service->>DB: MyBar ์ํฐํฐ ์์ฑ
+ Service->>ABV: ํ๋ ์ ์ +0.1
+ else ํต ๋ณต์ (DELETED โ ACTIVE)
+ Service->>DB: ์ํ ๋ณ๊ฒฝ & keptAt ๊ฐฑ์
+ Service->>ABV: ํ๋ ์ ์ +0.1
+ else ์ด๋ฏธ ํต๋ ์ํ
+ Service->>DB: keptAt๋ง ๊ฐฑ์
+ end
+
+ DB-->>Service: ์ ์ฅ ์๋ฃ
+ Service-->>Controller: ์ฑ๊ณต ์๋ต
+ Controller-->>User: 201 Created
+```
+
+### 3๏ธโฃ ์ค์๊ฐ ์๋ฆผ ์์คํ
(SSE)
+
+```mermaid
+sequenceDiagram
+ participant Client as ํด๋ผ์ด์ธํธ
+ participant Controller as NotificationController
+ participant Service as NotificationService
+ participant Emitter as SseEmitter
+ participant EventBus as ApplicationEventPublisher
+ participant PostService as PostService
+
+ Client->>Controller: SSE ๊ตฌ๋
์์ฒญ
+ Controller->>Service: subscribe()
+ Service->>Service: ์ฌ์ฉ์๋ณ Emitter ์์ฑ
+ Service->>Emitter: ์ฐ๊ฒฐ ์ด๋ฒคํธ ์ ์ก
+ Service-->>Controller: SseEmitter ๋ฐํ
+ Controller-->>Client: SSE ์คํธ๋ฆผ ์ฐ๊ฒฐ
+
+ Note over Client,Emitter: SSE ์ฐ๊ฒฐ ์ ์ง
+
+ PostService->>EventBus: ๋๊ธ/์ข์์ ์ด๋ฒคํธ ๋ฐํ
+ EventBus-->>Service: ์ด๋ฒคํธ ์์
+ Service->>Service: ์๋ฆผ ์์ฑ ๋ฐ ์ ์ฅ
+ Service->>Emitter: ์ค์๊ฐ ์๋ฆผ ์ ์ก
+ Emitter-->>Client: ์๋ฆผ ์์
+```
+
+## ๐ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
+```plaintext
+src
+โโโ main
+ โโโ java
+ โ โโโ com.back
+ โ โโโ domain # ๋๋ฉ์ธ๋ณ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง
+ โ โ โโโ user # ์ฌ์ฉ์ ๊ด๋ จ
+ โ โ โโโ cocktail # ์นตํ
์ผ ๋ ์ํผ
+ โ โ โโโ chatbot # AI ์ฑ๋ด '์ค๋ฆฌ'
+ โ โ โโโ mybar # MyBar (ํต) ๊ธฐ๋ฅ
+ โ โ โโโ post # ๊ฒ์ํ ๊ด๋ จ
+ โ โ โ โโโ category # ์นดํ
๊ณ ๋ฆฌ
+ โ โ โ โโโ comment # ๋๊ธ
+ โ โ โ โโโ post # ๊ฒ์๊ธ
+ โ โ โโโ notification # ์๋ฆผ ์์คํ
+ โ โโโ global # ์ ์ญ ๋ชจ๋
+ โ โโโ ai # Spring AI ์ค์
+ โ โโโ exception # ์์ธ ์ฒ๋ฆฌ
+ โ โโโ file # ํ์ผ ์
๋ก๋ (S3)
+ โ โโโ jwt # JWT ์ธ์ฆ
+ โ โโโ oauth2 # OAuth2 ์์
๋ก๊ทธ์ธ
+ โ โโโ rq # Request ์ปจํ
์คํธ
+ โ โโโ rsData # Response ํ์คํ
+ โ โโโ security # Spring Security ์ค์
+ โ โโโ util # ์ ํธ๋ฆฌํฐ
+ โโโ resources
+ โโโ prompts # AI ํ๋กฌํํธ
+ โ โโโ chatbot-system-prompt.txt
+ โ โโโ chatbot-response-rules.txt
+ โโโ application.yml # ๋ฉ์ธ ์ค์
+ โโโ application-dev.yml # ๊ฐ๋ฐ ํ๊ฒฝ
+ โโโ application-prod.yml # ์ด์ ํ๊ฒฝ
+ โโโ cocktails.csv # ์นตํ
์ผ ์ด๊ธฐ ๋ฐ์ดํฐ
+```
+
+---
+
+## ๐ฏ ์ฃผ์ ๊ธฐ๋ฅ
+
+### 1. ์นตํ
์ผ ๋๋ฉ์ธ
+- **์นตํ
์ผ ์กฐํ**: ๋ฌดํ์คํฌ๋กค ๊ธฐ๋ฐ ๋ชฉ๋ก ์กฐํ
+- **์์ธ ์ ๋ณด**: ๋ ์ํผ, ์ฌ๋ฃ, ์ ์กฐ๋ฒ, ์คํ ๋ฆฌ
+- **๊ฒ์/ํํฐ๋ง**: ๋์, ๋ฒ ์ด์ค, ํ์
๋ณ ํํฐ๋ง
+- **๊ณต์ ๊ธฐ๋ฅ**: ์นตํ
์ผ ๋ ์ํผ ๊ณต์ ๋งํฌ ์์ฑ
+
+### 2. AI ์ฑ๋ด '์ค๋ฆฌ'
+- **์์ฐ์ด ๋ํ**: ์นตํ
์ผ ๊ด๋ จ ์ง๋ฌธ ์๋ต
+- **๋ง์ถค ์ถ์ฒ**: ๊ธฐ๋ถ, ์ํฉ, ์ทจํฅ๋ณ ์นตํ
์ผ ์ถ์ฒ
+- **๋จ๊ณ๋ณ ์ถ์ฒ**: ๋์ โ ๋ฒ ์ด์ค โ ์คํ์ผ ์ ํ
+- **๋ํ ์ปจํ
์คํธ**: ์ต๊ทผ 5๊ฐ ๋ํ ๊ธฐ๋ฐ ์๋ต
+
+### 3. MyBar (ํต)
+- **์นตํ
์ผ ํต**: ์ข์ํ๋ ์นตํ
์ผ ์ ์ฅ
+- **๋ฌดํ์คํฌ๋กค**: ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
+- **์ํํธ ์ญ์ **: ํต ํด์ ํ ๋ณต์ ๊ฐ๋ฅ
+- **ํ๋ ์ ์**: ํต/์ธํต ์ ABV ์ ์ ๋ณ๋
+
+### 4. ์ปค๋ฎค๋ํฐ
+- **๊ฒ์ํ**: ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ฒ์๊ธ CRUD
+- **๋๊ธ**: ๊ฒ์๊ธ ๋๊ธ ์์ฑ/์กฐํ
+- **์ข์์**: ๊ฒ์๊ธ ์ถ์ฒ ๊ธฐ๋ฅ
+- **ํ๊ทธ**: ํด์ํ๊ทธ ๊ธฐ๋ฐ ๋ถ๋ฅ
+
+### 5. ์๋ฆผ ์์คํ
+- **์ค์๊ฐ ์๋ฆผ**: SSE ๊ธฐ๋ฐ ์ค์๊ฐ ํธ์
+- **์๋ฆผ ํ์
**: ๋๊ธ, ์ข์์, ํ๋ก์ฐ ๋ฑ
+- **์ฝ์ ์ฒ๋ฆฌ**: ์๋ฆผ ํ์ธ ํ ์๋ ์ด๋
+- **๋ฌดํ์คํฌ๋กค**: ์๋ฆผ ๋ชฉ๋ก ํ์ด์ง๋ค์ด์
+
+### 6. ์ธ์ฆ/์ธ๊ฐ
+- **์์
๋ก๊ทธ์ธ**: Kakao, Google, Naver OAuth2
+- **JWT ํ ํฐ**: Access/Refresh Token ๊ด๋ฆฌ
+- **Spring Security**: ๊ถํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด
+- **์ฟ ํค ์ธ์ฆ**: Secure, HttpOnly, SameSite
+
+---
+
+## ๐ ๋ณด์ ์ค์
+- **CORS**: ํ๋ก ํธ์๋ ๋๋ฉ์ธ๋ง ํ์ฉ
+- **JWT**: 15๋ถ Access, 30์ผ Refresh
+- **์ฟ ํค**: Secure(HTTPS), HttpOnly, SameSite
+- **OAuth2**: ์์
๋ก๊ทธ์ธ ํ๋ก๋ฐ์ด๋๋ณ ์ค์
+- **์์ธ ์ฒ๋ฆฌ**: ์ ์ญ ์์ธ ํธ๋ค๋ฌ
+
+---
+
+## ๐ ์ฑ๋ฅ ์ต์ ํ
+- **๋น๋๊ธฐ ์ฒ๋ฆฌ**: CompletableFuture ํ์ฉ
+- **์บ์ฑ**: Redis ์ธ์
์คํ ์ด
+- **์ปค์ ํ์ด์ง**: Offset ๋์ ์ปค์ ๊ธฐ๋ฐ
+- **Lazy Loading**: JPA ์ง์ฐ ๋ก๋ฉ
+- **์ธ๋ฑ์ฑ**: ๊ฒ์ ํ๋ DB ์ธ๋ฑ์ค
+
+---
+
+## ๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง
+
+
+
+---
diff --git a/build.gradle.kts b/build.gradle.kts
index dc4cc1d1..e865456e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -38,6 +38,8 @@ dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
+ implementation("org.springframework.boot:spring-boot-starter-batch")
+ testImplementation("org.springframework.batch:spring-batch-test")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
compileOnly("org.projectlombok:lombok")
diff --git a/src/main/java/com/back/domain/chatbot/controller/ChatbotController.java b/src/main/java/com/back/domain/chatbot/controller/ChatbotController.java
index e274d012..27b10971 100644
--- a/src/main/java/com/back/domain/chatbot/controller/ChatbotController.java
+++ b/src/main/java/com/back/domain/chatbot/controller/ChatbotController.java
@@ -38,9 +38,9 @@ public ResponseEntity> sendMessage(@Valid @RequestBody C
@GetMapping("/history/user/{userId}")
@Operation(summary = "์ ์ ๋ํ ํ์คํ ๋ฆฌ", description = "์ฌ์ฉ์ ์ฑํ
๊ธฐ๋ก ์กฐํ")
- public ResponseEntity>> getUserChatHistory(@PathVariable Long userId) {
+ public ResponseEntity>> getUserChatHistory(@PathVariable Long userId) {
try {
- List history = chatbotService.getUserChatHistory(userId);
+ List history = chatbotService.getUserChatHistory(userId);
return ResponseEntity.ok(RsData.successOf(history));
} catch (Exception e) {
log.error("์ฌ์ฉ์ ์ฑํ
๊ธฐ๋ก ์กฐํ ์ค ์ค๋ฅ ๋ฐ์: ", e);
diff --git a/src/main/java/com/back/domain/chatbot/dto/ChatRequestDto.java b/src/main/java/com/back/domain/chatbot/dto/ChatRequestDto.java
index 8045d01e..cd080d2e 100644
--- a/src/main/java/com/back/domain/chatbot/dto/ChatRequestDto.java
+++ b/src/main/java/com/back/domain/chatbot/dto/ChatRequestDto.java
@@ -15,10 +15,18 @@ public class ChatRequestDto {
private Long userId;
+ private String selectedValue; // ์: "NON_ALCOHOLIC"
// ๋จ๊ณ๋ณ ์ถ์ฒ ๊ด๋ จ ํ๋๋ค
+ /**
+ * @deprecated currentStep ํ๋๋ฅผ ์ฌ์ฉํ์ธ์. ์ด ํ๋๋ ํ์ ํธํ์ฑ์ ์ํด ์ ์ง๋ฉ๋๋ค.
+ */
+ @Deprecated
private boolean isStepRecommendation = false;
+
private Integer currentStep;
- private String selectedAlcoholStrength; // "ALL" ์ฒ๋ฆฌ๋ฅผ ์ํด ์คํ
3๊ฐ String์ผ๋ก ๋ณ๊ฒฝ
+ // "ALL" ์ฒ๋ฆฌ๋ฅผ ์ํด ์คํ
2๊ฐ String์ผ๋ก ๋ณ๊ฒฝ
+ private String selectedAlcoholStrength;
private String selectedAlcoholBaseType;
private String selectedCocktailType;
+
}
\ No newline at end of file
diff --git a/src/main/java/com/back/domain/chatbot/dto/ChatResponseDto.java b/src/main/java/com/back/domain/chatbot/dto/ChatResponseDto.java
index 59170e87..533eab5f 100644
--- a/src/main/java/com/back/domain/chatbot/dto/ChatResponseDto.java
+++ b/src/main/java/com/back/domain/chatbot/dto/ChatResponseDto.java
@@ -1,5 +1,6 @@
package com.back.domain.chatbot.dto;
+import com.back.domain.chatbot.enums.MessageSender;
import com.back.domain.chatbot.enums.MessageType;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -16,9 +17,12 @@
@Builder
public class ChatResponseDto {
+ private Long id; // ๋ฉ์์ง ID (DB ์ ์ฅ ํ ์์ฑ)
+ private Long userId; // ์ฌ์ฉ์ ID
private String message; // ํ
์คํธ ๋ฉ์์ง
+ private MessageSender sender; // ๋ฉ์์ง ๋ฐ์ ์ (USER / CHATBOT)
private MessageType type; // ๋ฉ์์ง ํ์ ํ์
- private LocalDateTime timestamp;
+ private LocalDateTime createdAt; // ์์ฑ ์๊ฐ (timestamp โ createdAt์ผ๋ก ๋ณ๊ฒฝ)
// ๋จ๊ณ๋ณ ์ถ์ฒ ๊ด๋ จ ๋ฐ์ดํฐ (type์ด RADIO_OPTIONS ๋๋ CARD_LIST์ผ ๋ ์ฌ์ฉ)
private StepRecommendationResponseDto stepData;
@@ -30,12 +34,14 @@ public class ChatResponseDto {
public ChatResponseDto(String message) {
this.message = message;
this.type = MessageType.TEXT;
- this.timestamp = LocalDateTime.now();
+ this.sender = MessageSender.CHATBOT;
+ this.createdAt = LocalDateTime.now();
}
public ChatResponseDto(String message, StepRecommendationResponseDto stepData) {
this.message = message;
- this.timestamp = LocalDateTime.now();
+ this.sender = MessageSender.CHATBOT;
+ this.createdAt = LocalDateTime.now();
this.stepData = stepData;
// stepData ๋ด์ฉ์ ๋ฐ๋ผ type ์๋ ์ค์
diff --git a/src/main/java/com/back/domain/chatbot/entity/ChatConversation.java b/src/main/java/com/back/domain/chatbot/entity/ChatConversation.java
index b6b572f2..dc34a450 100644
--- a/src/main/java/com/back/domain/chatbot/entity/ChatConversation.java
+++ b/src/main/java/com/back/domain/chatbot/entity/ChatConversation.java
@@ -34,4 +34,8 @@ public class ChatConversation {
@CreatedDate
private LocalDateTime createdAt;
+
+ // refactor#256 - metadata ํ๋ ์ถ๊ฐ
+ @Column(columnDefinition = "TEXT")
+ private String metadata;
}
\ No newline at end of file
diff --git a/src/main/java/com/back/domain/chatbot/enums/MessageType.java b/src/main/java/com/back/domain/chatbot/enums/MessageType.java
index 11e71ace..bf21c6c2 100644
--- a/src/main/java/com/back/domain/chatbot/enums/MessageType.java
+++ b/src/main/java/com/back/domain/chatbot/enums/MessageType.java
@@ -5,7 +5,8 @@ public enum MessageType {
RADIO_OPTIONS("๋ผ๋์ค์ต์
"), // ๋ผ๋์ค ๋ฒํผ ์ ํ์ง
CARD_LIST("์นด๋๋ฆฌ์คํธ"), // ์นตํ
์ผ ์ถ์ฒ ์นด๋ ๋ฆฌ์คํธ
LOADING("๋ก๋ฉ์ค"), // ๋ก๋ฉ ๋ฉ์์ง
- ERROR("์๋ฌ"); // ์๋ฌ ๋ฉ์์ง
+ ERROR("์๋ฌ"), // ์๋ฌ ๋ฉ์์ง
+ INPUT("์
๋ ฅ"); // ํ
์คํธ ์
๋ ฅ ์์ฒญ
private final String description;
diff --git a/src/main/java/com/back/domain/chatbot/repository/ChatConversationRepository.java b/src/main/java/com/back/domain/chatbot/repository/ChatConversationRepository.java
index 6e8dd106..ceac44a6 100644
--- a/src/main/java/com/back/domain/chatbot/repository/ChatConversationRepository.java
+++ b/src/main/java/com/back/domain/chatbot/repository/ChatConversationRepository.java
@@ -11,6 +11,8 @@ public interface ChatConversationRepository extends JpaRepository findByUserIdOrderByCreatedAtDesc(Long userId);
+ List findByUserIdOrderByCreatedAtAsc(Long userId);
+
List findTop20ByUserIdOrderByCreatedAtDesc(Long userId);
boolean existsByUserIdAndMessage(Long userId, String message);
diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java
index ea4abb39..c8e2a4dd 100644
--- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java
+++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java
@@ -14,6 +14,8 @@
import com.back.domain.cocktail.enums.AlcoholStrength;
import com.back.domain.cocktail.enums.CocktailType;
import com.back.domain.cocktail.repository.CocktailRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +35,7 @@
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
@Service
@@ -44,6 +47,8 @@ public class ChatbotService {
private final ChatConversationRepository chatConversationRepository;
private final CocktailRepository cocktailRepository;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
@Value("classpath:prompts/chatbot-system-prompt.txt")
private Resource systemPromptResource;
@@ -88,41 +93,336 @@ public void init() throws IOException {
@Transactional
public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
+ saveUserMessage(requestDto);
+
try {
- // ๋จ๊ณ๋ณ ์ถ์ฒ ๋ชจ๋ ํ์ธ (currentStep์ด ์์ผ๋ฉด ๋ฌด์กฐ๊ฑด ๋จ๊ณ๋ณ ์ถ์ฒ ๋ชจ๋)
- if (requestDto.isStepRecommendation() ||
- requestDto.getCurrentStep() != null ||
- isStepRecommendationTrigger(requestDto.getMessage())) {
- log.info("Recommendation chat mode for userId: {}", requestDto.getUserId());
+ Integer currentStep = requestDto.getCurrentStep();
+
+ // ========== 1์์: currentStep ๋ช
์์ ์ ์ด ==========
+ if (currentStep != null) {
+ log.info("[EXPLICIT] currentStep={}, userId={}, mode={}",
+ currentStep, requestDto.getUserId(),
+ currentStep == 0 ? "QA" : "STEP");
+
+ if (currentStep == 0) {
+ // ์ง๋ฌธํ ์ถ์ฒ ์ ํ ์ ์๋ด ๋ฉ์์ง์ INPUT ํ์
๋ฐํ
+ if ("QA".equalsIgnoreCase(requestDto.getMessage()) ||
+ requestDto.getMessage().contains("์ง๋ฌธํ")) {
+
+ log.info("์ง๋ฌธํ ์ถ์ฒ ์์ - userId: {}", requestDto.getUserId());
+
+ // ์ฌ์ฉ์ ์ ํ ๋ฉ์์ง ์ ์ฅ
+ ChatConversation userChoice = ChatConversation.builder()
+ .userId(requestDto.getUserId())
+ .message("์ง๋ฌธํ ์ทจํฅ ์ฐพ๊ธฐ")
+ .sender(MessageSender.USER)
+ .createdAt(LocalDateTime.now())
+ .build();
+ chatConversationRepository.save(userChoice);
+
+ String guideMessage = "์นตํ
์ผ์ ๊ด๋ จ๋ ์ง๋ฌธ์ ์
๋ ฅํด์ฃผ์ธ์!";
+
+ ChatConversation botGuide = ChatConversation.builder()
+ .userId(requestDto.getUserId())
+ .message(guideMessage)
+ .sender(MessageSender.CHATBOT)
+ .createdAt(LocalDateTime.now())
+ .build();
+ ChatConversation savedGuide = chatConversationRepository.save(botGuide);
+
+ // INPUT ํ์
์ผ๋ก ๋ฐํํ์ฌ ์ฌ์ฉ์ ์
๋ ฅ ์ ๋
+ return ChatResponseDto.builder()
+ .id(savedGuide.getId())
+ .userId(requestDto.getUserId())
+ .message(guideMessage)
+ .sender(MessageSender.CHATBOT)
+ .type(MessageType.INPUT)
+ .createdAt(savedGuide.getCreatedAt())
+ .metaData(ChatResponseDto.MetaData.builder()
+ .currentStep(0)
+ .actionType("์ง๋ฌธํ ์ถ์ฒ")
+ .build())
+ .build();
+ }
+
+ // ์ค์ ์ง๋ฌธ์ด ๋ค์ด์จ ๊ฒฝ์ฐ - AI ๊ธฐ๋ฐ ์นตํ
์ผ ์ถ์ฒ
+ log.info("์ง๋ฌธํ ์ถ์ฒ ๋ชจ๋ ์ง์
- userId: {}", requestDto.getUserId());
+ return generateQARecommendation(requestDto);
+ }
+ else if (currentStep >= 1 && currentStep <= 4) {
+ // ๋จ๊ณ๋ณ ์ถ์ฒ
+ log.info("๋จ๊ณ๋ณ ์ถ์ฒ ๋ชจ๋ ์ง์
- Step: {}, userId: {}",
+ currentStep, requestDto.getUserId());
+ return handleStepRecommendation(requestDto);
+ }
+ else {
+ // ์ ํจํ์ง ์์ step ๊ฐ
+ log.warn("์ ํจํ์ง ์์ currentStep: {}, userId: {}", currentStep, requestDto.getUserId());
+ return createErrorResponse("์๋ชป๋ ๋จ๊ณ ์ ๋ณด์
๋๋ค.");
+ }
+ }
+
+ // ========== 2์์: ํค์๋ ๊ฐ์ง (ํ์ ํธํ์ฑ) ==========
+ if (isStepRecommendationTrigger(requestDto.getMessage())) {
+ log.info("[LEGACY] ํค์๋ ๊ธฐ๋ฐ ๋จ๊ณ๋ณ ์ถ์ฒ ๊ฐ์ง - userId: {}", requestDto.getUserId());
+ requestDto.setCurrentStep(1);
return handleStepRecommendation(requestDto);
}
- // ์ผ๋ฐ ๋ํ ๋ชจ๋
- String response = generateAIResponse(requestDto);
+ // ========== 3์์: ๊ธฐ๋ณธ ์ผ๋ฐ ๋ํ ==========
+ log.info("[DEFAULT] ์ผ๋ฐ ๋ํ ๋ชจ๋ - userId: {}", requestDto.getUserId());
+ ChatConversation savedResponse = generateAIResponse(requestDto);
- // ์ผ๋ฐ ํ
์คํธ ์๋ต ์์ฑ (type์ด ์๋์ผ๋ก TEXT๋ก ์ค์ ๋จ)
return ChatResponseDto.builder()
- .message(response)
+ .id(savedResponse.getId())
+ .userId(requestDto.getUserId())
+ .message(savedResponse.getMessage())
+ .sender(MessageSender.CHATBOT)
.type(MessageType.TEXT)
- .timestamp(LocalDateTime.now())
+ .createdAt(savedResponse.getCreatedAt())
.build();
} catch (Exception e) {
log.error("์ฑํ
์๋ต ์์ฑ ์ค ์ค๋ฅ ๋ฐ์: ", e);
+ return createErrorResponse("์ฃ์กํฉ๋๋ค. ์ผ์์ ์ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+ }
+ }
- // ์๋ฌ ์๋ต
- return ChatResponseDto.builder()
- .message("์ฃ์กํฉ๋๋ค. ์ผ์์ ์ธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.")
- .type(MessageType.ERROR)
- .timestamp(LocalDateTime.now())
- .build();
+ /**
+ * ์ง๋ฌธํ ์ถ์ฒ - AI๊ฐ ์ง๋ฌธ์ ๋ถ์ํ์ฌ ์นตํ
์ผ ์ถ์ฒ
+ */
+ private ChatResponseDto generateQARecommendation(ChatRequestDto requestDto) {
+ String userQuestion = requestDto.getMessage();
+
+ // 1. AI๋ฅผ ํตํด ์ฌ์ฉ์ ์ง๋ฌธ ๋ถ์ ๋ฐ ์ถ์ฒ ์นตํ
์ผ ๋ชฉ๋ก ์์ฑ
+ List recommendedCocktailNames = analyzeCocktailRequest(userQuestion);
+
+ // 2. DB์์ ์นตํ
์ผ ๊ฒ์ (์ต๋ 7๊ฐ ๊ฒ์ํ์ฌ 3๊ฐ ์ ํ)
+ List recommendations = new ArrayList<>();
+ for (String cocktailName : recommendedCocktailNames) {
+ if (recommendations.size() >= 3) break;
+
+ // ์นตํ
์ผ ์ด๋ฆ์ผ๋ก ๊ฒ์
+ Page cocktailPage = cocktailRepository.searchWithFilters(
+ cocktailName,
+ null,
+ null,
+ null,
+ PageRequest.of(0, 1)
+ );
+
+ if (!cocktailPage.isEmpty()) {
+ Cocktail cocktail = cocktailPage.getContent().get(0);
+ recommendations.add(new CocktailSummaryResponseDto(
+ cocktail.getId(),
+ cocktail.getCocktailName(),
+ cocktail.getCocktailNameKo(),
+ cocktail.getCocktailImgUrl(),
+ cocktail.getAlcoholStrength().getDescription()
+ ));
+ }
+ }
+
+ // 3. ์ถ์ฒ ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ์ผ๋ฐ ํ
์คํธ ์๋ต
+ if (recommendations.isEmpty()) {
+ return generateTextResponse(requestDto, userQuestion);
+ }
+
+ // 4. AI๋ฅผ ํตํด ์ถ์ฒ ๋ฉ์์ง ์์ฑ
+ String recommendationMessage = generateRecommendationMessage(userQuestion, recommendations);
+
+ // 5. RESTART ์ต์
์ถ๊ฐ
+ List restartOption = List.of(
+ new StepRecommendationResponseDto.StepOption(
+ "RESTART",
+ "๋ค์ ์์ํ๊ธฐ",
+ null
+ )
+ );
+
+ // 6. StepRecommendationResponseDto ์์ฑ
+ StepRecommendationResponseDto stepData = new StepRecommendationResponseDto(
+ 0, // ์ง๋ฌธํ์ step 0
+ recommendationMessage,
+ restartOption, // RESTART ์ต์
์ถ๊ฐ
+ recommendations,
+ true
+ );
+
+ // 7. ๋ด ์๋ต ์ ์ฅ
+ ChatConversation savedResponse = saveBotResponse(
+ requestDto.getUserId(),
+ recommendationMessage,
+ stepData
+ );
+
+ // 8. ChatResponseDto ๋ฐํ
+ return ChatResponseDto.builder()
+ .id(savedResponse.getId())
+ .userId(requestDto.getUserId())
+ .message(recommendationMessage)
+ .sender(MessageSender.CHATBOT)
+ .type(MessageType.CARD_LIST)
+ .stepData(stepData)
+ .createdAt(savedResponse.getCreatedAt())
+ .metaData(ChatResponseDto.MetaData.builder()
+ .currentStep(0)
+ .actionType("์ง๋ฌธํ ์ถ์ฒ")
+ .build())
+ .build();
+ }
+
+ /**
+ * AI๋ฅผ ํตํด ์ฌ์ฉ์ ์ง๋ฌธ ๋ถ์ํ์ฌ ์ถ์ฒํ ์นตํ
์ผ ์ด๋ฆ ๋ชฉ๋ก ๋ฐํ
+ */
+ private List analyzeCocktailRequest(String userQuestion) {
+ String analysisPrompt = """
+ ์ฌ์ฉ์๊ฐ ๋ค์๊ณผ ๊ฐ์ ์นตํ
์ผ ๊ด๋ จ ์ง๋ฌธ์ ํ์ต๋๋ค:
+ "%s"
+
+ ์ด ์ง๋ฌธ์ ๊ฐ์ฅ ์ ํฉํ ์นตํ
์ผ์ ์ต๋ 7๊ฐ๊น์ง ์ถ์ฒํด์ฃผ์ธ์.
+ ๋ค์ ํ์์ผ๋ก๋ง ์๋ตํ์ธ์ (์นตํ
์ผ ์ด๋ฆ๋ง, ํ ์ค์ ํ๋์ฉ):
+ ์นตํ
์ผ์ด๋ฆ1
+ ์นตํ
์ผ์ด๋ฆ2
+ ์นตํ
์ผ์ด๋ฆ3
+ ...
+
+ ์ฃผ์์ฌํญ:
+ - ์๋ฌธ ์นตํ
์ผ ์ด๋ฆ๋ง ์์ฑ
+ - ๋ถ๊ฐ ์ค๋ช
์์ด ์นตํ
์ผ ์ด๋ฆ๋ง
+ - ์ค์ ์กด์ฌํ๋ ์ ๋ช
ํ ์นตํ
์ผ๋ง ์ถ์ฒ
+ """.formatted(userQuestion);
+
+ try {
+ String response = chatClient.prompt()
+ .system("๋น์ ์ ์นตํ
์ผ ์ ๋ฌธ๊ฐ์
๋๋ค. ์ฌ์ฉ์ ์ง๋ฌธ์ ๋ง๋ ์นตํ
์ผ์ ์ถ์ฒํฉ๋๋ค.")
+ .user(analysisPrompt)
+ .options(OpenAiChatOptions.builder()
+ .withTemperature(0.7)
+ .withMaxTokens(150)
+ .build())
+ .call()
+ .content();
+
+ // ์๋ต์ ์ค ๋จ์๋ก ํ์ฑํ์ฌ ์นตํ
์ผ ์ด๋ฆ ๋ชฉ๋ก ์์ฑ
+ List cocktailNames = response.lines()
+ .map(String::trim)
+ .filter(line -> !line.isEmpty())
+ .limit(7)
+ .collect(Collectors.toList());
+
+ log.info("AI ์ถ์ฒ ์นตํ
์ผ ๋ชฉ๋ก: {}", cocktailNames);
+ return cocktailNames;
+
+ } catch (Exception e) {
+ log.error("์นตํ
์ผ ๋ถ์ ์ค ์ค๋ฅ: ", e);
+ // ์ค๋ฅ ์ ๊ธฐ๋ณธ ์นตํ
์ผ ๋ชฉ๋ก ๋ฐํ
+ return List.of("Mojito", "Margarita", "Cosmopolitan", "Martini", "Daiquiri");
}
}
- // ============ ์์ ๋ ๋ฉ์๋๋ค ============
+ /**
+ * AI๋ฅผ ํตํด ์ถ์ฒ ๋ฉ์์ง ์์ฑ
+ */
+ private String generateRecommendationMessage(String userQuestion, List recommendations) {
+ String cocktailList = recommendations.stream()
+ .map(c -> c.cocktailNameKo() != null ? c.cocktailNameKo() : c.cocktailName())
+ .collect(Collectors.joining(", "));
+
+ String messagePrompt = """
+ ์ฌ์ฉ์๊ฐ "%s"๋ผ๊ณ ์ง๋ฌธํ์ต๋๋ค.
+
+ ๋ค์ ์นตํ
์ผ๋ค์ ์ถ์ฒํฉ๋๋ค: %s
+
+ ์ฌ์ฉ์์ ์ง๋ฌธ์ ๋ฐ์ํ ์น๊ทผํ ์ถ์ฒ ๋ฉ์์ง๋ฅผ 100์ ์ด๋ด๋ก ์์ฑํด์ฃผ์ธ์.
+ '์ค๋ฆฌ'๋ผ๋ ๋ฐํ
๋ ์บ๋ฆญํฐ๋ก ๋ต๋ณํ๋ฉฐ, ์ฌ์ฉ์ ์ง๋ฌธ์ ํต์ฌ์ ์ธ๊ธํ๋ฉด์ ์นตํ
์ผ ์ถ์ฒ์ ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐํ์ธ์.
+ ์ด๋ชจ์ง๋ฅผ 1-2๊ฐ ํฌํจํ์ธ์.
+ """.formatted(userQuestion, cocktailList);
+
+ try {
+ String message = chatClient.prompt()
+ .system(systemPrompt)
+ .user(messagePrompt)
+ .options(OpenAiChatOptions.builder()
+ .withTemperature(0.8)
+ .withMaxTokens(100)
+ .build())
+ .call()
+ .content();
+
+ message += "\n์นตํ
์ผ์ ์์ธํ ์ ๋ณด๋ '์์ธ๋ณด๊ธฐ'๋ฅผ ํด๋ฆญํด์ ํ์ธํ ์ ์์ด์.\n" +
+ "๋ง์์ ๋๋ ์นตํ
์ผ์ 'ํต' ๋ฒํผ์ ๋๋ฌ ๋๋ง์ Bar์ ์ ์ฅํด๋ณด์ธ์!";
+
+ return message.trim();
+
+ } catch (Exception e) {
+ log.error("์ถ์ฒ ๋ฉ์์ง ์์ฑ ์ค ์ค๋ฅ: ", e);
+ return "๐น ์์ฒญํ์ ์นตํ
์ผ์ ์ฐพ์๋ดค์ด์! ์ค๋ฆฌ๊ฐ ์์ ํ ์นตํ
์ผ๋ค์ ์ถ์ฒํด๋๋ฆด๊ฒ์.";
+ }
+ }
/**
- * ๋ํ ์ปจํ
์คํธ ๋น๋ - ๋ณ๊ฒฝ์ฌํญ: sender๋ก ๊ตฌ๋ถํ์ฌ ๋ํ ์ฌ๊ตฌ์ฑ
+ * ์ถ์ฒํ ์นตํ
์ผ์ด ์์ ๊ฒฝ์ฐ ์ผ๋ฐ ํ
์คํธ ์๋ต ์์ฑ
+ */
+ private ChatResponseDto generateTextResponse(ChatRequestDto requestDto, String userQuestion) {
+ ChatConversation savedResponse = generateAIResponse(requestDto);
+
+ return ChatResponseDto.builder()
+ .id(savedResponse.getId())
+ .userId(requestDto.getUserId())
+ .message(savedResponse.getMessage())
+ .sender(MessageSender.CHATBOT)
+ .type(MessageType.TEXT)
+ .createdAt(savedResponse.getCreatedAt())
+ .metaData(ChatResponseDto.MetaData.builder()
+ .currentStep(0)
+ .actionType("์ง๋ฌธํ ์ถ์ฒ")
+ .build())
+ .build();
+ }
+
+ private void saveUserMessage(ChatRequestDto requestDto) {
+ String metadata = null;
+ if (requestDto.getSelectedValue() != null) {
+ try {
+ metadata = objectMapper.writeValueAsString(Map.of("selectedValue", requestDto.getSelectedValue()));
+ } catch (JsonProcessingException e) {
+ log.error("์ฌ์ฉ์ ์ ํ ๊ฐ JSON ์ง๋ ฌํ ์คํจ", e);
+ }
+ }
+
+ ChatConversation userMessage = ChatConversation.builder()
+ .userId(requestDto.getUserId())
+ .message(requestDto.getMessage())
+ .sender(MessageSender.USER)
+ .createdAt(LocalDateTime.now())
+ .metadata(metadata)
+ .build();
+ chatConversationRepository.save(userMessage);
+ }
+
+ private ChatConversation saveBotResponse(Long userId, String message, Object stepData) {
+ String metadata = null;
+ if (stepData != null) {
+ try {
+ metadata = objectMapper.writeValueAsString(stepData);
+ } catch (JsonProcessingException e) {
+ log.error("๋ด ์๋ต ๋ฉํ๋ฐ์ดํฐ JSON ์ง๋ ฌํ ์คํจ", e);
+ }
+ }
+
+ ChatConversation botResponse = ChatConversation.builder()
+ .userId(userId)
+ .message(message)
+ .sender(MessageSender.CHATBOT)
+ .createdAt(LocalDateTime.now())
+ .metadata(metadata)
+ .build();
+ return chatConversationRepository.save(botResponse);
+ }
+
+ /**
+ * ๋ํ ์ปจํ
์คํธ ๋น๋
*/
private String buildConversationContext(List recentChats) {
if (recentChats.isEmpty()) {
@@ -131,7 +431,6 @@ private String buildConversationContext(List recentChats) {
StringBuilder context = new StringBuilder("\n\nใ์ต๊ทผ ๋ํ ๊ธฐ๋กใ\n");
- // ์๊ฐ ์ญ์์ผ๋ก ์ ๋ ฌ๋ ๋ฆฌ์คํธ๋ฅผ ์๊ฐ์์ผ๋ก ์ฌ์ ๋ ฌ
List orderedChats = new ArrayList<>(recentChats);
orderedChats.sort((a, b) -> a.getCreatedAt().compareTo(b.getCreatedAt()));
@@ -147,12 +446,8 @@ private String buildConversationContext(List recentChats) {
return context.toString();
}
- /**
- * ๋ํ ์ ์ฅ - ๋ณ๊ฒฝ์ฌํญ: ์ฌ์ฉ์ ๋ฉ์์ง์ ๋ด ์๋ต์ ๊ฐ๊ฐ ๋ณ๋๋ก ์ ์ฅ
- */
@Transactional
- public void saveConversation(ChatRequestDto requestDto, String response) {
- // 1. ์ฌ์ฉ์ ๋ฉ์์ง ์ ์ฅ
+ public ChatConversation saveConversation(ChatRequestDto requestDto, String response) {
ChatConversation userMessage = ChatConversation.builder()
.userId(requestDto.getUserId())
.message(requestDto.getMessage())
@@ -161,28 +456,55 @@ public void saveConversation(ChatRequestDto requestDto, String response) {
.build();
chatConversationRepository.save(userMessage);
- // 2. ๋ด ์๋ต ์ ์ฅ
ChatConversation botResponse = ChatConversation.builder()
.userId(requestDto.getUserId())
.message(response)
.sender(MessageSender.CHATBOT)
.createdAt(LocalDateTime.now())
.build();
- chatConversationRepository.save(botResponse);
+ return chatConversationRepository.save(botResponse);
}
- /**
- * ์ฌ์ฉ์ ์ฑํ
๊ธฐ๋ก ์กฐํ - ๋ณ๊ฒฝ์ฌํญ: sender ๊ตฌ๋ถ ์์ด ๋ชจ๋ ๋ฉ์์ง ์๊ฐ์์ผ๋ก ์กฐํ
- */
@Transactional(readOnly = true)
- public List getUserChatHistory(Long userId) {
- return chatConversationRepository.findByUserIdOrderByCreatedAtDesc(userId);
+ public List getUserChatHistory(Long userId) {
+ List history = chatConversationRepository.findByUserIdOrderByCreatedAtAsc(userId);
+
+ return history.stream().map(conversation -> {
+ ChatResponseDto.ChatResponseDtoBuilder builder = ChatResponseDto.builder()
+ .id(conversation.getId())
+ .userId(conversation.getUserId())
+ .message(conversation.getMessage())
+ .sender(conversation.getSender())
+ .createdAt(conversation.getCreatedAt());
+
+ String metadata = conversation.getMetadata();
+ if (metadata != null && !metadata.isEmpty()) {
+ try {
+ if (conversation.getSender() == MessageSender.CHATBOT) {
+ StepRecommendationResponseDto stepData = objectMapper.readValue(metadata, StepRecommendationResponseDto.class);
+ builder.stepData(stepData);
+
+ if (stepData.getOptions() != null && !stepData.getOptions().isEmpty()) {
+ builder.type(MessageType.RADIO_OPTIONS);
+ } else if (stepData.getRecommendations() != null && !stepData.getRecommendations().isEmpty()) {
+ builder.type(MessageType.CARD_LIST);
+ } else {
+ builder.type(MessageType.TEXT);
+ }
+ } else {
+ builder.type(MessageType.TEXT);
+ }
+ } catch (JsonProcessingException e) {
+ log.error("๋ํ ๊ธฐ๋ก metadata ์ญ์ง๋ ฌํ ์คํจ [ID: {}]: {}", conversation.getId(), e.getMessage());
+ builder.type(MessageType.TEXT);
+ }
+ } else {
+ builder.type(MessageType.TEXT);
+ }
+ return builder.build();
+ }).collect(Collectors.toList());
}
- /**
- * FE์์ ์์ฑํ ๋ด ๋ฉ์์ง๋ฅผ DB์ ์ ์ฅ
- * ์: ์ธ์ฌ๋ง, ์๋ด ๋ฉ์์ง, ์๋ฌ ๋ฉ์์ง ๋ฑ
- */
@Transactional
public ChatConversation saveBotMessage(SaveBotMessageDto dto) {
ChatConversation botMessage = ChatConversation.builder()
@@ -195,19 +517,12 @@ public ChatConversation saveBotMessage(SaveBotMessageDto dto) {
return chatConversationRepository.save(botMessage);
}
- /**
- * ๊ธฐ๋ณธ ์ธ์ฌ๋ง ์์ฑ ๋ฐ ์ ์ฅ
- * ์ฑํ
์์ ์ ํธ์ถํ์ฌ ์ธ์ฌ๋ง์ DB์ ์ ์ฅ
- * ์ด๋ฏธ ๋์ผํ ์ธ์ฌ๋ง์ด ์กด์ฌํ๋ฉด ์ค๋ณต ์ ์ฅํ์ง ์์
- * MessageType.RADIO_OPTIONS์ options ๋ฐ์ดํฐ๋ฅผ ํฌํจํ ChatResponseDto ๋ฐํ
- */
@Transactional
public ChatResponseDto createGreetingMessage(Long userId) {
String greetingMessage = "์๋
ํ์ธ์! ๐น ๋ฐํ
๋ '์ค๋ฆฌ'์์.\n" +
"์ทจํฅ์ ๋ง๋ ์นตํ
์ผ์ ์ถ์ฒํด๋๋ฆด๊ฒ์!\n" +
"์ด๋ค ์ ํ์ผ๋ก ์ฐพ์๋๋ฆด๊น์?";
- // ์ ํ ์ต์
์์ฑ
List options = List.of(
new StepRecommendationResponseDto.StepOption(
"QA",
@@ -221,19 +536,17 @@ public ChatResponseDto createGreetingMessage(Long userId) {
)
);
- // StepRecommendationResponseDto ์์ฑ
StepRecommendationResponseDto stepData = new StepRecommendationResponseDto(
- 0, // ์ธ์ฌ๋ง์ step 0
+ 0,
greetingMessage,
options,
null,
false
);
- // ์ค๋ณต ํ์ธ: ๋์ผํ ์ธ์ฌ๋ง์ด ์ด๋ฏธ ์กด์ฌํ๋์ง ํ์ธ
boolean greetingExists = chatConversationRepository.existsByUserIdAndMessage(userId, greetingMessage);
- // ์ค๋ณต๋์ง ์์ ๊ฒฝ์ฐ์๋ง DB์ ์ ์ฅ
+ ChatConversation savedGreeting = null;
if (!greetingExists) {
ChatConversation greeting = ChatConversation.builder()
.userId(userId)
@@ -241,36 +554,31 @@ public ChatResponseDto createGreetingMessage(Long userId) {
.sender(MessageSender.CHATBOT)
.createdAt(LocalDateTime.now())
.build();
- chatConversationRepository.save(greeting);
+ savedGreeting = chatConversationRepository.save(greeting);
log.info("์ธ์ฌ๋ง ์ ์ฅ ์๋ฃ - userId: {}", userId);
} else {
log.info("์ด๋ฏธ ์ธ์ฌ๋ง์ด ์กด์ฌํ์ฌ ์ ์ฅ ์๋ต - userId: {}", userId);
}
- // ChatResponseDto ๋ฐํ
return ChatResponseDto.builder()
+ .id(savedGreeting != null ? savedGreeting.getId() : null)
+ .userId(userId)
.message(greetingMessage)
+ .sender(MessageSender.CHATBOT)
.type(MessageType.RADIO_OPTIONS)
.stepData(stepData)
- .timestamp(LocalDateTime.now())
+ .createdAt(LocalDateTime.now())
.build();
}
- /**
- * ์ฌ์ฉ์์ ์ฒซ ๋ํ ์ฌ๋ถ ํ์ธ
- * ์ฒซ ๋ํ์ธ ๊ฒฝ์ฐ ์ธ์ฌ๋ง ์๋ ์์ฑ์ ํ์ฉ ๊ฐ๋ฅ
- */
@Transactional(readOnly = true)
public boolean isFirstConversation(Long userId) {
return chatConversationRepository.findTop20ByUserIdOrderByCreatedAtDesc(userId).isEmpty();
}
- // ============ ๊ธฐ์กด ๋ฉ์๋๋ค (๋ณ๊ฒฝ ์์) ============
-
private String buildSystemMessage(InternalMessageType type) {
StringBuilder sb = new StringBuilder(systemPrompt);
- // ๋ฉ์์ง ํ์
๋ณ ์ถ๊ฐ ์ง์์ฌํญ
switch (type) {
case RECIPE:
sb.append("\n\nใ๋ ์ํผ ๋ต๋ณ ๋ชจ๋ใ์ ํํ ์ฌ๋ฃ ๋น์จ๊ณผ ์ ์กฐ ์์๋ฅผ ๊ฐ์กฐํ์ธ์.");
@@ -295,15 +603,15 @@ private String buildUserMessage(String userMessage, InternalMessageType type) {
private OpenAiChatOptions getOptionsForMessageType(InternalMessageType type) {
return switch (type) {
case RECIPE -> OpenAiChatOptions.builder()
- .withTemperature(0.3) // ์ ํ์ฑ ์ค์
- .withMaxTokens(400) // ๋ ์ํผ๋ ๊ธธ๊ฒ
+ .withTemperature(0.3)
+ .withMaxTokens(400)
.build();
case RECOMMENDATION -> OpenAiChatOptions.builder()
- .withTemperature(0.9) // ๋ค์์ฑ ์ค์
+ .withTemperature(0.9)
.withMaxTokens(250)
.build();
case QUESTION -> OpenAiChatOptions.builder()
- .withTemperature(0.7) // ๊ท ํ
+ .withTemperature(0.7)
.withMaxTokens(200)
.build();
default -> OpenAiChatOptions.builder()
@@ -314,12 +622,10 @@ private OpenAiChatOptions getOptionsForMessageType(InternalMessageType type) {
}
private String postProcessResponse(String response, InternalMessageType type) {
- // ์๋ต ๊ธธ์ด ์ ํ ํ์ธ
if (response.length() > 500) {
response = response.substring(0, 497) + "...";
}
- // ์ด๋ชจ์ง ์ถ๊ฐ (ํ์
๋ณ)
if (type == InternalMessageType.RECIPE && !response.contains("๐น")) {
response = "๐น " + response;
}
@@ -327,50 +633,36 @@ private String postProcessResponse(String response, InternalMessageType type) {
return response;
}
- /**
- * AI ์๋ต ์์ฑ
- */
- private String generateAIResponse(ChatRequestDto requestDto) {
+ private ChatConversation generateAIResponse(ChatRequestDto requestDto) {
log.info("Normal chat mode for userId: {}", requestDto.getUserId());
- // ๋ฉ์์ง ํ์
๊ฐ์ง (๋ด๋ถ enum ์ฌ์ฉ)
InternalMessageType messageType = detectMessageType(requestDto.getMessage());
- // ์ต๊ทผ ๋ํ ๊ธฐ๋ก ์กฐํ (์ต์ 20๊ฐ ๋ฉ์์ง - USER์ CHATBOT ๋ฉ์์ง ๋ชจ๋ ํฌํจ)
List recentChats =
chatConversationRepository.findTop20ByUserIdOrderByCreatedAtDesc(requestDto.getUserId());
- // ๋ํ ์ปจํ
์คํธ ์์ฑ
String conversationContext = buildConversationContext(recentChats);
- // ChatClient ๋น๋ ์์ฑ
var promptBuilder = chatClient.prompt()
.system(buildSystemMessage(messageType) + conversationContext)
.user(buildUserMessage(requestDto.getMessage(), messageType));
- // ์๋ต ์์ฑ
String response = promptBuilder
.options(getOptionsForMessageType(messageType))
.call()
.content();
- // ์๋ต ํ์ฒ๋ฆฌ
response = postProcessResponse(response, messageType);
- // ๋ํ ์ ์ฅ - ์ฌ์ฉ์ ๋ฉ์์ง์ ๋ด ์๋ต์ ๊ฐ๊ฐ ์ ์ฅ
- saveConversation(requestDto, response);
-
- return response;
+ return saveBotResponse(requestDto.getUserId(), response, null);
}
- /**
- * ๋ก๋ฉ ๋ฉ์์ง ์์ฑ
- */
public ChatResponseDto createLoadingMessage() {
return ChatResponseDto.builder()
.message("์๋ต์ ์์ฑํ๋ ์ค...")
+ .sender(MessageSender.CHATBOT)
.type(MessageType.LOADING)
- .timestamp(LocalDateTime.now())
+ .createdAt(LocalDateTime.now())
.metaData(ChatResponseDto.MetaData.builder()
.isTyping(true)
.build())
@@ -398,14 +690,35 @@ private InternalMessageType detectMessageType(String message) {
return InternalMessageType.CASUAL_CHAT;
}
- // ๋จ๊ณ๋ณ ์ถ์ฒ ์์ ํค์๋ ๊ฐ์ง
+ @Deprecated
private boolean isStepRecommendationTrigger(String message) {
+ log.warn("๋ ๊ฑฐ์ ํค์๋ ๊ฐ์ง ์ฌ์ฉ๋จ. currentStep ์ฌ์ฉ ๊ถ์ฅ. message: {}", message);
String lower = message.toLowerCase().trim();
- return lower.contains("๋จ๊ณ๋ณ ์ถ์ฒ");
+ return lower.contains("๋จ๊ณ๋ณ ์ทจํฅ ์ฐพ๊ธฐ");
+ }
+
+ private ChatResponseDto createErrorResponse(String errorMessage) {
+ return ChatResponseDto.builder()
+ .message(errorMessage)
+ .sender(MessageSender.CHATBOT)
+ .type(MessageType.ERROR)
+ .createdAt(LocalDateTime.now())
+ .build();
}
private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
Integer currentStep = requestDto.getCurrentStep();
+
+ if (currentStep == 1 && "STEP".equalsIgnoreCase(requestDto.getMessage())) {
+ ChatConversation userChoice = ChatConversation.builder()
+ .userId(requestDto.getUserId())
+ .message("๋จ๊ณ๋ณ ์ทจํฅ ์ฐพ๊ธฐ")
+ .sender(MessageSender.USER)
+ .createdAt(LocalDateTime.now())
+ .build();
+ chatConversationRepository.save(userChoice);
+ }
+
if (currentStep == null || currentStep <= 0) {
currentStep = 1;
}
@@ -417,93 +730,145 @@ private ChatResponseDto handleStepRecommendation(ChatRequestDto requestDto) {
switch (currentStep) {
case 1:
stepData = getAlcoholStrengthOptions();
- message = "๋จ๊ณ๋ณ ๋ง์ถค ์ถ์ฒ์ ์์ํฉ๋๋ค! ๐ฏ\n์ํ์๋ ๋์๋ฅผ ์ ํํด์ฃผ์ธ์!";
+ message = "๋จ๊ณ๋ณ ๋ง์ถค ์ทจํฅ ์ถ์ฒ์ ์์ํฉ๋๋ค! ๐ฏ\n์ํ์๋ ๋์๋ฅผ ์ ํํด์ฃผ์ธ์!";
type = MessageType.RADIO_OPTIONS;
break;
case 2:
- stepData = getAlcoholBaseTypeOptions(parseAlcoholStrength(requestDto.getSelectedAlcoholStrength()));
- message = "์ข์ ์ ํ์ด๋ค์! ์ด์ ๋ฒ ์ด์ค๊ฐ ๋ ์ ์ ์ ํํด์ฃผ์ธ์ ๐ธ";
+ // ๋
ผ์์ฝ ์ ํ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ค๋ฅธ ์ต์
์ ๊ณต
+ boolean isNonAlcoholic = "NON_ALCOHOLIC".equals(requestDto.getSelectedAlcoholStrength());
+
+ if (isNonAlcoholic) {
+ // ๋
ผ์์ฝ์ธ ๊ฒฝ์ฐ: ๊ธ๋ผ์ค ํ์
์ ํ
+ stepData = getCocktailTypeOptions();
+ message = "๋
ผ์์ฝ ์นตํ
์ผ์ด๋ค์! ๐ฅค\n์ด๋ค ์คํ์ผ์ ์นตํ
์ผ์ ์ํ์๋์?";
+ } else {
+ // ์์ฝ์ธ ๊ฒฝ์ฐ: ๋ฒ ์ด์ค ํ์
์ ํ
+ stepData = getAlcoholBaseTypeOptions(parseAlcoholStrength(requestDto.getSelectedAlcoholStrength()));
+ message = "์ข์ ์ ํ์ด๋ค์! \n์ด์ ๋ฒ ์ด์ค๊ฐ ๋ ์ ์ ์ ํํด์ฃผ์ธ์ ๐ธ";
+ }
type = MessageType.RADIO_OPTIONS;
break;
case 3:
- stepData = getCocktailTypeOptions(
- parseAlcoholStrength(requestDto.getSelectedAlcoholStrength()),
- parseAlcoholBaseType(requestDto.getSelectedAlcoholBaseType())
+ stepData = new StepRecommendationResponseDto(
+ 3,
+ null,
+ null,
+ null,
+ false
);
- message = "์๋ฒฝํด์! ๋ง์ง๋ง์ผ๋ก ์ด๋ค ์คํ์ผ๋ก ์ฆ๊ธฐ์ค ๊ฑด๊ฐ์? ๐ฅ";
- type = MessageType.RADIO_OPTIONS;
+ message = "์ข์์! ์ด์ ์ํ๋ ์นตํ
์ผ ์คํ์ผ์ ์์ ๋กญ๊ฒ ๋ง์ํด์ฃผ์ธ์ ๐ฌ\n์์ผ๋ฉด 'x', ๋๋ '์์'์ ์
๋ ฅํด์ฃผ์ธ์!";
+ type = MessageType.INPUT;
break;
case 4:
- stepData = getFinalRecommendations(
- parseAlcoholStrength(requestDto.getSelectedAlcoholStrength()),
- parseAlcoholBaseType(requestDto.getSelectedAlcoholBaseType()),
- parseCocktailType(requestDto.getSelectedCocktailType())
- );
+ // ๋
ผ์์ฝ ์ฌ๋ถ ๋ค์ ํ์ธ
+ boolean isNonAlcoholicFinal = "NON_ALCOHOLIC".equals(requestDto.getSelectedAlcoholStrength());
+
+ if (isNonAlcoholicFinal) {
+ // ๋
ผ์์ฝ: ๋์์ ์นตํ
์ผ ํ์
์ผ๋ก ๊ฒ์
+ stepData = getFinalRecommendationsForNonAlcoholic(
+ parseCocktailType(requestDto.getSelectedCocktailType()),
+ requestDto.getMessage()
+ );
+ } else {
+ // ์์ฝ: ๋์์ ๋ฒ ์ด์ค ํ์
์ผ๋ก ๊ฒ์
+ stepData = getFinalRecommendationsWithMessage(
+ parseAlcoholStrength(requestDto.getSelectedAlcoholStrength()),
+ parseAlcoholBaseType(requestDto.getSelectedAlcoholBaseType()),
+ requestDto.getMessage()
+ );
+ }
message = stepData.getStepTitle();
- type = MessageType.CARD_LIST; // ์ต์ข
์ถ์ฒ์ ์นด๋ ๋ฆฌ์คํธ
+ type = MessageType.CARD_LIST;
break;
default:
stepData = getAlcoholStrengthOptions();
- message = "๋จ๊ณ๋ณ ๋ง์ถค ์ถ์ฒ์ ์์ํฉ๋๋ค! ๐ฏ";
+ message = "๋จ๊ณ๋ณ ๋ง์ถค ์ทจํฅ ์ถ์ฒ์ ์์ํฉ๋๋ค! ๐ฏ";
type = MessageType.RADIO_OPTIONS;
}
- // ๋ฉํ๋ฐ์ดํฐ ํฌํจ
+ ChatConversation savedResponse = saveBotResponse(requestDto.getUserId(), message, stepData);
+
ChatResponseDto.MetaData metaData = ChatResponseDto.MetaData.builder()
.currentStep(currentStep)
.totalSteps(4)
- .isTyping(true)
+ .isTyping(type != MessageType.CARD_LIST)
.delay(300)
.build();
return ChatResponseDto.builder()
+ .id(savedResponse.getId())
+ .userId(requestDto.getUserId())
.message(message)
+ .sender(MessageSender.CHATBOT)
.type(type)
.stepData(stepData)
.metaData(metaData)
- .timestamp(LocalDateTime.now())
+ .createdAt(savedResponse.getCreatedAt())
.build();
}
- // ============ ๋จ๊ณ๋ณ ์ถ์ฒ ๊ด๋ จ ๋ฉ์๋๋ค ============
- // "ALL" ๋๋ null/๋น๊ฐ์ null๋ก ์ฒ๋ฆฌํ์ฌ ์ ์ฒด ์ ํ ์๋ฏธ
+ private StepRecommendationResponseDto getCocktailTypeOptions() {
+ List options = new ArrayList<>();
- private AlcoholStrength parseAlcoholStrength(String value) {
+ options.add(new StepRecommendationResponseDto.StepOption(
+ "ALL",
+ "์ ์ฒด",
+ null
+ ));
+
+ for (CocktailType type : CocktailType.values()) {
+ options.add(new StepRecommendationResponseDto.StepOption(
+ type.name(),
+ type.getDescription(),
+ null
+ ));
+ }
+
+ return new StepRecommendationResponseDto(
+ 2,
+ "์ด๋ค ์คํ์ผ์ ์นตํ
์ผ์ ์ํ์๋์?",
+ options,
+ null,
+ false
+ );
+ }
+
+ private CocktailType parseCocktailType(String value) {
if (value == null || value.trim().isEmpty() || "ALL".equalsIgnoreCase(value)) {
return null;
}
try {
- return AlcoholStrength.valueOf(value);
+ return CocktailType.valueOf(value);
} catch (IllegalArgumentException e) {
- log.warn("Invalid AlcoholStrength value: {}", value);
+ log.warn("Invalid CocktailType value: {}", value);
return null;
}
}
- private AlcoholBaseType parseAlcoholBaseType(String value) {
+ private AlcoholStrength parseAlcoholStrength(String value) {
if (value == null || value.trim().isEmpty() || "ALL".equalsIgnoreCase(value)) {
return null;
}
try {
- return AlcoholBaseType.valueOf(value);
+ return AlcoholStrength.valueOf(value);
} catch (IllegalArgumentException e) {
- log.warn("Invalid AlcoholBaseType value: {}", value);
+ log.warn("Invalid AlcoholStrength value: {}", value);
return null;
}
}
- private CocktailType parseCocktailType(String value) {
+ private AlcoholBaseType parseAlcoholBaseType(String value) {
if (value == null || value.trim().isEmpty() || "ALL".equalsIgnoreCase(value)) {
return null;
}
try {
- return CocktailType.valueOf(value);
+ return AlcoholBaseType.valueOf(value);
} catch (IllegalArgumentException e) {
- log.warn("Invalid CocktailType value: {}", value);
+ log.warn("Invalid AlcoholBaseType value: {}", value);
return null;
}
}
@@ -511,7 +876,6 @@ private CocktailType parseCocktailType(String value) {
private StepRecommendationResponseDto getAlcoholStrengthOptions() {
List options = new ArrayList<>();
- // "์ ์ฒด" ์ต์
์ถ๊ฐ
options.add(new StepRecommendationResponseDto.StepOption(
"ALL",
"์ ์ฒด",
@@ -538,7 +902,6 @@ private StepRecommendationResponseDto getAlcoholStrengthOptions() {
private StepRecommendationResponseDto getAlcoholBaseTypeOptions(AlcoholStrength alcoholStrength) {
List options = new ArrayList<>();
- // "์ ์ฒด" ์ต์
์ถ๊ฐ
options.add(new StepRecommendationResponseDto.StepOption(
"ALL",
"์ ์ฒด",
@@ -562,74 +925,118 @@ private StepRecommendationResponseDto getAlcoholBaseTypeOptions(AlcoholStrength
);
}
- private StepRecommendationResponseDto getCocktailTypeOptions(AlcoholStrength alcoholStrength, AlcoholBaseType alcoholBaseType) {
- List options = new ArrayList<>();
-
- // "์ ์ฒด" ์ต์
์ถ๊ฐ
- options.add(new StepRecommendationResponseDto.StepOption(
- "ALL",
- "์ ์ฒด",
- null
- ));
-
- for (CocktailType cocktailType : CocktailType.values()) {
- options.add(new StepRecommendationResponseDto.StepOption(
- cocktailType.name(),
- cocktailType.getDescription(),
- null
- ));
- }
-
- return new StepRecommendationResponseDto(
- 3,
- "์ด๋ค ์ข
๋ฅ์ ์์ผ๋ก ๋์๊ฒ ์ด์?",
- options,
- null,
- false
- );
- }
-
- private StepRecommendationResponseDto getFinalRecommendations(
+ private StepRecommendationResponseDto getFinalRecommendationsWithMessage(
AlcoholStrength alcoholStrength,
AlcoholBaseType alcoholBaseType,
- CocktailType cocktailType) {
- // ํํฐ๋ง ์กฐ๊ฑด์ ๋ง๋ ์นตํ
์ผ ๊ฒ์
- // "ALL" ์ ํ ์ ํด๋น ํํฐ๋ฅผ null๋ก ์ฒ๋ฆฌํ์ฌ ์ ์ฒด ๊ฒ์
+ String userMessage) {
+
List strengths = (alcoholStrength == null) ? null : List.of(alcoholStrength);
List baseTypes = (alcoholBaseType == null) ? null : List.of(alcoholBaseType);
- List cocktailTypes = (cocktailType == null) ? null : List.of(cocktailType);
+
+ String keyword = null;
+ if (userMessage != null && !userMessage.trim().isEmpty()) {
+ String trimmed = userMessage.trim().toLowerCase();
+ if (!trimmed.equals("x") && !trimmed.equals("์์")) {
+ keyword = userMessage;
+ }
+ }
Page cocktailPage = cocktailRepository.searchWithFilters(
- null, // ํค์๋ ์์
+ keyword,
strengths,
- cocktailTypes,
+ null,
baseTypes,
- PageRequest.of(0, 3) // ์ต๋ 3๊ฐ ์ถ์ฒ
+ PageRequest.of(0, 3)
);
List recommendations = cocktailPage.getContent().stream()
- .map(cocktail -> new CocktailSummaryResponseDto(
- cocktail.getId(),
- cocktail.getCocktailName(),
- cocktail.getCocktailNameKo(),
- cocktail.getCocktailImgUrl(),
- cocktail.getAlcoholStrength().getDescription()
- ))
- .collect(Collectors.toList());
-
- // ์ถ์ฒ ์ด์ ๋ ๊ฐ ์นตํ
์ผ๋ณ ์ค๋ช
์ผ๋ก ๋ค์ด๊ฐ๋๋ก ์ ๋
+ .map(cocktail -> new CocktailSummaryResponseDto(
+ cocktail.getId(),
+ cocktail.getCocktailName(),
+ cocktail.getCocktailNameKo(),
+ cocktail.getCocktailImgUrl(),
+ cocktail.getAlcoholStrength().getDescription()
+ ))
+ .collect(Collectors.toList());
+
String stepTitle = recommendations.isEmpty()
? "์กฐ๊ฑด์ ๋ง๋ ์นตํ
์ผ์ ์ฐพ์ ์ ์์ต๋๋ค ๐ข"
: "์ง ๐๐\n" +
"์นตํ
์ผ์ ์์ธํ ์ ๋ณด๋ '์์ธ๋ณด๊ธฐ'๋ฅผ ํด๋ฆญํด์ ํ์ธํ ์ ์์ด์.\n" +
"๋ง์์ ๋๋ ์นตํ
์ผ์ 'ํต' ๋ฒํผ์ ๋๋ฌ ๋๋ง์ Bar์ ์ ์ฅํด๋ณด์ธ์!";
+ // RESTART ์ต์
์ถ๊ฐ
+ List restartOption = List.of(
+ new StepRecommendationResponseDto.StepOption(
+ "RESTART",
+ "๋ค์ ์์ํ๊ธฐ",
+ null
+ )
+ );
+
return new StepRecommendationResponseDto(
4,
stepTitle,
- null,
+ restartOption, // RESTART ์ต์
์ถ๊ฐ
+ recommendations,
+ true
+ );
+ }
+ private StepRecommendationResponseDto getFinalRecommendationsForNonAlcoholic(
+ CocktailType cocktailType,
+ String userMessage) {
+
+ // ๋
ผ์์ฝ ๋์๋ง ํํฐ๋ง
+ List strengths = List.of(AlcoholStrength.NON_ALCOHOLIC);
+ List types = (cocktailType == null) ? null : List.of(cocktailType);
+
+ String keyword = null;
+ if (userMessage != null && !userMessage.trim().isEmpty()) {
+ String trimmed = userMessage.trim().toLowerCase();
+ if (!trimmed.equals("x") && !trimmed.equals("์์")) {
+ keyword = userMessage;
+ }
+ }
+
+ Page cocktailPage = cocktailRepository.searchWithFilters(
+ keyword,
+ strengths,
+ types, // ์นตํ
์ผ ํ์
ํํฐ ์ ์ฉ
+ null, // ๋ฒ ์ด์ค ํ์
์ null
+ PageRequest.of(0, 3)
+ );
+
+ List recommendations = cocktailPage.getContent().stream()
+ .map(cocktail -> new CocktailSummaryResponseDto(
+ cocktail.getId(),
+ cocktail.getCocktailName(),
+ cocktail.getCocktailNameKo(),
+ cocktail.getCocktailImgUrl(),
+ cocktail.getAlcoholStrength().getDescription()
+ ))
+ .collect(Collectors.toList());
+
+ String stepTitle = recommendations.isEmpty()
+ ? "์กฐ๊ฑด์ ๋ง๋ ๋
ผ์์ฝ ์นตํ
์ผ์ ์ฐพ์ ์ ์์ต๋๋ค ๐ข"
+ : "์ง ๐๐ ๋
ผ์์ฝ ์นตํ
์ผ ์ถ์ฒ!\n" +
+ "์นตํ
์ผ์ ์์ธํ ์ ๋ณด๋ '์์ธ๋ณด๊ธฐ'๋ฅผ ํด๋ฆญํด์ ํ์ธํ ์ ์์ด์.\n" +
+ "๋ง์์ ๋๋ ์นตํ
์ผ์ 'ํต' ๋ฒํผ์ ๋๋ฌ ๋๋ง์ Bar์ ์ ์ฅํด๋ณด์ธ์!";
+
+ // RESTART ์ต์
์ถ๊ฐ
+ List restartOption = List.of(
+ new StepRecommendationResponseDto.StepOption(
+ "RESTART",
+ "๋ค์ ์์ํ๊ธฐ",
+ null
+ )
+ );
+
+ return new StepRecommendationResponseDto(
+ 4,
+ stepTitle,
+ restartOption, // RESTART ์ต์
์ถ๊ฐ
recommendations,
true
);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/back/domain/cocktail/comment/entity/CocktailComment.java b/src/main/java/com/back/domain/cocktail/comment/entity/CocktailComment.java
index 5f0a5e2a..967b8b0e 100644
--- a/src/main/java/com/back/domain/cocktail/comment/entity/CocktailComment.java
+++ b/src/main/java/com/back/domain/cocktail/comment/entity/CocktailComment.java
@@ -15,10 +15,7 @@
import java.time.LocalDateTime;
@Entity
-@Table(
- name = "cocktail_comment",
- uniqueConstraints = @UniqueConstraint(columnNames = {"cocktail_id", "user_id"}) // ์ฌ์ฉ์ 1๊ฐ ๋๊ธ ์ ํ
-)
+@Table(name = "cocktail_comment")
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
diff --git a/src/main/java/com/back/domain/cocktail/comment/repository/CocktailCommentRepository.java b/src/main/java/com/back/domain/cocktail/comment/repository/CocktailCommentRepository.java
index 758012ee..ad747111 100644
--- a/src/main/java/com/back/domain/cocktail/comment/repository/CocktailCommentRepository.java
+++ b/src/main/java/com/back/domain/cocktail/comment/repository/CocktailCommentRepository.java
@@ -1,6 +1,7 @@
package com.back.domain.cocktail.comment.repository;
import com.back.domain.cocktail.comment.entity.CocktailComment;
+import com.back.domain.post.comment.enums.CommentStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -13,5 +14,5 @@ public interface CocktailCommentRepository extends JpaRepository findTop10ByCocktailIdAndIdLessThanOrderByIdDesc(Long cocktailId, Long lastId);
- boolean existsByCocktailIdAndUserId(Long cocktailId, Long id);
+ boolean existsByCocktailIdAndUserIdAndStatusNot(Long cocktailId, Long id, CommentStatus status);
}
diff --git a/src/main/java/com/back/domain/cocktail/comment/service/CocktailCommentService.java b/src/main/java/com/back/domain/cocktail/comment/service/CocktailCommentService.java
index 8cf0d20c..fbb2d9af 100644
--- a/src/main/java/com/back/domain/cocktail/comment/service/CocktailCommentService.java
+++ b/src/main/java/com/back/domain/cocktail/comment/service/CocktailCommentService.java
@@ -32,7 +32,12 @@ public CocktailCommentResponseDto createCocktailComment(Long cocktailId, Cocktai
.orElseThrow(() -> new IllegalArgumentException("์นตํ
์ผ์ด ์กด์ฌํ์ง ์์ต๋๋ค. id=" + cocktailId));
// ์ฌ์ฉ์๋น ๋๊ธ 1๊ฐ ์ ํ ์ฒดํฌ
- boolean exists = cocktailCommentRepository.existsByCocktailIdAndUserId(cocktailId, user.getId());
+ boolean exists = cocktailCommentRepository.existsByCocktailIdAndUserIdAndStatusNot(
+ cocktailId,
+ user.getId(),
+ CommentStatus.DELETED // DELETED ์ํ๋ ์ ์ธํ๊ณ ๊ฒ์ฌ
+ );
+
if (exists) {
throw new IllegalArgumentException("์ด๋ฏธ ๋๊ธ์ ์์ฑํ์
จ์ต๋๋ค.");
}
diff --git a/src/main/java/com/back/domain/cocktail/controller/CocktailController.java b/src/main/java/com/back/domain/cocktail/controller/CocktailController.java
index a389f05a..6c2268af 100644
--- a/src/main/java/com/back/domain/cocktail/controller/CocktailController.java
+++ b/src/main/java/com/back/domain/cocktail/controller/CocktailController.java
@@ -31,6 +31,7 @@ public RsData getCocktailDetailById(@PathVariable lon
return RsData.successOf(cocktailDetailResponseDto);
}
+ // @param lastValue ๋ค์ ํ์ด์ง์์ ์ด ๊ฐ๋ณด๋ค ์์ ํญ๋ชฉ๋ง ๊ฐ์ ธ์ค๊ธฐ ์ํด ์ฌ์ฉ
// @param lastId ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์นตํ
์ผ ID (์ฒซ ์์ฒญ null ๊ฐ๋ฅ)
// @param size ๊ฐ์ ธ์ฌ ๋ฐ์ดํฐ ๊ฐ์ (๊ธฐ๋ณธ๊ฐ DEFAULT_SIZE)
// @return RsData ํํ์ ์นตํ
์ผ ์์ฝ ์ ๋ณด ๋ฆฌ์คํธ
@@ -38,10 +39,12 @@ public RsData getCocktailDetailById(@PathVariable lon
@Transactional
@Operation(summary = "์นตํ
์ผ ๋ค๊ฑด ์กฐํ")
public RsData> getCocktails(
+ @RequestParam(value = "lastValue", required = false) Long lastValue,
@RequestParam(value = "lastId", required = false) Long lastId,
- @RequestParam(value = "size", required = false) Integer size
+ @RequestParam(value = "size", required = false) Integer size,
+ @RequestParam(value = "sortBy", required = false, defaultValue = "recent") String sortBy
) {
- List cocktails = cocktailService.getCocktails(lastId, size);
+ List cocktails = cocktailService.getCocktails(lastValue, lastId, size, sortBy);
return RsData.successOf(cocktails);
}
diff --git a/src/main/java/com/back/domain/cocktail/dto/CocktailDetailResponseDto.java b/src/main/java/com/back/domain/cocktail/dto/CocktailDetailResponseDto.java
index f6fb8591..ddcac3f5 100644
--- a/src/main/java/com/back/domain/cocktail/dto/CocktailDetailResponseDto.java
+++ b/src/main/java/com/back/domain/cocktail/dto/CocktailDetailResponseDto.java
@@ -1,5 +1,6 @@
package com.back.domain.cocktail.dto;
+import com.back.domain.cocktail.entity.Cocktail;
import com.back.domain.cocktail.service.CocktailService;
import java.util.List;
@@ -14,6 +15,26 @@ public record CocktailDetailResponseDto(
String cocktailImgUrl,
String cocktailStory,
List ingredient,
- String recipe
+ String recipe,
+ String cocktailPreview
) {
+ public static CocktailDetailResponseDto from(Cocktail cocktail, List ingredients){
+ String preview =cocktail.getCocktailStory().length() >80 ?
+ cocktail.getCocktailStory().substring(0,80)+"..."
+ : cocktail.getCocktailStory();
+
+ return new CocktailDetailResponseDto(
+ cocktail.getId(),
+ cocktail.getCocktailName(),
+ cocktail.getCocktailNameKo(),
+ cocktail.getAlcoholStrength().getDescription(),
+ cocktail.getCocktailType().getDescription(),
+ cocktail.getAlcoholBaseType().getDescription(),
+ cocktail.getCocktailImgUrl(),
+ cocktail.getCocktailStory(),
+ ingredients,
+ cocktail.getRecipe(),
+ preview
+ );
+ }
}
diff --git a/src/main/java/com/back/domain/cocktail/dto/CocktailSearchResponseDto.java b/src/main/java/com/back/domain/cocktail/dto/CocktailSearchResponseDto.java
index 06078a19..63e10c87 100644
--- a/src/main/java/com/back/domain/cocktail/dto/CocktailSearchResponseDto.java
+++ b/src/main/java/com/back/domain/cocktail/dto/CocktailSearchResponseDto.java
@@ -1,34 +1,34 @@
package com.back.domain.cocktail.dto;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import com.back.domain.cocktail.entity.Cocktail;
-@Getter
-@Setter
-@NoArgsConstructor
-public class CocktailSearchResponseDto {
+public record CocktailSearchResponseDto (
- private long cocktailId;
- private String cocktailName;
- private String cocktailNameKo;
- private String alcoholStrength;
- private String cocktailType;
- private String alcoholBaseType;
- private String cocktailImgUrl;
- private String cocktailStory;
+ Long cocktailId,
+ String cocktailName,
+ String cocktailNameKo,
+ String alcoholStrength,
+ String cocktailType,
+ String alcoholBaseType,
+ String cocktailImgUrl,
+ String cocktailStory,
+ String cocktailPreview
+){
+ public static CocktailSearchResponseDto from(Cocktail cocktail){
+ String preview =cocktail.getCocktailStory().length() >80 ?
+ cocktail.getCocktailStory().substring(0,80)+"..."
+ : cocktail.getCocktailStory();
- public CocktailSearchResponseDto(long cocktailId, String cocktailName, String cocktailNameKo,
- String alcoholStrength, String cocktailType,
- String alcoholBaseType, String cocktailImgUrl,
- String cocktailStory) {
- this.cocktailId = cocktailId;
- this.cocktailName = cocktailName;
- this.cocktailNameKo = cocktailNameKo;
- this.alcoholStrength = alcoholStrength;
- this.cocktailType = cocktailType;
- this.alcoholBaseType = alcoholBaseType;
- this.cocktailImgUrl = cocktailImgUrl;
- this.cocktailStory = cocktailStory;
+ return new CocktailSearchResponseDto(
+ cocktail.getId(),
+ cocktail.getCocktailName(),
+ cocktail.getCocktailNameKo(),
+ cocktail.getAlcoholStrength().getDescription(),
+ cocktail.getCocktailType().getDescription(),
+ cocktail.getAlcoholBaseType().getDescription(),
+ cocktail.getCocktailImgUrl(),
+ cocktail.getCocktailStory(),
+ preview
+ );
}
}
\ No newline at end of file
diff --git a/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java b/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java
index 0695c469..8187f9c3 100644
--- a/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java
+++ b/src/main/java/com/back/domain/cocktail/repository/CocktailRepository.java
@@ -16,17 +16,58 @@
@Repository
public interface CocktailRepository extends JpaRepository {
- // ์ฒซ ์์ฒญ โ ์ต์ ์(๋ด๋ฆผ์ฐจ์)์ผ๋ก ์ ๋ ฌํด์ ๊ฐ์ ธ์ค๊ธฐ
+ // ์ ์ฒด์กฐํ : ์ต์ ์
List findAllByOrderByIdDesc(Pageable pageable);
-
- // ๋ฌดํ์คํฌ๋กค โ lastId๋ณด๋ค ์์ ID๋ค ๊ฐ์ ธ์ค๊ธฐ
List findByIdLessThanOrderByIdDesc(Long lastId, Pageable pageable);
- List findByCocktailNameContainingIgnoreCaseOrIngredientContainingIgnoreCase(String cocktailName, String ingredient);
+ // ์ ์ฒด ์กฐํ: keepsCount ๊ธฐ์ค ๋ด๋ฆผ์ฐจ์
+ @Query("""
+ SELECT c FROM Cocktail c
+ LEFT JOIN MyBar m ON m.cocktail = c AND m.status = 'ACTIVE'
+ GROUP BY c.id
+ ORDER BY COUNT(m) DESC, c.id DESC
+ """)
+ List findAllOrderByKeepCountDesc(Pageable pageable);
+
+ // ๋ฌดํ์คํฌ๋กค ์กฐํ: lastKeepCount ์ดํ
+ @Query("""
+ SELECT c FROM Cocktail c
+ LEFT JOIN MyBar m ON m.cocktail = c AND m.status = 'ACTIVE'
+ GROUP BY c.id
+ HAVING COUNT(m) < :lastKeepCount OR (COUNT(m) = :lastKeepCount AND c.id < :lastId)
+ ORDER BY COUNT(m) DESC, c.id DESC
+""")
+ List findByKeepCountLessThanOrderByKeepCountDesc(
+ @Param("lastKeepCount") Long lastKeepCount,
+ @Param("lastId") Long lastId,
+ Pageable pageable
+ );
+
+ // ๋๊ธ์
+ @Query("SELECT c FROM Cocktail c " +
+ "LEFT JOIN CocktailComment cm ON cm.cocktail = c " +
+ "GROUP BY c.id " +
+ "ORDER BY COUNT(cm) DESC, c.id DESC")
+ List findAllOrderByCommentsCountDesc(Pageable pageable);
+
+ @Query("""
+ SELECT c FROM Cocktail c
+ LEFT JOIN CocktailComment cm ON cm.cocktail = c
+ GROUP BY c.id
+ HAVING COUNT(cm) < :lastCommentsCount OR (COUNT(cm) = :lastCommentsCount AND c.id < :lastId)
+ ORDER BY COUNT(cm) DESC, c.id DESC
+ """)
+ List findByCommentsCountLessThanOrderByCommentsCountDesc(
+ @Param("lastCommentsCount") Long lastCommentsCount,
+ @Param("lastId") Long lastId,
+ Pageable pageable
+ );
+ //๊ฒ์, ํํฐ
@Query("SELECT c FROM Cocktail c " +
"WHERE (:keyword IS NULL OR :keyword = '' OR " +
" LOWER(c.cocktailName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
+ " LOWER(c.cocktailNameKo) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
" LOWER(c.ingredient) LIKE LOWER(CONCAT('%', :keyword, '%')))" +
" AND (:strengths IS NULL OR c.alcoholStrength IN :strengths) " + // ์์ฝ์ฌ ๋์ ํํฐ๋ฅผ ๋ด๋น
" AND (:types IS NULL OR c.cocktailType IN :types) " + // ์นตํ
์ผ ํ์
ํํฐ๋ฅผ ๋ด๋น
diff --git a/src/main/java/com/back/domain/cocktail/service/CocktailService.java b/src/main/java/com/back/domain/cocktail/service/CocktailService.java
index 4865fcec..82b8039d 100644
--- a/src/main/java/com/back/domain/cocktail/service/CocktailService.java
+++ b/src/main/java/com/back/domain/cocktail/service/CocktailService.java
@@ -37,37 +37,43 @@ public Cocktail getCocktailById(Long id) {
.orElseThrow(() -> new IllegalArgumentException("Cocktail not found. id=" + id));
}
- // ์นตํ
์ผ ๋ฌดํ์คํฌ๋กค ์กฐํ
@Transactional(readOnly = true)
- public List getCocktails(Long lastId, Integer size) { // ๋ฌดํ์คํฌ๋กค ์กฐํ, ํด๋ผ์ด์ธํธ ์ชฝ์์ lastId์ size ์ ๋ณด๋ฅผ ๋ฐ์.(์คํฌ๋กค ์ด๋ฒคํธ)
+ public List getCocktails(Long lastValue, Long lastId, Integer size, String sortBy) {
int fetchSize = (size != null) ? size : DEFAULT_SIZE;
-
+ Pageable pageable = PageRequest.of(0, fetchSize);
List cocktails;
- if (lastId == null) {
- // ์ฒซ ์์ฒญ โ ์ต์ ๋ฐ์ดํฐ๋ถํฐ
- cocktails = cocktailRepository.findAllByOrderByIdDesc(PageRequest.of(0, fetchSize));
- } else {
- // ๋ฌดํ์คํฌ๋กค โ ๋ง์ง๋ง ID๋ณด๋ค ์์ ๋ฐ์ดํฐ ์กฐํ
- cocktails = cocktailRepository.findByIdLessThanOrderByIdDesc(lastId, PageRequest.of(0, fetchSize));
+
+ Long cursor = (lastValue != null) ? lastValue : lastId;
+
+ switch (sortBy != null ? sortBy.toLowerCase() : "") {
+ case "keeps":
+ cocktails = (cursor == null)
+ ? cocktailRepository.findAllOrderByKeepCountDesc(pageable)
+ : cocktailRepository.findByKeepCountLessThanOrderByKeepCountDesc(cursor, lastId, pageable);
+ break;
+ case "comments":
+ cocktails = (cursor == null)
+ ? cocktailRepository.findAllOrderByCommentsCountDesc(pageable)
+ : cocktailRepository.findByCommentsCountLessThanOrderByCommentsCountDesc(cursor, lastId, pageable);
+ break;
+ default:
+ cocktails = (cursor == null)
+ ? cocktailRepository.findAllByOrderByIdDesc(pageable)
+ : cocktailRepository.findByIdLessThanOrderByIdDesc(cursor, pageable);
+ break;
}
+
return cocktails.stream()
- .map(c -> new CocktailSummaryResponseDto(c.getId(), c.getCocktailName(), c.getCocktailNameKo(), c.getCocktailImgUrl(), c.getAlcoholStrength().getDescription()))
+ .map(c -> new CocktailSummaryResponseDto(
+ c.getId(),
+ c.getCocktailName(),
+ c.getCocktailNameKo(),
+ c.getCocktailImgUrl(),
+ c.getAlcoholStrength().getDescription()
+ ))
.collect(Collectors.toList());
}
- // ์นตํ
์ผ ๊ฒ์๊ธฐ๋ฅ
- @Transactional(readOnly = true)
- public List cocktailSearch(String keyword) {
- // cockTailName, ingredient์ด ํ๋๋ง ์์ ์๋ ์๊ณ ๋ ๋ค ์์ ์๋ ์์
- if (keyword == null || keyword.trim().isEmpty()) {
- // ์๋ฌด ๊ฒ์์ด ์์ผ๋ฉด ์ ์ฒด ๋ฐํ ์ฒ๋ฆฌ
- return cocktailRepository.findAll();
- } else {
- // ์ด๋ฆ ๋๋ ์ฌ๋ฃ ๋ ์ค ํ๋๋ผ๋ ๋งค์นญ๋๋ฉด ๊ฒฐ๊ณผ ๋ฐํ
- return cocktailRepository.findByCocktailNameContainingIgnoreCaseOrIngredientContainingIgnoreCase(keyword, keyword);
- }
- }
-
// ์นตํ
์ผ ๊ฒ์,ํํฐ๊ธฐ๋ฅ
@Transactional(readOnly = true)
public List searchAndFilter(CocktailSearchRequestDto cocktailSearchRequestDto) {
@@ -105,25 +111,12 @@ public List searchAndFilter(CocktailSearchRequestDto
//Cocktail ์ํฐํฐ โ CocktailResponseDto ์๋ต DTO๋ก ๋ฐ๊ฟ์ฃผ๋ ๊ณผ์
List resultDtos = pageResult.stream()
- .map(c -> new CocktailSearchResponseDto(
- c.getId(),
- c.getCocktailName(),
- c.getCocktailNameKo(),
- c.getAlcoholStrength().getDescription(),
- c.getCocktailType().getDescription(),
- c.getAlcoholBaseType().getDescription(),
- c.getCocktailImgUrl(),
- c.getCocktailStory()
- ))
+ .map(CocktailSearchResponseDto::from)
.collect(Collectors.toList());
return resultDtos;
}
-// private List nullIfEmpty(List list) {
-// return CollectionUtils.isEmpty(list) ? null : list;
-// }
-
// ์นตํ
์ผ ์์ธ์กฐํ
@Transactional(readOnly = true)
public CocktailDetailResponseDto getCocktailDetailById(Long cocktailId) {
@@ -133,18 +126,7 @@ public CocktailDetailResponseDto getCocktailDetailById(Long cocktailId) {
// ingredient ๋ถ์ ๋ณํ
List formattedIngredient = parseIngredients(convertFractions(cocktail.getIngredient()));
- return new CocktailDetailResponseDto(
- cocktail.getId(),
- cocktail.getCocktailName(),
- cocktail.getCocktailNameKo(),
- cocktail.getAlcoholStrength().getDescription(),
- cocktail.getCocktailType().getDescription(),
- cocktail.getAlcoholBaseType().getDescription(),
- cocktail.getCocktailImgUrl(),
- cocktail.getCocktailStory(),
- formattedIngredient,
- cocktail.getRecipe()
- );
+ return CocktailDetailResponseDto.from(cocktail, formattedIngredient);
}
private String convertFractions(String ingredient) {
diff --git a/src/main/java/com/back/domain/mybar/controller/MyBarController.java b/src/main/java/com/back/domain/mybar/controller/MyBarController.java
index d8047ffd..808dbb77 100644
--- a/src/main/java/com/back/domain/mybar/controller/MyBarController.java
+++ b/src/main/java/com/back/domain/mybar/controller/MyBarController.java
@@ -1,5 +1,6 @@
package com.back.domain.mybar.controller;
+import com.back.domain.mybar.dto.MyBarIdResponseDto;
import com.back.domain.mybar.dto.MyBarListResponseDto;
import com.back.domain.mybar.service.MyBarService;
import com.back.global.rsData.RsData;
@@ -7,14 +8,20 @@
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
+import java.time.LocalDateTime;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-
-import java.time.LocalDateTime;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/me/bar")
@@ -23,41 +30,32 @@
@PreAuthorize("isAuthenticated()")
public class MyBarController {
- /**
- * ๋ด ๋ฐ(ํต) API ์ปจํธ๋กค๋ฌ.
- * ๋ด๊ฐ ํตํ ์นตํ
์ผ ๋ชฉ๋ก ์กฐํ, ํต ์ถ๊ฐ/๋ณต์, ํต ํด์ ๋ฅผ ์ ๊ณตํฉ๋๋ค.
- */
-
private final MyBarService myBarService;
- /**
- * ๋ด ๋ฐ ๋ชฉ๋ก ์กฐํ(๋ฌดํ์คํฌ๋กค)
- * @param userId ์ธ์ฆ๋ ์ฌ์ฉ์ ID
- * @param lastKeptAt ์ด์ ํ์ด์ง ๋ง์ง๋ง keptAt (์ต์
)
- * @param lastId ์ด์ ํ์ด์ง ๋ง์ง๋ง id (์ต์
)
- * @param limit ํ์ด์ง ํฌ๊ธฐ(1~100)
- * @return ํต ์์ดํ
๋ชฉ๋ก๊ณผ ๋ค์ ํ์ด์ง ์ปค์
- */
@GetMapping
- @Operation(summary = "๋ด ๋ฐ ๋ชฉ๋ก", description = "๋ด๊ฐ ํตํ ์นตํ
์ผ ๋ชฉ๋ก ์กฐํ. ๋ฌดํ ์คํฌ๋กค ์ปค์ ์ง์")
- public RsData getMyBarList(
+ @Operation(summary = "๋ด ๋ฐ ๊ฒฝ๋ ๋ชฉ๋ก", description = "์ฐ ID ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.")
+ public RsData> getMyBarIds(
+ @AuthenticationPrincipal SecurityUser principal
+ ) {
+ Long userId = principal.getId();
+ List body = myBarService.getMyBarIds(userId);
+ return RsData.successOf(body);
+ }
+
+ @GetMapping("/detail")
+ @Operation(summary = "๋ด ๋ฐ ์์ธ ๋ชฉ๋ก", description = "์ปค์ ๊ธฐ๋ฐ์ผ๋ก ์์ธ ์ฐ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.")
+ public RsData getMyBarDetail(
@AuthenticationPrincipal SecurityUser principal,
@RequestParam(required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastKeptAt,
@RequestParam(required = false) Long lastId,
- @RequestParam(defaultValue = "20") @Min(1) @Max(100) int limit
+ @RequestParam(defaultValue = "50") @Min(1) @Max(100) int limit
) {
Long userId = principal.getId();
- MyBarListResponseDto body = myBarService.getMyBar(userId, lastKeptAt, lastId, limit);
+ MyBarListResponseDto body = myBarService.getMyBarDetail(userId, lastKeptAt, lastId, limit);
return RsData.successOf(body);
}
- /**
- * ํต ์ถ๊ฐ(์์ฑ/๋ณต์/์ฌํต)
- * @param userId ์ธ์ฆ๋ ์ฌ์ฉ์ ID
- * @param cocktailId ์นตํ
์ผ ID
- * @return 201 kept
- */
@PostMapping("/{cocktailId}/keep")
@Operation(summary = "ํต ์ถ๊ฐ/๋ณต์", description = "ํด๋น ์นตํ
์ผ์ ๋ด ๋ฐ์ ํตํฉ๋๋ค. ์ด๋ฏธ ์ญ์ ์ํ๋ฉด ๋ณต์")
public RsData keep(
@@ -66,15 +64,9 @@ public RsData keep(
) {
Long userId = principal.getId();
myBarService.keep(userId, cocktailId);
- return RsData.of(201, "kept"); // Aspect๊ฐ HTTP 201๋ก ์ค์
+ return RsData.of(201, "kept");
}
- /**
- * ํต ํด์ (์ํํธ ์ญ์ ) โ ๋ฉฑ๋ฑ
- * @param userId ์ธ์ฆ๋ ์ฌ์ฉ์ ID
- * @param cocktailId ์นตํ
์ผ ID
- * @return 200 deleted
- */
@DeleteMapping("/{cocktailId}/keep")
@Operation(summary = "ํต ํด์ ", description = "๋ด ๋ฐ์์ ํด๋น ์นตํ
์ผ์ ์ญ์ (์ํํธ ์ญ์ , ๋ฉฑ๋ฑ)")
public RsData unkeep(
diff --git a/src/main/java/com/back/domain/mybar/dto/MyBarIdResponseDto.java b/src/main/java/com/back/domain/mybar/dto/MyBarIdResponseDto.java
new file mode 100644
index 00000000..90bc0634
--- /dev/null
+++ b/src/main/java/com/back/domain/mybar/dto/MyBarIdResponseDto.java
@@ -0,0 +1,22 @@
+package com.back.domain.mybar.dto;
+
+import com.back.domain.mybar.entity.MyBar;
+import java.time.LocalDateTime;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class MyBarIdResponseDto {
+ private Long id;
+ private Long cocktailId;
+ private LocalDateTime keptAt;
+
+ public static MyBarIdResponseDto from(MyBar myBar) {
+ return MyBarIdResponseDto.builder()
+ .id(myBar.getId())
+ .cocktailId(myBar.getCocktail().getId())
+ .keptAt(myBar.getKeptAt())
+ .build();
+ }
+}
diff --git a/src/main/java/com/back/domain/mybar/dto/MyBarItemResponseDto.java b/src/main/java/com/back/domain/mybar/dto/MyBarItemResponseDto.java
index 8603cf6f..eea56894 100644
--- a/src/main/java/com/back/domain/mybar/dto/MyBarItemResponseDto.java
+++ b/src/main/java/com/back/domain/mybar/dto/MyBarItemResponseDto.java
@@ -1,5 +1,6 @@
package com.back.domain.mybar.dto;
+import com.back.domain.cocktail.enums.AlcoholStrength;
import com.back.domain.mybar.entity.MyBar;
import lombok.Builder;
import lombok.Getter;
@@ -12,6 +13,8 @@ public class MyBarItemResponseDto {
private Long id;
private Long cocktailId;
private String cocktailName;
+ private String cocktailNameKo; // ์นตํ
์ผ์ ํ๊ธ ํ๊ธฐ ์ด๋ฆ
+ private AlcoholStrength alcoholStrength; // ๋์ ๋ ์ด๋ธ๋ก ์ฐ์ด๋ ์์ฝ์ฌ ๊ฐ๋
private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime keptAt;
@@ -21,6 +24,8 @@ public static MyBarItemResponseDto from(MyBar m) {
.id(m.getId())
.cocktailId(m.getCocktail().getId())
.cocktailName(m.getCocktail().getCocktailName())
+ .cocktailNameKo(m.getCocktail().getCocktailNameKo())
+ .alcoholStrength(m.getCocktail().getAlcoholStrength())
.imageUrl(m.getCocktail().getCocktailImgUrl())
.createdAt(m.getCreatedAt())
.keptAt(m.getKeptAt())
diff --git a/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java b/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java
index 84c45b96..f4c858b3 100644
--- a/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java
+++ b/src/main/java/com/back/domain/mybar/repository/MyBarRepository.java
@@ -15,9 +15,11 @@
@Repository
public interface MyBarRepository extends JpaRepository {
- /** ๋๋ง์ bar(ํต) ๋ชฉ๋ก: ACTIVE๋ง, id desc */
+ /** ๋๋ง์ bar(ํต) ๋ชฉ๋ก: ACTIVE๋ง, keptAt desc + id desc */
Page findByUser_IdAndStatusOrderByKeptAtDescIdDesc(Long userId, KeepStatus status, Pageable pageable);
+ List findByUser_IdAndStatusOrderByKeptAtDescIdDesc(Long userId, KeepStatus status);
+
@Query("""
select m from MyBar m
where m.user.id = :userId
diff --git a/src/main/java/com/back/domain/mybar/service/MyBarService.java b/src/main/java/com/back/domain/mybar/service/MyBarService.java
index c93d22b9..6f7ced4a 100644
--- a/src/main/java/com/back/domain/mybar/service/MyBarService.java
+++ b/src/main/java/com/back/domain/mybar/service/MyBarService.java
@@ -1,6 +1,7 @@
package com.back.domain.mybar.service;
import com.back.domain.cocktail.repository.CocktailRepository;
+import com.back.domain.mybar.dto.MyBarIdResponseDto;
import com.back.domain.mybar.dto.MyBarItemResponseDto;
import com.back.domain.mybar.dto.MyBarListResponseDto;
import com.back.domain.mybar.entity.MyBar;
@@ -8,6 +9,10 @@
import com.back.domain.mybar.repository.MyBarRepository;
import com.back.domain.user.repository.UserRepository;
import com.back.domain.user.service.AbvScoreService;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -15,11 +20,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
@Service
@RequiredArgsConstructor
public class MyBarService {
@@ -33,7 +33,15 @@ public class MyBarService {
// - ์ปค์: lastKeptAt + lastId ์กฐํฉ์ผ๋ก ์์ ์ ์ธ ์ ๋ ฌ/ํ์ด์ง๋ค์ด์
// - ์ฒซ ํ์ด์ง: ๊ฐ์ฅ ์ต๊ทผ keptAt ๊ธฐ์ค์ผ๋ก ์ต์ ์
@Transactional(readOnly = true)
- public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long lastId, int limit) {
+ public List getMyBarIds(Long userId) {
+ List rows = myBarRepository.findByUser_IdAndStatusOrderByKeptAtDescIdDesc(userId, KeepStatus.ACTIVE);
+ return rows.stream()
+ .map(MyBarIdResponseDto::from)
+ .collect(Collectors.toList());
+ }
+
+ @Transactional(readOnly = true)
+ public MyBarListResponseDto getMyBarDetail(Long userId, LocalDateTime lastKeptAt, Long lastId, int limit) {
int safeLimit = Math.max(1, Math.min(limit, 100));
int fetchSize = safeLimit + 1; // ๋ค์ ํ์ด์ง ์ฌ๋ถ ํ๋จ์ฉ์ผ๋ก 1๊ฐ ๋ ์กฐํ
@@ -50,10 +58,13 @@ public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long
// +1 ๋ก์ฐ๊ฐ ์์ผ๋ฉด ๋ค์ ํ์ด์ง๊ฐ ์กด์ฌ
boolean hasNext = rows.size() > safeLimit;
- if (hasNext) rows = rows.subList(0, safeLimit);
+ if (hasNext) {
+ rows = rows.subList(0, safeLimit);
+ }
- List items = new ArrayList<>();
- for (MyBar myBar : rows) items.add(MyBarItemResponseDto.from(myBar));
+ List items = rows.stream()
+ .map(MyBarItemResponseDto::from)
+ .collect(Collectors.toList());
LocalDateTime nextKeptAt = null;
Long nextId = null;
diff --git a/src/main/java/com/back/domain/post/comment/repository/CommentRepository.java b/src/main/java/com/back/domain/post/comment/repository/CommentRepository.java
index 238ee02b..f141c1ee 100644
--- a/src/main/java/com/back/domain/post/comment/repository/CommentRepository.java
+++ b/src/main/java/com/back/domain/post/comment/repository/CommentRepository.java
@@ -1,6 +1,7 @@
package com.back.domain.post.comment.repository;
import com.back.domain.post.comment.entity.Comment;
+import com.back.domain.post.comment.enums.CommentStatus;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -8,7 +9,9 @@
@Repository
public interface CommentRepository extends JpaRepository {
- List findTop10ByPostIdOrderByIdDesc(Long postId);
+ // ์ฒซ ํ์ด์ง (lastId == null)
+ List findTop10ByPostIdAndStatusNotOrderByIdDesc(Long postId, CommentStatus status);
- List findTop10ByPostIdAndIdLessThanOrderByIdDesc(Long postId, Long lastId);
+ // ๋ฌดํ์คํฌ๋กค (lastId != null)
+ List findTop10ByPostIdAndIdLessThanAndStatusNotOrderByIdDesc(Long postId, Long id, CommentStatus status);
}
diff --git a/src/main/java/com/back/domain/post/comment/service/CommentService.java b/src/main/java/com/back/domain/post/comment/service/CommentService.java
index e8f5e8a7..1d0aaca6 100644
--- a/src/main/java/com/back/domain/post/comment/service/CommentService.java
+++ b/src/main/java/com/back/domain/post/comment/service/CommentService.java
@@ -68,12 +68,12 @@ public CommentResponseDto createComment(Long postId, CommentCreateRequestDto req
@Transactional(readOnly = true)
public List getComments(Long postId, Long lastId) {
if (lastId == null) {
- return commentRepository.findTop10ByPostIdOrderByIdDesc(postId)
+ return commentRepository.findTop10ByPostIdAndStatusNotOrderByIdDesc(postId, CommentStatus.DELETED)
.stream()
.map(CommentResponseDto::new)
.toList();
} else {
- return commentRepository.findTop10ByPostIdAndIdLessThanOrderByIdDesc(postId, lastId)
+ return commentRepository.findTop10ByPostIdAndIdLessThanAndStatusNotOrderByIdDesc(postId, lastId, CommentStatus.DELETED)
.stream()
.map(CommentResponseDto::new)
.toList();
@@ -95,7 +95,7 @@ public CommentResponseDto updateComment(Long postId, Long commentId, CommentUpda
Comment comment = findCommentWithValidation(postId, commentId);
- if (!comment.getUser().equals(user)) {
+ if (!comment.getUser().getId().equals(user.getId())) {
throw new IllegalStateException("๋ณธ์ธ์ ๋๊ธ๋ง ์์ ํ ์ ์์ต๋๋ค.");
}
@@ -113,7 +113,7 @@ public void deleteComment(Long postId, Long commentId) {
Comment comment = findCommentWithValidation(postId, commentId);
- if (!comment.getUser().equals(user)) {
+ if (!comment.getUser().getId().equals(user.getId())) {
throw new IllegalStateException("๋ณธ์ธ์ ๋๊ธ๋ง ์ญ์ ํ ์ ์์ต๋๋ค.");
}
diff --git a/src/main/java/com/back/domain/post/post/entity/Post.java b/src/main/java/com/back/domain/post/post/entity/Post.java
index c0734789..70791888 100644
--- a/src/main/java/com/back/domain/post/post/entity/Post.java
+++ b/src/main/java/com/back/domain/post/post/entity/Post.java
@@ -68,6 +68,7 @@ public class Post {
private List comments = new ArrayList<>();
// Post โ PostImage = 1:N
+ @Builder.Default
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("sortOrder ASC") // ์กฐํ ์ ์์๋๋ก ์ ๋ ฌ
private List images = new ArrayList<>();
@@ -76,6 +77,7 @@ public class Post {
@Column(name = "video_url")
private String videoUrl;
+ @Builder.Default
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List postTags = new ArrayList<>();
diff --git a/src/main/java/com/back/domain/post/post/service/PostService.java b/src/main/java/com/back/domain/post/post/service/PostService.java
index 5dbbe264..b5a2161f 100644
--- a/src/main/java/com/back/domain/post/post/service/PostService.java
+++ b/src/main/java/com/back/domain/post/post/service/PostService.java
@@ -116,7 +116,7 @@ public List getPosts(PostSortScrollRequestDto reqBody) {
}
// ๊ฒ์๊ธ ๋จ๊ฑด ์กฐํ ๋ก์ง
- @Transactional(readOnly = true)
+ @Transactional
public PostResponseDto getPost(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new NoSuchElementException("ํด๋น ๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค. ID: " + postId));
diff --git a/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java b/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java
index 3e705940..5131ea5d 100644
--- a/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java
+++ b/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java
@@ -16,6 +16,5 @@ public static class UserInfoDto {
private final String nickname;
private final Boolean isFirstLogin;
private final Double abvDegree;
-
}
}
diff --git a/src/main/java/com/back/domain/user/entity/User.java b/src/main/java/com/back/domain/user/entity/User.java
index c746c241..4ad1a26d 100644
--- a/src/main/java/com/back/domain/user/entity/User.java
+++ b/src/main/java/com/back/domain/user/entity/User.java
@@ -14,6 +14,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Objects;
@Entity
@Table(name = "users") // ์์ฝ์ด ์ถฉ๋ ๋ฐฉ์ง๋ฅผ ์ํด "users" ๊ถ์ฅ
@@ -70,6 +71,17 @@ public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List postLikes = new ArrayList<>();
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ User user = (User) o;
+ return Objects.equals(id, user.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
public boolean isAdmin() {
return "ADMIN".equalsIgnoreCase(role);
diff --git a/src/main/java/com/back/domain/user/service/UserAuthService.java b/src/main/java/com/back/domain/user/service/UserAuthService.java
index 22b32a5c..6e58b8cf 100644
--- a/src/main/java/com/back/domain/user/service/UserAuthService.java
+++ b/src/main/java/com/back/domain/user/service/UserAuthService.java
@@ -25,7 +25,8 @@
@RequiredArgsConstructor
public class UserAuthService {
- static Set param1 = Set.of("๋๋ํ", "๋ ์ฌํ", "๋ง์ทจํ", "์๋ธ๋ธ", "์ผํฐํ", "์ํธ๋ฌ์ค", "๋์๋์", "ํก์๋", "๊ฑฐํ๊ฐ๋", "ํ์ด๋ณผํ",
+ static Set param1 = Set.of(
+ "๋๋ํ", "๋ ์ฌํ", "๋ง์ทจํ", "์๋ธ๋ธ", "์ผํฐํ", "์ํธ๋ฌ์ค", "๋์๋์", "ํก์๋", "๊ฑฐํ๊ฐ๋", "ํ์ด๋ณผํ",
"์์ฆ๋ง์", "์ธ์ธํ", "๊ฑฐ๋งํ", "์ฐ๋งํ", "๊ท์ฐฎ์", "์๋ฑํ", "๋ง์ด๊ฐ", "์ ์ธ์๊ธ", "์๊ถ์ฐฝ", "๊ธฐ๋ฌํ",
"์กธ๋ฆฐ", "์ผ์นํ", "์ฒ ํ์ ์ธ", "๋ฌด์ค๋ ฅ", "๋ฝ์กํ", "์ ํฌ์ ์ธ", "๋ฐฐ๋ถ๋ฅธ", "๋์ถฉํ", "์ฉ๋", "์ฒ ์ง๋",
"์ ๊ทํ๋", "๋ง์ถคํ", "๋ค๊ธํ", "์ฐ๋ฟ๋ฅํ", "๊ตฌ์ํ", "๋ฌธ์ด๋ฐ", "์ํฌ์๊ธฐ", "ํฐ๋ฌด๋", "๊ท์ฒ", "์ฌ๋๋ ํ",
@@ -33,48 +34,43 @@ public class UserAuthService {
"ํ๋ฌดํ", "ํ๊ธฐ์นจ", "๋ฟ์ด๋๋", "์ง์ฒํ", "๊ธฐ์ด๋ค๋", "ํค๋งค๋", "์์ฃฝํ", "์
์๋ฐ์น", "๊ฒฉ๋ ฌํ", "์๊น๋ฒ์ฉ",
"์ค์ง๋", "์ชผ๋ฅด๋ฅด", "๊ฟ๊บฝ", "๋จธ์ฑํ", "ํ์ฒญ๋๋", "์ถ์ ", "์ฒ๋ฐฉ์ง์ถ", "์ด๋ฆฌ๋ฅ์ ", "์ง์ฃผํ๋", "๊ฒธ์ฐ์ฉ์",
"๋ฟ์ฐ", "์ฉ์", "์ง ๋ด๋๋", "์ฒ ์ฉ", "ํฅ๊ฑดํ", "์๊ฐํ", "๋จ๋ํ", "๊พธ๋ํ", "๋๊ณต์ง์ง", "๋์ง๋์ง",
- "๋น๋ฐ", "๊ฐ์ดํ", "์ฌ๋ํ", "์์ธํ", "ํฐ์ง๋ฏํ", "๋ฌ๋ฌํ", "์ฌ์
ํ", "๊ธฐ๊ดดํ", "์ฉ๋งนํ", "๊ป๋๋ฌ์ด",
+ "๊ฐ์ดํ", "์ฌ๋ํ", "์์ธํ", "ํฐ์ง๋ฏํ", "๋ฌ๋ฌํ", "์ฌ์
ํ", "๊ธฐ๊ดดํ", "์ฉ๋งนํ", "๊ป๋๋ฌ์ด",
"ํ๋ก์ด๋", "ํ๋ฅ๋๋", "๋ถ๋", "์ ๋งคํ", "์ฐ๋ํ", "ํ๊ธฐ์ง", "์ฉ์ด๋ฒ๋ฆฐ", "๋ชฝ๋กฑํ", "ํ์ธ", "ํฉ๋นํ",
- "๊ฑฐ๋์์", "๋์ฐจ๊ฒ๊ตฌ๋ฆผ", "์ด์ด์์", "๋ํต์ฝ", "์ง๊ฐ", "์ด์ค์๊ฐ", "๋์นจ๋", "๊ณ ๋ฌด์ฅ๊ฐ", "์์๊ฑด", "๋ฐ๋๊ฐ๋น",
- "์งํ์ฒ ํ", "์ก์ง๊ฐ๋ฃจ", "์ฒ ๊ฐ๋ฐฉ", "๋จธ๋ฆฌ๋", "์๋งํ์ง", "๋ผ์ดํฐ", "์๊ฐ๋ฝ", "์คํฐ์ปค", "๋๋ผํต", "์ด์ ",
- "๋ฒผ๋ฝ", "๋๊ฑธ๋ ", "ํ๋ฆฌ์ฑ", "์๊ธ๋นต", "๋ ๊ฐ", "์คํฐ๋กํผ", "๊ฑด์ ์ง", "๊ป์ข
์ด", "์ํ์ ", "๋น๋์ฐ์ฐ",
- "๊ณ ๋๋ฆ", "์ ๋ฑ๊ฐ", "์์ด", "์ง์ฐ๊ฐ", "๊ตญ์", "๋ฐฅ์ฅ", "์ฐํ์ฌ", "๊นํธ", "์ฐ์งํฉ", "์ฒญํ
์ดํ",
- "๊น๋ฐฅ๋ง์ด", "๊ณฐํก์ด", "์ฒญ์๊ธฐ", "๋ฐค์ก์ด", "์ฅ์์", "์ฒ ์ฐฝ์ด", "ํด์ง์ฌ", "์ ๋ฐ", "๊ณฝํฐ์", "์คํ๋ง",
+ "๊ฑฐ๋์์", "์์ํ", "์ด์ด์๋", "๋ํต์ฝ", "์ด์ค์๊ฐ", "๋์นจ๋", "๊ณ ๋ฌด์ฅ๊ฐ", "์์๊ฑด", "๋ฐ๋๊ฐ๋น",
+ "์งํ์ฒ ํ", "์ก์ง๊ฐ๋ฃจ", "์ฒ ๊ฐ๋ฐฉ", "๋จธ๋ฆฌ๋", "์๋งํ์ง", "ํ๋ฆฌ์ฑ", "์๊ธ๋นต", "๋ ๊ฐ", "์คํฐ๋กํผ", "๊ฑด์ ์ง",
+ "๊ป์ข
์ด", "์ํ์ ", "๋น๋์ฐ์ฐ", "๊ณ ๋๋ฆ", "์ ๋ฑ๊ฐ", "์์ด", "์ง์ฐ๊ฐ", "๊ตญ์", "๋ฐฅ์ฅ", "์ฐํ์ฌ", "๊นํธ",
+ "์ฐ์งํฉ", "์ฒญํ
์ดํ", "๊ณฐํก์ด", "์ฒญ์๊ธฐ", "๋ฐค์ก์ด", "์ฅ์์", "์ฒ ์ฐฝ์ด", "ํด์ง์ฌ", "์ ๋ฐ", "๊ณฝํฐ์", "์คํ๋ง",
"๊ณ ํฅ๋์ฅ", "๋จธ๋ํฉ", "์ฅ๋
๋", "๊ฐ์ง", "์ด๋ฌต๊ผฌ์น", "ํํ๊ธฐ", "๊ตฐ๊ณ ๊ตฌ๋ง", "์นด์ธํธ", "๊ฑด์กฐ๋", "๋ฐ์นด์ค๋ณ",
- "์ฐ์ฒดํต", "์ฃผ์ฐจ๊ถ", "ํธ์ค๋ญ์น", "์งํ์", "์ถ๋ฆฌ๋", "์ด๋ถ๊ฐ", "์กํฌ", "๋นจ๋", "์ง๋ ์ด", "๊น์นซ๊ตญ",
+ "์ฐ์ฒดํต", "์ฃผ์ฐจ๊ถ", "ํธ์ค๋ญ์น", "์งํ์", "์ถ๋ฆฌ๋", "์ด๋ถํฅ", "์กํฌ", "๋นจ๋", "์ง๋ ์ด", "๊น์นซ๊ตญ",
"์ค์ง์ด์ฑ", "์ ๊ธฐ์ฅํ", "๊ฝ๋ณ", "๋์๋ฝํต", "๊ตฌ๊ธ์์", "์๋ฐฐ์ถ์", "๊ณ ๋ฌด์ค", "๋ง์น", "์ ํต๊ธฐํ", "์๋์๊ณ",
- "๋ฐฉ๋ฒ์ฐฝ", "๊น์ฐฝ", "๋ง์ทจ์กํฌ", "๋ ์ฌ๊ตญ์", "ํฐํ๊ฐ์ง", "์์ธ๋ฐฅ์ฅ", "์ฌ์
๊น์น", "ํ์ธ์๊ฐ", "์๋ฑ๊ณฐํก");
-
- static Set param2 = Set.of("๋ํ ๋ฆฌ๋ฑ๊ฐ๊ตฌ๋ฆฌ", "์ํ๋ฆฌ์นด๋ค๊ฐ", "๊ฐ๋จ์ฑ์ธ๊ตฐ์", "์ ๊ณ ๋", "์์ฝ์ฌ๋ฌ๋ฒ", "๊ฒจ์์", "์ฒญ๊ฐ๊ตฌ๋ฆฌ", "์ฐ์์ ",
- "๋งฅ์ฃผ๋ฌธ์ด", "์นตํ
์ผ์ต๋ฌด์", "๋ณด๋์นด์๋ฌ", "์งํ ๋๊ฑฐ๋ถ์ด", "ํ
ํฌ๋ผ์ฝ์ํ
", "๋ผํญ๊ท", "์ฌ์ผ๊ณ ์์ด", "๋ง๊ฑธ๋ฆฌ๋๊บผ๋น",
- "ํ์ด๋ณผํ๋ค", "๋ชจํํ ๋๊ณ ๋", "ํผ๋์ฝ๋ผ๋ค๊ณฐ", "์ดํ์ธํญ๊ท", "ํ์ด์์ญ์ด", "๋ค๊ทธ๋ก๋์ฒญ๋
", "IPA์ฑ๊ธฐ์ฌ",
- "๋ธ๋ฌ๋๋ฉ๋ฆฌ์ฌ์ฌ", "์์คํคํธ๋์ด", "์ํ์ฐจํ ๋ผ", "์ ์๋๋กฑ๋ฝ", "๋ณต๋ถ์์ฌ์ฐ", "๊ตญํ์ฃผํด์ ๋จ", "์๋งฅ์ธ๋",
- "์ ํต์ฃผ๊ณต๋ฃก", "ํ์ ์
์ด", "์ค์ง์ด์์ทจ๋จ", "๋ฏผํธ๋ผ์ฟค", "๋
์ฝฉ๋ฒํฐ๊ณต์์", "์ํ๋๋ฌด๋๊ตฌ๋ฆฌ", "๊ณ ๋์ฃผํญ๊ท",
- "๋น๋น๋ฐฅ๋ฐ๋คํ๋ฒ", "๋ผ์ง๊ป๋ฐ๊ธฐ์ฐธ์", "์์ฃผ์๊ธฐ๋ฆฐ", "๋์์ฅํฌ์ฝ๋ผ๋ฆฌ", "๊ตฐ๋ง๋์ผ๋ฃฉ๋ง", "๋ง๋ผํ๋๊ตฌ๋ฆฌ",
- "์ผ๊ฒน์ด์ฒญ๋
", "๊ณฑ์ฐฝ์๋ฌ", "์นํจ๋์ฌ", "๋ผ๋ฉด์์ฆ", "๋ด๋ณตํ ๋ผ", "๋๋ฉด๋ถ์ฌ์กฐ", "์ ค๋ฆฌ๊ณฐํดํ๋ฆฌ", "์์ด์ค๋ง๊ณฐ",
- "์ ๊ฐ๋ฝํ ๋ค์ด๋", "๊ธฐ๋ฆ๋ก๋ณถ์ด์๋ฌ", "๊ณ ๊ตฌ๋ง๋ฐ๋๊ฐ๋น", "ํ์ธ์ ํ์
๋ง", "๋ฒ๋ฐ๊ธฐ๊ธฐ์ฌ๋จ", "๊ณฐํํ๋ค",
- "๋ง๋๋นตํ ๋ฆฌ์ปจ", "์ฅ์์์์ผ์ ", "๋ฟ๋งํด๋๋๊ณค", "๊ป๋ฑ์ง์์ญ์ด", "๊ณค๋๋ ๋ผ์ฟค", "์คํฐ์ปคํค๋ผํด๋ ์ค",
- "์ผ์๋ณผํ์นํ", "์ค๋ ์ง๋ฌธ์ด๊ตญ์", "๊ฐ์ฅ๊ฒ์ฅ๊ฑฐ๋ถ", "์นด์คํ
๋ผ๋ฐํด", "์ด์ฝ์ก์ดํ์กฐ", "๊ฑด๋นต์
์ด",
- "๋๊ตฌ๋ฆฌ๋น์๋์ฑ
๋ณธ๋ถ", "๋ํ๊ตฌ์ด์ฒ์ฌ", "๊ณจ๋ฑ
์ด๋ฒํ๋ก", "๋ผ๋ผ๋ง๋ผํค์ ์", "๋ธ๊ธฐ์ํฌ๋ฆผ์ฝ์๋ผ",
- "์ฐน์๋ก๊ณ ๋", "๊ฟ๊ฟ์ ๋น", "๋ฒ๊ฐ์นํจ์ง์ฌ", "๊ณ ์นผ์์ฒญ์์น", "๊ฐ๊ทธ๋ฆฐ๋๋ง๋ฑ", "์ํ์ ์
๋ง", "๋ฏผํธ์ด์ฝ๊ท์ ",
- "ํต๋ญ์๋ฌด๋์ฅ", "๋ฐ๊ฑด์กฐ์ค์ง์ด๊ตฐ๋จ", "์ฐธ๊นจ๋ถ์์ด", "๋ฐ๋๋ํด์ปค", "๋ณต์ญ์๋๋๋๊ตฌ๋ฆฌ", "๋์ตธ๊ป๋ฐ๊ธฐ",
- "๋์ฅ๋น๋ฒ", "์ ์๋ ์ธ์ง๊ณฐ", "๋๋นํญ๊ท", "์ฃผ์ ์์ฌ๋ฅ๊ฐ", "์ฝ์น์ฆํ๋๋ผ", "์ฐ์ ํฉํ ๋ฐฐ", "๋ง๊ฑธ๋ฆฌ๋๋กฑ๋ฝ",
- "์งฌ๋ฝ๊ธฐ๋ฆฐ", "๊น์น๋ง๋์ฌ์ ", "์ค์ด๋๋ฌด๋๋ณด", "๋ฒํฐ์ฟ ํค์ด์พก์ด", "๋์น๋ฏธํด๊ณจ", "์ฒญ์๊ณ ์ถ๋๊ณ ๋",
- "๋ค์ฌ๊ธฐ์๋ฏผ", "์์ฌ๋น๋๋๊ณค", "๋ถ์์ง์นด๋ฉ๋ ์จ", "๊ณฐ์ ค๋ฆฌ์ ์ฌ", "๊ทค๊ป์ง๊ธฐ์ฌ", "๋ฉธ์น์๊ตญ", "์๋งฅ๋ฐ์ดํน",
- "๋ณ๋ฐ๊ฐ๋๋ง๋ฑ", "๊ตดํ๊น๋ฌํฝ์ด", "์นด๋ ํธ๋์ด", "ํ์ฌ๋ฆฌ๋๋", "์ค์ฝ๋
ธ๋ฏธ์ผ๋ผํ๋ค", "๊ฝ๋ฐฐ๊ธฐ๋๋",
- "๋ฐํฌํฐ๋๊ณ ๋", "๊ณ ๊ธฐ๊ตญ์์บฅ๊ฑฐ๋ฃจ", "์ด์ฝํ์ด์ฌ๋จ", "ํด์ฅ๊ตญ๊ณฐ", "์ฐ๋ ๊ธฐํต์์ ", "๋ฌ๊ณ ๋๋๊นจ๋น",
- "์ผ๋ค์๊ฑฐ๋ถ", "ํ๊ฐ์ฐจ๋๋ง๋ฑ", "์นด๋ํธ์์
๋ง", "์นํจ๋ฐ๋ฐ๋ฅ", "๋ฑ์ ์ํธ์", "ํ์ ๋๊ตฌ๋ฆฌ", "์ฝฉ๋๋ฌผ์นด๋ฉ๋ ์จ",
- "๋ํจ์ผ๊ฒน๋๊ณ ๋", "๊ตด๋น๊ฐ์์ง", "๋ง์ฐฝํญ๊ท", "๊ฐ์ํ๊น์น๊ตฌ", "์ด๋ฌต์ฌ์", "๋ถ์ถ๋ง๋ฒ", "ํ์์กํ์คํฐ",
- "๋งค์ดํ๋น๋๊ธฐ", "๋ง๋ผ์ ๊ณจํ ๋ผ", "๋ผ์ง๊ป๋ฐ๊ธฐ๊ฐ๊ตฌ๋ฆฌ", "์ ๊ตญํธ๋์ด", "๋๋ถ์ค๋ฆฌ", "๊น๋๊ธฐ์ฝ๋ผ๋ฆฌ",
- "๋ผ๋ณถ์ด์ฌ์ด", "์ํ๋ง๋ฌธ์ด", "ํผ์์ฒญ๊ฐ๊ตฌ๋ฆฌ", "๊ณ ๋ฑ์ดํญ๊ท", "๊ตญ๋ฐฅํ์ถฉ๋ฅ", "๋ญํธ๋ง์", "๋ฐ๋๋์ฐ๋ญ",
- "๊น๋ง์ด์นํ", "์ ๊ฐ๋ฝ๋ง๋ฏธ์", "๋ฌผํ๊ฑฐ๋ถ์ด", "ํ์นํ์ด์๋", "์ฒญํ์์ด", "์ฐธ์น๊ฝ์น", "ํด์ฅ๋ผ๋ฉด๋งค๋จธ๋",
- "์๊ผฌ์นํ ๋ผ", "์๋ก์๋ก๋๋ฐฉ", "๋ฌ๊ฑ๋ง์ด์์ญ์ด", "๊น๋ฐฅํญ๊ท", "์ฐธ์ธ๋ฉ๊ฒ", "๊ณ ์ถ์ ๊ฐ", "์น์ฆ๋ฎ๋ฐฅ์ฌ์ฐ",
- "๋ญ๊ป์ง๊ณฐ", "๊นป์๋ฌด๋น๋ฒ๋ ", "๊ฐ๋น์ฐ๋๋ง๋ฑ", "๋ฏธ์ญ๊ตญ๋๊ณ ๋", "์์ฑ์์ฌ์", "๋๋ฃจ์น๊ธฐ์ฒญ์์น", "๊ณ๋ํ๋ผ์ด๋๋",
- "๊น์น์ฐ๊ฐํ ๋ผ", "์นผ๊ตญ์๋ผ์ฟค", "์ฐ๊ฐ๋๋ฐฉ", "ํด๋ฌผํ์ฝ๋ฟ์", "์๊ตญ์ํ๋ฒ", "๋ก๊ผฌ์น์์ด", "๋ ์น์๊น๋ง๊ท",
- "๋ผ๋ฉ์๋ฌ", "๋๋ฒ ๊ณต๋ฃก", "๋ค์๋ง๋๊ณ ๋", "๊ณฑ์ฐฝ์์ฌ์ด", "์ฝ๋ผ๋ถ๊ทน๊ณฐ", "๋์ฅ์ฐ๊ฐ๊ฐ์์ง", "์ ค๋ฆฌํธ๋์ด",
- "์นตํ
์ผ์ฐธ์", "๋ฒ๋ธํฐ์นํจ", "์ค๋ ์ง๋งฅ์ฃผ๋๋๊ณค", "๊ตฌ์ด์น์ฆ๊ธฐ๋ฆฐ", "๋ง๋๋นต๊ฑฐ๋ถ์ด", "์๊ณ ๊ธฐํ๋ค",
- "์ด์ฝ์ฐ์ ๋๊ตฌ๋ฆฌ", "์ํ๋ ๊ฑฐ๋ฏธ", "์ฅ์์ํ๊ธฐ๋ฆฐ", "ํผ์ํ ์คํธ์กฑ์ ๋น", "๋ก๊ฐ๋น์๋ฌ", "์ผ์ดํฌ๋ง๋ชจ์ค",
- "์ค์์ฐธ์", "๊ด์ด๋ฒํฐ์บฃ", "ํฉํ๊ตญ๋ผ์ฟค", "๊ฐ๋๋กํญ๊ท");
+ "๋ฐฉ๋ฒ์ฐฝ", "๊น์ฐฝ", "๋ง์ทจ์กํฌ", "๋ ์ฌ๊ตญ์", "ํฐํ๊ฐ์ง", "์์ธ๋ฐฅ์ฅ", "์ฌ์
๊น์น", "ํ์ธ์๊ฐ", "์๋ฑ๊ณฐํก,",
+ "ํน๋ฐ๋", "๋์ ํ๋", "๋ป์ญํ", "์ํผ์๋", "๊ทผ๋ณธ์๋", "์ ์ ๋๊ฐ", "๊ณจ๋๋ฆฌ๋", "๋ ๊บผ์ด", "์ค์ง๋", "์ง๋ฆฌ๋",
+ "ํ์คํฐ", "์ฒ๋ํ", "์๋ จํ", "์์ด๋กฌํ", "๋ฅ๊ธ๋ง์", "์์ผํ", "ํ๋ฌผํ", "๋ง๋ํ", "๋ฏธ๋ํ", "ํธ์ํ", "๋
๋
ํ",
+ "๋ฐ์ญํ", "๋งจ๋คํ", "์ค์นํ", "ํ๋ จํ", "๋๋ฅธํ", "์ํฌํ", "์ฟจํ", "ํํ", "์๋ฐฉ๊ถ", "๊ธ๋ฐ์ง", "์๋ก๋ฌ๋ก",
+ "๋๋ง์", "ํต์ธ์ธ", "์์ธ", "๋ฌด๋
๋ฌด์", "๋ง์ฌ๊ท์ฐฎ"
+ );
+
+ static Set param2 = Set.of(
+ "์ ๊ณ ๋", "๊ฒจ์์", "์ฒญ๊ฐ๊ตฌ๋ฆฌ", "์ฐ์์ ", "๋งฅ์ฃผ๋ฌธ์ด","์๋งฅ์ธ๋", "ํ์ ์
์ด", "๋ฏผํธ๋ผ์ฟค", "๋ด๋ณตํ ๋ผ", "๊ณฐํํ๋ค", "๊ฟ๊ฟ์ ๋น", "๋์ฅ๋น๋ฒ", "๋๋นํญ๊ท",
+ "์งฌ๋ฝ๊ธฐ๋ฆฐ", "๋ฉธ์น์๊ตญ", "ํด์ฅ๊ตญ๊ณฐ", "๋ง์ฐฝํญ๊ท", "์ด๋ฌต์ฌ์", "๋ถ์ถ๋ง๋ฒ", "๋๋ถ์ค๋ฆฌ", "๋ญํธ๋ง์", "์ฒญํ์์ด",
+ "์ฐธ์น๊ฝ์น", "ํญ๊ท์งฑ", "์ฐธ์ธ๋ฉ๊ฒ", "๊ณ ์ถ์ ๊ฐ", "๋ญ๊ป์ง๊ณฐ", "์ฐ๊ฐ๋๋ฐฉ", "๋ผ๋ฉ์๋ฌ", "๋๋ฒ ๊ณต๋ฃก", "์ค์์ฐธ์",
+ "๋๊บผ๋น", "๋๊ตฌ๋ฆฌ", "ํธ๋์ด", "๋๊นจ๋น", "์ ๋ น", "์์ ", "ํด์ ", "๋์", "์
๋ง", "์ฒ์ฌ", "๋น๋๊ธฐ", "์ฐธ์",
+ "๊ณ ์์ด", "๊ฐ์์ง", "๋ฌธ์ด", "์ค์ง์ด", "ํ์ด์๋", "์น์์", "๋ถ๋์ ", "๋ก์ผ", "์ฐ์ฃผ์ ",
+ "๋ง๊ฑธ๋ฆฌ", "์์ธ", "๊ณ ๋์ฃผ", "ํผ์๊ณฐ", "ํซ๋๊ทธ", "๊ณ๋๋นต", "๋ถ์ด๋นต",
+ "ํธ๋ก๋งจ", "๋ง๋๊ณฐ", "์ํ๋งจ", "๋ง์น๊ณฐ", "๋ณ๋ฐ๊ฐ", "์ ๋ฆฌ์ปต", "๋ฒฝ๋๋งจ", "์ ๋ด๋", "์ฒ ๊ฐ๋ฐฉ", "์ฃผ์ ์", "ํต์ฃผ๋จน",
+ "๋ถ๋ฐฉ๋ง", "๋๋ฉฉ์ด", "๋ฅํ๋ฆฌ", "๋ฐฉ๊ตฌ์์ด", "์ ๋ง๋ณด", "๋์๋ฝ", "๊ณ ๋ฌด์ค", "์ง์ฐ๊ฐ", "์๋์๊ณ", "๋ง์น",
+ "์ฐํ์ฌ", "๋ผ์ดํฐ", "ํ๋ฆฌ์ฑ", "๋ ๊ฐ", "์งํ์ฒ ", "๊ตญ๋ฐฅ์์ ", "์์ฃผ์์ ", "์์ฃผํฌ๋ฌ", "์คํธ๋ฌ",
+ "๊น์น๋๋", "๊น๋ฐฅ๋์ฌ", "ํ์ ์ ์ฌ", "์นํจ๊ท์กฑ", "๋ผ๋ฉด๋์", "๋์ธ", "์ ์ ", "๋ฌด๋น", "๊ด๋", "์ฌ๋", "๋ง๋๋", "๋ฌด๋ฒ์", "๋ชฝ์๊ฐ", "ํ๋",
+ "์นผ์ฝ์ด", "์ด์ก์ด", "๋๊ตด๊พผ", "์ฅ์ฌ์น", "ํ๊ฐ", "์๊ฐ", "์กฐํญ", "์ ๋น",
+ "๋ง์", "์ฉ์ฌ", "์ข๋น", "๊ฐ์", "๊ตฌ๋ฏธํธ", "๋๋", "์ฌ์ฐ", "๋ถ๊ณฐ", "ํํ", "์น๋ฅ์ด", "์ต", "ํดํ",
+ "๊ฐ๋ฌผ์น", "๋ฉ๊ธฐ", "๋ฏธ๊พธ๋ฆฌ", "์๊ฐ๋ฆฌ", "๋ ์น", "๊ฐ์ค๋ฆฌ", "ํด๋ง", "๋ถ๊ฐ์ฌ", "์ฑ๊ฒ", "๋ฉ๊ฒ", "ํด์ผ",
+ "์ฅ์", "์๊ถ์ด", "๋ถ๋๋ง", "๊ฐ๋ง์ฅ", "๋ฌผ๋ ๋ฐฉ", "์ด๊ฐ์ง", "๊ธฐ์์ฅ", "๋์ฒญ", "๋ง๋ฃจ", "์ฅ๋
",
+ "๋ง์ฅ", "์ผ์ ", "๋ชฝ๋ฅ์ด", "๊ณก๊ดญ์ด", "์ฝ์๋ฃจ", "๋ซ", "ํธ๋ฏธ", "์ง๊ฒ", "๋๋ชป", "๋์ฌ", "๋ํธ", "๋ณผํธ",
+ "ํตํญํ", "์๋ฅํ", "๋ํฌ", "๋ฏธ์ฌ์ผ", "ํฑํฌ", "์ ํจ", "ํญ๊ณต๋ชจ", "์ ์ํจ", "์ ํฌ๊ธฐ", "๋ ์ด๋",
+ "๋ณด๋์นด", "๋ฐํฌ๋ผ", "์ฌ์ผ", "์์คํค", "๊น๋ฃจ์", "์งํ ๋", "๋ชจํ๋", "์์งฑ",
+ "์ฐ๋นต","๋๋ค๋ ๋ค", "๋ปฅํ๊ธฐ", "์๋ฉ๋ฐ", "ํด์บ", "ํ๋ํฌ", "๊ฑด๋ฌ", "์์์น", "๊นกํจ", "๋ฐฑ์", "๊ผฐ๋",
+ "์ผ๋ฏผ์ด", "ํ๋ฑ", "๊ธ์", "ํ์", "์์ฌ", "์ด๋ชจ", "์ผ์ด", "๋๋งน์ด"
+ );
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
diff --git a/src/main/java/com/back/global/security/CustomAuthenticationFilter.java b/src/main/java/com/back/global/security/CustomAuthenticationFilter.java
index 062ed8b8..ade7a695 100644
--- a/src/main/java/com/back/global/security/CustomAuthenticationFilter.java
+++ b/src/main/java/com/back/global/security/CustomAuthenticationFilter.java
@@ -55,87 +55,120 @@ private void work(HttpServletRequest request, HttpServletResponse response, Filt
String uri = request.getRequestURI();
String method = request.getMethod();
- // ๊ฐ๋ฐ ํธ์์ฑ์ ์ํด ๋ชจ๋ ์์ฒญ ํต๊ณผ (SecurityConfig์์ ๋ชจ๋ ์์ฒญ permitAll)
- /*
- if (
- uri.startsWith("/h2-console") ||
- uri.startsWith("/login/oauth2/") ||
- uri.startsWith("/oauth2/") ||
- uri.startsWith("/actuator/") ||
- uri.startsWith("/swagger-ui/") ||
- uri.startsWith("/api-docs/") ||
- uri.equals("/") ||
- // ์กฐํ API๋ค - ๊ถํ ๋ถํ์
- (method.equals("GET") && uri.startsWith("/cocktails")) ||
- (method.equals("POST") && uri.equals("/cocktails/search")) ||
- (method.equals("GET") && uri.startsWith("/posts")) ||
- (method.equals("GET") && uri.contains("/comments"))
- ) {
- filterChain.doFilter(request, response);
- return;
- }
- */
+ log.debug("===== Authentication Filter Start =====");
+ log.debug("Request: {} {}", method, uri);
- // ์ฟ ํค์์ accessToken ๊ฐ์ ธ์ค๊ธฐ
- String accessToken = rq.getCookieValue("accessToken", "");
+ String accessToken = null;
+
+ // 1. ๋จผ์ Authorization ํค๋์์ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ ์๋
+ String authHeader = request.getHeader("Authorization");
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ accessToken = authHeader.substring(7);
+ log.debug("Token found in Authorization header");
+ }
- logger.debug("accessToken : " + accessToken);
+ // 2. Authorization ํค๋์ ์์ผ๋ฉด ์ฟ ํค์์ ๊ฐ์ ธ์ค๊ธฐ
+ if (accessToken == null || accessToken.isBlank()) {
+ accessToken = rq.getCookieValue("accessToken", "");
+ if (!accessToken.isBlank()) {
+ log.debug("Token found in Cookie");
+ }
+ }
boolean isAccessTokenExists = !accessToken.isBlank();
if (!isAccessTokenExists) {
+ log.debug("No access token found - proceeding without authentication");
filterChain.doFilter(request, response);
return;
}
User user = null;
- boolean isAccessTokenValid = false;
// accessToken ๊ฒ์ฆ
- if (isAccessTokenExists) {
- if (jwtUtil.validateAccessToken(accessToken)) {
+ if (jwtUtil.validateAccessToken(accessToken)) {
+ log.debug("Access token is valid");
+
+ try {
Long userId = jwtUtil.getUserIdFromToken(accessToken);
String email = jwtUtil.getEmailFromToken(accessToken);
String nickname = jwtUtil.getNicknameFromToken(accessToken);
- user = User.builder()
- .id(userId)
- .email(email)
- .nickname(nickname)
- .role("USER")
- .build();
- isAccessTokenValid = true;
+ if (userId != null) {
+ user = User.builder()
+ .id(userId)
+ .email(email)
+ .nickname(nickname)
+ .role("USER")
+ .build();
+
+ log.debug("User extracted - ID: {}, Email: {}, Nickname: {}", userId, email, nickname);
+ } else {
+ log.warn("User ID is null in token");
+ }
+ } catch (Exception e) {
+ log.error("Error extracting user info from token", e);
+ }
+ } else {
+ log.warn("Access token validation failed");
+
+ // ํ ํฐ์ด ๋ง๋ฃ๋ ๊ฒฝ์ฐ์๋ ์ ๋ณด ์ถ์ถ ์๋ (์ ํ์ )
+ try {
+ Long userId = jwtUtil.getUserIdFromToken(accessToken);
+ String email = jwtUtil.getEmailFromToken(accessToken);
+ String nickname = jwtUtil.getNicknameFromToken(accessToken);
+
+ if (userId != null && email != null && nickname != null) {
+ user = User.builder()
+ .id(userId)
+ .email(email)
+ .nickname(nickname)
+ .role("USER")
+ .build();
+
+ // ์ ํ ํฐ ๋ฐ๊ธ (์ฟ ํค ๋ฐฉ์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ง)
+ if (authHeader == null) {
+ String newAccessToken = jwtUtil.generateAccessToken(userId, email, nickname);
+ rq.setCrossDomainCookie("accessToken", newAccessToken, accessTokenExpiration);
+ log.info("New access token issued for user: {}", userId);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Failed to extract user info from expired token", e);
}
}
+ // user๊ฐ null์ด๋ฉด ์ธ์ฆ ์คํจ
if (user == null) {
+ log.warn("Authentication failed - user is null");
filterChain.doFilter(request, response);
return;
}
- // accessToken์ด ๋ง๋ฃ๋์ผ๋ฉด ์๋ก ๋ฐ๊ธ
- if (isAccessTokenExists && !isAccessTokenValid) {
- String newAccessToken = jwtUtil.generateAccessToken(user.getId(), user.getEmail(), user.getNickname());
- rq.setCrossDomainCookie("accessToken", newAccessToken, accessTokenExpiration);
- }
-
// SecurityContext์ ์ธ์ฆ ์ ๋ณด ์ ์ฅ
- UserDetails userDetails = new SecurityUser(
- user.getId(),
- user.getEmail(),
- user.getNickname(),
- user.isFirstLogin(),
- user.getAuthorities(),
- Map.of() // JWT ์ธ์ฆ์์๋ ๋น attributes
- );
- Authentication authentication = new UsernamePasswordAuthenticationToken(
- userDetails,
- userDetails.getPassword(),
- userDetails.getAuthorities()
- );
- SecurityContextHolder
- .getContext()
- .setAuthentication(authentication);
+ try {
+ UserDetails userDetails = new SecurityUser(
+ user.getId(),
+ user.getEmail(),
+ user.getNickname(),
+ user.isFirstLogin(),
+ user.getAuthorities(),
+ Map.of() // JWT ์ธ์ฆ์์๋ ๋น attributes
+ );
+
+ Authentication authentication = new UsernamePasswordAuthenticationToken(
+ userDetails,
+ userDetails.getPassword(),
+ userDetails.getAuthorities()
+ );
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ log.info("โ
Authentication SUCCESS - User ID: {}, Nickname: {}", user.getId(), user.getNickname());
+ log.debug("===== Authentication Filter End =====");
+ } catch (Exception e) {
+ log.error("Error setting authentication in SecurityContext", e);
+ }
filterChain.doFilter(request, response);
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 4f164228..8b4570f2 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -61,7 +61,7 @@ spring:
ai:
openai:
- api-key: ${GEMINI_API_KEY}
+ api-key: ${OPEN_API_KEY}
chat:
base-url: "https://generativelanguage.googleapis.com/v1beta/openai/"
options:
@@ -82,14 +82,6 @@ server:
enabled: true
force: true
-# ์ถํ ์ญ์ ์์
-gemini:
- api:
- key: ${GEMINI_API_KEY}
- model-name: "gemini-2.5-flash"
- url: "https://generativelanguage.googleapis.com/v1beta/models"
-
-
custom:
dev:
cookieDomain: localhost
diff --git a/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java b/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java
index 463ba129..8c1a8854 100644
--- a/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java
+++ b/src/test/java/com/back/domain/mybar/controller/MyBarControllerTest.java
@@ -1,5 +1,7 @@
package com.back.domain.mybar.controller;
+import com.back.domain.cocktail.enums.AlcoholStrength;
+import com.back.domain.mybar.dto.MyBarIdResponseDto;
import com.back.domain.mybar.dto.MyBarItemResponseDto;
import com.back.domain.mybar.dto.MyBarListResponseDto;
import com.back.domain.mybar.service.MyBarService;
@@ -95,34 +97,24 @@ private RequestPostProcessor withPrincipal(SecurityUser principal) {
}
@Test
- @DisplayName("Get my bar list - first page")
- void getMyBarList_withoutCursor() throws Exception {
- SecurityUser principal = createPrincipal(1L);
- LocalDateTime keptAt = LocalDateTime.of(2025, 1, 1, 10, 0);
- LocalDateTime createdAt = keptAt.minusDays(1);
-
- MyBarItemResponseDto item = MyBarItemResponseDto.builder()
- .id(3L)
- .cocktailId(10L)
- .cocktailName("Margarita")
- .imageUrl("https://example.com/margarita.jpg")
- .createdAt(createdAt)
- .keptAt(keptAt)
- .build();
-
- MyBarListResponseDto responseDto = new MyBarListResponseDto(
- List.of(item),
- true,
- keptAt.minusMinutes(5),
- 2L
+ @DisplayName("๊ฒฝ๋ ๋ด ๋ฐ ๋ชฉ๋ก์ ์กฐํํ๋ค")
+ void getMyBarIds() throws Exception {
+ SecurityUser principal = createPrincipal(5L);
+
+ List response = List.of(
+ MyBarIdResponseDto.builder()
+ .id(123L)
+ .cocktailId(1L)
+ .keptAt(LocalDateTime.of(2025, 10, 10, 12, 0))
+ .build(),
+ MyBarIdResponseDto.builder()
+ .id(124L)
+ .cocktailId(5L)
+ .keptAt(LocalDateTime.of(2025, 10, 9, 15, 30))
+ .build()
);
- given(myBarService.getMyBar(
- eq(principal.getId()),
- isNull(LocalDateTime.class),
- isNull(Long.class),
- eq(20)
- )).willReturn(responseDto);
+ given(myBarService.getMyBarIds(principal.getId())).willReturn(response);
mockMvc.perform(get("/me/bar")
.with(withPrincipal(principal))
@@ -130,79 +122,69 @@ void getMyBarList_withoutCursor() throws Exception {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("success"))
- .andExpect(jsonPath("$.data.items[0].id").value(3L))
- .andExpect(jsonPath("$.data.items[0].cocktailId").value(10L))
- .andExpect(jsonPath("$.data.items[0].cocktailName").value("Margarita"))
- .andExpect(jsonPath("$.data.items[0].imageUrl").value("https://example.com/margarita.jpg"))
- .andExpect(jsonPath("$.data.items[0].createdAt").value(ISO_WITH_SECONDS.format(createdAt)))
- .andExpect(jsonPath("$.data.items[0].keptAt").value(ISO_WITH_SECONDS.format(keptAt)))
- .andExpect(jsonPath("$.data.hasNext").value(true))
- .andExpect(jsonPath("$.data.nextKeptAt").value(ISO_WITH_SECONDS.format(keptAt.minusMinutes(5))))
- .andExpect(jsonPath("$.data.nextId").value(2L));
+ .andExpect(jsonPath("$.data[0].id").value(123L))
+ .andExpect(jsonPath("$.data[0].cocktailId").value(1L))
+ .andExpect(jsonPath("$.data[0].keptAt").value("2025-10-10T12:00:00"))
+ .andExpect(jsonPath("$.data[1].cocktailId").value(5L));
- verify(myBarService).getMyBar(
- eq(principal.getId()),
- isNull(LocalDateTime.class),
- isNull(Long.class),
- eq(20)
- );
+ verify(myBarService).getMyBarIds(principal.getId());
}
@Test
- @DisplayName("Get my bar list - next page")
- void getMyBarList_withCursor() throws Exception {
- SecurityUser principal = createPrincipal(7L);
- LocalDateTime cursorKeptAt = LocalDateTime.of(2025, 2, 10, 9, 30, 15);
- LocalDateTime itemKeptAt = cursorKeptAt.minusMinutes(1);
- LocalDateTime itemCreatedAt = itemKeptAt.minusDays(2);
+ @DisplayName("์์ธ ๋ด ๋ฐ ๋ชฉ๋ก์ ์กฐํํ๋ค")
+ void getMyBarDetail() throws Exception {
+ SecurityUser principal = createPrincipal(9L);
+ LocalDateTime keptAt = LocalDateTime.of(2025, 10, 1, 10, 0);
+ LocalDateTime createdAt = keptAt.minusDays(1);
MyBarItemResponseDto item = MyBarItemResponseDto.builder()
- .id(20L)
- .cocktailId(33L)
- .cocktailName("Negroni")
- .imageUrl("https://example.com/negroni.jpg")
- .createdAt(itemCreatedAt)
- .keptAt(itemKeptAt)
+ .id(123L)
+ .cocktailId(1L)
+ .cocktailName("Mojito")
+ .cocktailNameKo("๋ชจํ๋")
+ .alcoholStrength(AlcoholStrength.MEDIUM)
+ .imageUrl("https://example.com/mojito.jpg")
+ .createdAt(createdAt)
+ .keptAt(keptAt)
.build();
- MyBarListResponseDto responseDto = new MyBarListResponseDto(
+ MyBarListResponseDto response = new MyBarListResponseDto(
List.of(item),
false,
null,
null
);
- given(myBarService.getMyBar(
+ given(myBarService.getMyBarDetail(
eq(principal.getId()),
- eq(cursorKeptAt),
- eq(99L),
- eq(5)
- )).willReturn(responseDto);
+ isNull(LocalDateTime.class),
+ isNull(Long.class),
+ eq(50)
+ )).willReturn(response);
- mockMvc.perform(get("/me/bar")
+ mockMvc.perform(get("/me/bar/detail")
.with(withPrincipal(principal))
- .param("lastKeptAt", cursorKeptAt.toString())
- .param("lastId", "99")
- .param("limit", "5")
+ .param("limit", "50")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("success"))
- .andExpect(jsonPath("$.data.items[0].id").value(20L))
- .andExpect(jsonPath("$.data.items[0].cocktailName").value("Negroni"))
+ .andExpect(jsonPath("$.data.items[0].cocktailName").value("Mojito"))
+ .andExpect(jsonPath("$.data.items[0].cocktailNameKo").value("๋ชจํ๋"))
+ .andExpect(jsonPath("$.data.items[0].keptAt").value(ISO_WITH_SECONDS.format(keptAt)))
.andExpect(jsonPath("$.data.hasNext").value(false))
.andExpect(jsonPath("$.data.nextKeptAt").doesNotExist());
- verify(myBarService).getMyBar(
+ verify(myBarService).getMyBarDetail(
eq(principal.getId()),
- eq(cursorKeptAt),
- eq(99L),
- eq(5)
+ isNull(LocalDateTime.class),
+ isNull(Long.class),
+ eq(50)
);
}
@Test
- @DisplayName("Keep cocktail")
+ @DisplayName("ํต ์ถ๊ฐ")
void keepCocktail() throws Exception {
SecurityUser principal = createPrincipal(11L);
Long cocktailId = 42L;
@@ -221,7 +203,7 @@ void keepCocktail() throws Exception {
}
@Test
- @DisplayName("Unkeep cocktail")
+ @DisplayName("ํต ํด์ ")
void unkeepCocktail() throws Exception {
SecurityUser principal = createPrincipal(12L);
Long cocktailId = 77L;
@@ -238,4 +220,4 @@ void unkeepCocktail() throws Exception {
verify(myBarService).unkeep(principal.getId(), cocktailId);
}
-}
\ No newline at end of file
+}
diff --git a/terraform/main.tf b/terraform/main.tf
index 2e43504c..93093b37 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -113,17 +113,35 @@ resource "aws_route_table_association" "association_4" {
resource "aws_security_group" "sg_1" {
name = "${var.prefix}-sg-1"
+ # HTTP ํ์ฉ
ingress {
- from_port = 0
- to_port = 0
- protocol = "all"
+ from_port = 80
+ to_port = 80
+ protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
+ # HTTPS ํ์ฉ
+ ingress {
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ # Nginx Proxy Manager ๊ด๋ฆฌ์ ํ์ด์ง
+ ingress {
+ from_port = 81
+ to_port = 81
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ # Egress - ์ธ๋ถ๋ก ๋๊ฐ๋ ํธ๋ํฝ์ ํ์ฉ (ํจํค์ง ๋ค์ด๋ก๋, API ํธ์ถ ๋ฑ)
egress {
- from_port = 0
- to_port = 0
- protocol = "all"
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}