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 ์ธ๋ฑ์Šค + +--- + +## ๐Ÿ“Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ + +image + +--- 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"] }