diff --git a/README.md b/README.md index 4091044d..6ef19f2e 100644 --- a/README.md +++ b/README.md @@ -1 +1,167 @@ -# WEB5_7_7STARBALL_BE \ No newline at end of file +# ๐ŸŒŠ MarineLeisure - ์ˆ˜์ƒ๋ ˆ์ € ํŠนํ™” ํ•ด์–‘ ์ •๋ณด ์„œ๋น„์Šค + +> **7์„ฑ๊ตฌ ํŒ€**์˜ ๋ ˆ์ € ํ™œ๋™์ž๋ฅผ ์œ„ํ•œ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋งž์ถคํ˜• ํ•ด์–‘ ์ •๋ณด ์ œ๊ณต ํ”Œ๋žซํผ + +--- + +## ๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ + +**MarineLeisure**๋Š” ๋‚š์‹œ, ์Šค์ฟ ๋ฒ„๋‹ค์ด๋น™, ํ•ด๋ฃจ์งˆ, ์„œํ•‘ ๋“ฑ ์ˆ˜์ƒ๋ ˆ์ € ํ™œ๋™์„ ์ฆ๊ธฐ๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•ด ์„ค๊ณ„๋œ ์›น ๊ธฐ๋ฐ˜ ์ •๋ณด ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ๊ธฐ์ƒ ๋ฐ ํ•ด์–‘ ์ƒ๋ฌผ ์ •๋ณด๋ฅผ ํ†ตํ•ด **์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ธ ๋ ˆ์ € ํ™œ๋™**์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + +> ์ˆ˜์˜จ, ์กฐ๋ฅ˜, ํŒŒ๋„, ์‹œ์•ผ ๋“ฑ ๋‹ค์–‘ํ•œ ํ•ด์–‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ ˆ์ € ์ตœ์  ์žฅ์†Œ๋ฅผ ์ถ”์ฒœํ•˜๊ณ , ์œ„ํ—˜ ์ƒ๋ฌผ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + +--- + +## ๐Ÿ‘ฅ ํŒ€์› ์†Œ๊ฐœ + +| ์ด๋ฆ„ | ์—ญํ•  | ๋‹ด๋‹น | ์ด๋ฉ”์ผ | GitHub | ํ•œ๋งˆ๋”” | +|------|------|------|--------|--------|--------| +| ๐Ÿ‘จโ€๐Ÿ’ป ์กฐ๊ฑด์›… | ๋ฐฑ์—”๋“œ ํŒ€์žฅ | ์™ธ๋ถ€ API ์—ฐ๊ณ„ ๋ฐ ์ง€๋„, ์•Œ๋ฆผ ๊ด€๋ จ ์„œ๋น„์Šค| gwj0421@gmail.com | [gunwoong1630](https://github.com/gunwoong1630?tab=repositories) | Hi | +| ๐Ÿ‘จโ€๐Ÿ’ป ๊น€๋ช…์ง„ | ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ | ํ™œ๋™ ๊ด€๋ จ ์„œ๋น„์Šค | audwls239@gmail.com | [audwls239](https://github.com/audwls239?tab=repositories) | ์ž˜ ๋ถ€ํƒ๋“œ๋ ค์š”! | +| ๐Ÿ‘จโ€๐Ÿ’ป ํ—ˆ์žฌ์› | ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ | ์ธ์ฆ ์„œ๋น„์Šค ๊ฐœ๋ฐœ | jaewonheo666@gmail.com | [johnhuh619](https://github.com/johnhuh619) | ์•ˆ๋…•ํ•˜์„ธ์š”~~ | +| ๐Ÿ‘ฉโ€๐Ÿ’ป ์ด์„ ๋นˆ | ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ | ๋ชจ์ž„ ๊ด€๋ จ ์„œ๋น„์Šค | twinkey0201@gmail.com | [twinkey0201](https://github.com/](https://github.com/garusitell)) | ์•ˆ๋…•ํ•˜์‹ญ๋‹ˆ๊นŒ | + +--- + +## ๐Ÿ’ก ๊ธฐํš ์˜๋„ + +- ๊ธฐ์กด ๊ธฐ์ƒยทํ•ด์–‘ ์ •๋ณด๋Š” **ํ•™์ˆ  ๋ชฉ์ **์ด๊ฑฐ๋‚˜ **ํŒŒํŽธํ™”**๋˜์–ด ์žˆ์–ด ์ ‘๊ทผ์ด ์–ด๋ ค์›€ +- ์ˆ˜์ƒ๋ ˆ์ € ํ™œ๋™์€ **ํ™œ๋™๋ณ„ ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ์ƒ์ด** +- **์‚ฌ์šฉ์ž์˜ ์œ„์น˜, ํ™œ๋™ ์œ ํ˜•์— ๋”ฐ๋ผ ๋งž์ถค ์ •๋ณด ์ œ๊ณต**์ด ํ•„์š” + +> MarineLeisure๋Š” ํ™œ๋™๋ณ„ ๋งž์ถคํ˜• ์ง€์ˆ˜, ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ, ์ฆ๊ฒจ์ฐพ๊ธฐ, ๋ชจ์ž„ ๊ฐœ์„ค, ์œ„ํ—˜ ์ƒ๋ฌผ ์•Œ๋ฆผ ๋“ฑ์„ ํ†ตํ•ด ๋ณด๋‹ค **๊ฐœ์ธํ™”๋œ ๋ ˆ์ € ์„œ๋น„์Šค**๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽฏ ์ฃผ์š” ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์„ค๋ช… | +|------|------| +| ๐Ÿ“ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ | ํ˜„์žฌ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๊ฐ€๊นŒ์šด ์ตœ์ ์˜ ๋ ˆ์ € ์žฅ์†Œ ์ถ”์ฒœ | +| ๐ŸŒŠ ํ™œ๋™๋ณ„ ์ •๋ณด ์ œ๊ณต | ๋‚š์‹œ, ํ•ด๋ฃจ์งˆ, ๋‹ค์ด๋น™, ์„œํ•‘์— ๋งž์ถ˜ ๊ธฐ์ƒ ๋ฐ ํ•ด์–‘ ๋ฐ์ดํ„ฐ ์ œ๊ณต | +| ๐ŸŸ ํ•ด์–‘ ์œ„ํ—˜ ์ƒ๋ฌผ ์•Œ๋ฆผ | ๊ตญ๋ฆฝ์ˆ˜์‚ฐ๊ณผํ•™์› ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์œ„ํ—˜ ์ƒ๋ฌผ ์ถœ๋ชฐ ๊ฒฝ๊ณ  | +| โญ ์ฆ๊ฒจ์ฐพ๊ธฐ & ์•Œ๋ฆผ | ์ž์ฃผ ๊ฐ€๋Š” ํฌ์ธํŠธ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ์กฐ๊ฑด ๋งŒ์กฑ ์‹œ ์•Œ๋ฆผ ์ „์†ก | +| ๐Ÿ‘ฅ ๋ชจ์ž„ ์ƒ์„ฑ | ๋ ˆ์ € ํฌ์ธํŠธ ๊ธฐ๋ฐ˜ ๋ชจ์ž„ ๊ฐœ์„ค ๋ฐ ์ฐธ์—ฌ (์„ ์ฐฉ์ˆœ ๋ชจ์ง‘) | + +--- + +## ๐Ÿ› ๏ธ Troubleshooting (๋ฌธ์ œ ํ•ด๊ฒฐ ์‚ฌ๋ก€) + +### ๐Ÿ”ธ 1. ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ง€์  ์ถ”์ฒœ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ +- **๋ฌธ์ œ**: ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ์œ„์น˜๋กœ ์ฃผ๋ณ€ ์ง€์ ์„ ์ถ”์ฒœํ•  ๋•Œ, ๋น„ํšŒ์› ํฌํ•จ ์ „ ์‚ฌ์šฉ์ž ์š”์ฒญ์œผ๋กœ ํŠธ๋ž˜ํ”ฝ ์ง‘์ค‘. DB ์กฐํšŒ๊ฐ€ ๋ณต์žกํ•ด ๋ณ‘๋ชฉ ๋ฐœ์ƒ. +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - ํ–‰์ •๊ตฌ์—ญ ๋‹จ์œ„๋กœ ํ”„๋ฆฌ์…‹ ์ง€์ ์„ ๋ฏธ๋ฆฌ ์ •์˜ ๋ฐ ์บ์‹ฑ + - ์‚ฌ์šฉ์ž๋Š” ์ขŒํ‘œ๋งŒ ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„๋Š” ํ•ด๋‹น ์œ„์น˜์— ๋งž๋Š” ์‚ฌ์ „ ์ •์˜ ์ง€์  ๋ชฉ๋ก์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ +- โœ… **ํšจ๊ณผ**: ์‹ค์‹œ๊ฐ„ DB ์กฐํšŒ ํšŸ์ˆ˜ ๊ฐ์†Œ, ์„ฑ๋Šฅ ์•ˆ์ •์„ฑ ํ™•๋ณด + +### ๐Ÿ”ธ 2. ์นด์นด์˜ค API ์‚ฌ์šฉ ์ค‘ 429 Too Many Requests ๋ฐœ์ƒ +- **๋ฌธ์ œ**: ์—ญ์ง€์˜ค์ฝ”๋”ฉ ์š”์ฒญ์ด ๋งŽ์•„์ ธ ์นด์นด์˜ค API์˜ ํ˜ธ์ถœ ํ•œ๋„๋ฅผ ์ดˆ๊ณผ โ†’ ์š”์ฒญ ์‹คํŒจ์œจ ์ฆ๊ฐ€ +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - Resilience4j ์„œํ‚ท๋ธŒ๋ ˆ์ด์ปค ์ ์šฉ + - ์ผ์ • ์‹คํŒจ์œจ ์ด์ƒ์ผ ๋•Œ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์ฐจ๋‹จ + fallback ์‘๋‹ต ์ฒ˜๋ฆฌ +- โœ… **ํšจ๊ณผ**: API ์•ˆ์ •์„ฑ ํ–ฅ์ƒ, ์‹œ์Šคํ…œ ๋‹ค์šด ์œ„ํ—˜ ๋ฐฉ์ง€ + +### ๐Ÿ”ธ 3. ์™ธ๋ถ€ API ์˜์กด โ†’ ์ž์ฒด ์—ญ์ง€์˜ค์ฝ”๋”ฉ ์‹œ์Šคํ…œ ๊ตฌ์ถ• +- **๋ฌธ์ œ**: Kakao Maps API ์˜์กด์œผ๋กœ 429 ์—๋Ÿฌ ์ง€์† ๋ฐœ์ƒ, ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€ ์‹œ ๋ถˆ์•ˆ์ • +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - ๊ณต๊ณต OpenAPI์—์„œ ์ œ๊ณตํ•˜๋Š” ์‹œ/๋„ ๊ฒฝ๊ณ„ SHP ํŒŒ์ผ ํ™•๋ณด + - GeoTools ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ์ขŒํ‘œ โ†’ ํ–‰์ •๊ตฌ์—ญ ์ž์ฒด ๋ณ€ํ™˜ ์‹œ์Šคํ…œ ๊ตฌํ˜„ +- โœ… **ํšจ๊ณผ**: ์™ธ๋ถ€ API ์˜์กด ์ œ๊ฑฐ, ํŠธ๋ž˜ํ”ฝ ๊ธ‰์ฆ ์‹œ์—๋„ ์•ˆ์ •์  ์‘๋‹ต ๊ฐ€๋Šฅ + +### ๐Ÿ”ธ 4. ์œ„ํ—˜์ƒ๋ฌผ PDF โ†’ JSON ์ž๋™ํ™” ์˜ค๋ฅ˜ +- **๋ฌธ์ œ**: ๊ตญ๋ฆฝ์ˆ˜์‚ฐ๊ณผํ•™์› PDF ์–‘์‹์ด ์œ ๋™์ ์ด๋ผ ๊ธฐ์กด ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ํŒŒ์‹ฑ ๋ฐฉ์‹ ์‹คํŒจ +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - OpenAI๋ฅผ ํ™œ์šฉํ•ด PDF ๋‚ด ํ…์ŠคํŠธ ํŒจํ„ด ๋ถ„์„ โ†’ JSON์œผ๋กœ ์ง์ ‘ ๋ณ€ํ™˜ + - ๋ถˆ๊ทœ์น™ํ•œ ํ‘œ์—์„œ๋„ ๋ฌธ๋งฅ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ณด ์ถ”์ถœ ๊ฐ€๋Šฅ +- โœ… **ํšจ๊ณผ**: ์ž๋™ํ™” ์ •ํ™•๋„ ํ–ฅ์ƒ, ๋น„์šฉ/์†๋„ ์ตœ์ ํ™” (1,000ํ† ํฐ โ‰’ 0.0002๋‹ฌ๋Ÿฌ) + +### ๐Ÿ”ธ 5. Native Query์˜ ๊ณผ๋„ํ•œ ์กฐ์ธ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ +- **๋ฌธ์ œ**: ๋ณต์žกํ•œ LEFT JOIN์œผ๋กœ ์ธํ•ด ๋ถˆํ•„์š”ํ•œ ์ค‘๋ณต ๋กœ์šฐ ๋ฐœ์ƒ, ๊ฒฐ๊ณผ์…‹ ๋น„๋Œ€ โ†’ ์‘๋‹ต ์‹œ๊ฐ„ ์ง€์—ฐ +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - ๋„ค์ดํ‹ฐ๋ธŒ SQL ๋‹จ์ˆœํ™” ๋ฐ ์กฐํšŒ ์ตœ์ ํ™” + - ํ•„์š” ๋ฐ์ดํ„ฐ๋งŒ ๋ถ„๋ฆฌ ์กฐํšŒํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ๊ฐ€๊ณต +- โœ… **ํšจ๊ณผ**: p95 ์‘๋‹ต ์‹œ๊ฐ„ ๊ฐœ์„  (7์ดˆ โ†’ 1.79์ดˆ โ†’ 20ms๊นŒ์ง€) + +### ๐Ÿ”ธ 6. Hikari ์ปค๋„ฅ์…˜ ํ’€ ๊ณผ๋ถ€ํ•˜ ๋ฌธ์ œ +- **๋ฌธ์ œ**: ๊ณ ํŠธ๋ž˜ํ”ฝ ํ™˜๊ฒฝ์—์„œ ์ปค๋„ฅ์…˜ ํ’€ ๋ณ‘๋ชฉ์œผ๋กœ ์ธํ•ด ์ฒ˜๋ฆฌ๋Ÿ‰ ์ œํ•œ +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - `maximumPoolSize`๋ฅผ 10 โ†’ 20 โ†’ 30 โ†’ 50์œผ๋กœ ๋‹จ๊ณ„์  ์กฐ์ • ํ›„ ์ตœ์ ๊ฐ’ ๋„์ถœ + - ์ตœ์ข… ๊ถŒ์žฅ ์„ค์ •: **maximumPoolSize = 30** +- โœ… **ํšจ๊ณผ**: ์ปค๋„ฅ์…˜ ์•ˆ์ •ํ™”, ์‘๋‹ต ์‹œ๊ฐ„ p95 โ‰ˆ 15ms + +### ๐Ÿ”ธ 7. OAuth2 ๋กœ๊ทธ์ธ ๋ณด์•ˆ ์ทจ์•ฝ์  +- **๋ฌธ์ œ**: state/code ํƒˆ์ทจ ๊ณต๊ฒฉ์— ์ทจ์•ฝ ๊ฐ€๋Šฅ์„ฑ +- **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**: + - **PKCE(Proof Key for Code Exchange)** ์ ์šฉ + - code_verifier๋ฅผ ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅ, ํƒˆ์ทจ ์‹œ์—๋„ ์ธ์ฆ ๋ถˆ๊ฐ€ +- โœ… **ํšจ๊ณผ**: OAuth ๋ณด์•ˆ ๊ฐ•ํ™”, ์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ ํ™•๋ณด + +--- + +## ๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ + +| ๊ตฌ๋ถ„ | ๊ธฐ์ˆ  | +|------|------| +| **Frontend** | React, Next.js, TailwindCSS | +| **Backend** | Spring Boot, JWT, REST API | +| **DB** | MySQL, H2 (ํ…Œ์ŠคํŠธ) | +| **Infra** | AWS EC2, Docker, GitHub Actions (CI/CD) | +| **API** | ๋ฐ”๋‹ค๋ˆ„๋ฆฌ OpenAPI, Open-Meteo, Kakao API, ๊ตญ๋ฆฝ์ˆ˜์‚ฐ๊ณผํ•™์› | +| **๊ธฐํƒ€** | Redis , Geo tools, Apache PDFBox (๋ณด๊ณ ์„œ), GitHub | + +--- + +## ๐Ÿงญ ์‚ฌ์šฉ ํ๋ฆ„ ์˜ˆ์‹œ + +### ๐ŸŽฃ ๋‚š์‹œ ์œ ์ € A +- ํ˜„์žฌ ์œ„์น˜ ๊ธฐ์ค€ **๋‚š์‹œ ํฌ์ธํŠธ ์ถ”์ฒœ** +- โ†’ ์ถ”์ฒœ ํฌ์ธํŠธ 3๊ณณ + ํ•ด์–‘ ์ •๋ณด ์ œ๊ณต + +### ๐Ÿคฟ ๋‹ค์ด๋ฒ„ B +- ํŠน์ • ๋ฐ˜๊ฒฝ ๋‚ด **๋‹ค์ด๋น™ ์žฅ์†Œ ์กฐํšŒ** +- โ†’ ์ถ”์ฒœ ํฌ์ธํŠธ 3๊ณณ + ํ•ด์–‘ ์ •๋ณด ์ œ๊ณต + +--- + +## ๐Ÿ”— API ๋ชฉ๋ก + +| ํ•ญ๋ชฉ | ์ถœ์ฒ˜ | ์„ค๋ช… | +|------|------|------| +| ๊ธฐ์ƒ/ํ•ด์–‘ ์ •๋ณด | [Open-Meteo Marine API](https://open-meteo.com/) | ์ˆ˜์˜จ, ์กฐ๋ฅ˜, ํŒŒ๋„, ๋ฐ”๋žŒ ๋“ฑ | +| ํ•ดํŒŒ๋ฆฌ ์†๋ณด | [๊ตญ๋ฆฝ์ˆ˜์‚ฐ๊ณผํ•™์›](https://www.nifs.go.kr/) | ํ•ด์–‘ ์œ„ํ—˜ ์ƒ๋ฌผ ์ •๋ณด | +| ์œ„์น˜ ์„œ๋น„์Šค | Kakao Map API | ์œ„๊ฒฝ๋„ โ†” ํ–‰์ •๊ตฌ์—ญ ๋ณ€ํ™˜ | +| ์ฃผ์†Œ์ •๋ณด | Kakao Geocoding | ์œ„์น˜ ๊ธฐ๋ฐ˜ ์ฃผ์†Œ ์ถ”์ • | +| ๋กœ๊ทธ์ธ/OAuth | Kakao Oauth 2.0 | ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์—ฐ๋™ | + +--- + +## ๐Ÿ“ธ ๊ฐœ๋ฐœ ๊ฒฐ๊ณผ๋ฌผ + +- โœ… Postman ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (๋ชจ๋“  API ์ •์ƒ ์‘๋‹ต) +- โœ… ERD ๋ฐ ์Šคํ‚ค๋งˆ ์„ค๊ณ„ ์™„๋ฃŒ +- โœ… GitHub Actions ํ†ตํ•œ Docker ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ ์ž๋™ํ™” +- โœ… ์ด๋ฉ”์ผ ์•Œ๋ฆผ ์—ฐ๋™ ์™„๋ฃŒ +- โœ… ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋ ˆ์ € ํฌ์ธํŠธ ์ถ”์ฒœ ๊ธฐ๋Šฅ ์ •์ƒ ์ž‘๋™ + +--- + +## ๐Ÿ“… ์ผ์ • (WBS ์š”์•ฝ) + +| ์ฃผ์ฐจ | ์ฃผ์š” ๋‚ด์šฉ | +|------|----------| +| 1์ฃผ์ฐจ | ๊ธฐํš/์š”๊ตฌ์‚ฌํ•ญ ๋„์ถœ, ์™ธ๋ถ€ API ๋ถ„์„, ๊ณตํ†ต ์ฝ”๋“œ ์ž‘์„ฑ | +| 2์ฃผ์ฐจ | ๋ฐฑ์—”๋“œ API ๊ฐœ๋ฐœ ๋ฐ DB ์„ค๊ณ„ | +| 3์ฃผ์ฐจ | ํ”„๋ก ํŠธ-๋ฐฑ ํ†ตํ•ฉ ๋ฐ ๋ฆฌํŒฉํ† ๋ง, ๊ธฐ๋Šฅ ์ถ”๊ฐ€ | +| 4์ฃผ์ฐจ | ๋ฐฐํฌ ๋ฐ ๋ฐœํ‘œ ์ž๋ฃŒ ์ค€๋น„ | + +๐Ÿ“Ž ์ „์ฒด ์ผ์ • ๋ณด๊ธฐ: [WBS Notion ๋งํฌ](https://www.notion.so/WBS-21e15a01205481a59984c28195e49260?pvs=21) + +--- + +## ๐Ÿงช ์„ค์น˜ ๋ฐ ์‹คํ–‰ ๋ฐฉ๋ฒ• + +1. `.env` ํŒŒ์ผ ์ž‘์„ฑ +2. Docker ๊ธฐ๋ฐ˜ ๋ฐฐํฌ +```bash +# ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ +docker-compose up --build diff --git a/build.gradle b/build.gradle index bf983205..8cbd85ff 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,10 @@ dependencies { // circuit breaker dependencies implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0' + + // mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + } dependencyManagement { diff --git a/src/main/java/sevenstar/marineleisure/activity/controller/ActivityController.java b/src/main/java/sevenstar/marineleisure/activity/controller/ActivityController.java index f116c856..700c31c6 100644 --- a/src/main/java/sevenstar/marineleisure/activity/controller/ActivityController.java +++ b/src/main/java/sevenstar/marineleisure/activity/controller/ActivityController.java @@ -2,18 +2,16 @@ import static sevenstar.marineleisure.global.exception.enums.ActivityErrorCode.*; -import java.math.BigDecimal; import java.util.Map; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; -import sevenstar.marineleisure.activity.dto.reponse.ActivityDetailResponse; import sevenstar.marineleisure.activity.dto.reponse.ActivitySummaryResponse; import sevenstar.marineleisure.activity.dto.reponse.ActivityWeatherResponse; import sevenstar.marineleisure.activity.dto.request.ActivityDetailRequest; @@ -22,6 +20,8 @@ import sevenstar.marineleisure.activity.service.ActivityService; import sevenstar.marineleisure.global.domain.BaseResponse; import sevenstar.marineleisure.global.enums.ActivityCategory; +import sevenstar.marineleisure.spot.dto.detail.SpotDetailReadResponse; +import sevenstar.marineleisure.spot.service.SpotService; @RestController @RequiredArgsConstructor @@ -29,6 +29,7 @@ public class ActivityController { private final ActivityService activityService; + private final SpotService spotService; @GetMapping("/index") public ResponseEntity>> getActivityIndex(@ModelAttribute ActivityIndexRequest activityIndexRequest) { @@ -39,13 +40,20 @@ public ResponseEntity>> getAct )); } + // @GetMapping("/{activity}/detail") + // public ResponseEntity> getActivityDetail(@PathVariable ActivityCategory activity, @ModelAttribute ActivityDetailRequest activityDetailRequest) { + // try { + // return BaseResponse.success(activityService.getActivityDetail(activity, new BigDecimal(activityDetailRequest.latitude()), new BigDecimal(activityDetailRequest.longitude()))); + // } catch (RuntimeException e) { + // return BaseResponse.error(INVALID_ACTIVITY); + // } + // } + @GetMapping("/{activity}/detail") - public ResponseEntity> getActivityDetail(@PathVariable ActivityCategory activity, @ModelAttribute ActivityDetailRequest activityDetailRequest) { - try { - return BaseResponse.success(activityService.getActivityDetail(activity, new BigDecimal(activityDetailRequest.latitude()), new BigDecimal(activityDetailRequest.longitude()))); - } catch (RuntimeException e) { - return BaseResponse.error(INVALID_ACTIVITY); - } + ResponseEntity> getSpotDetail(@PathVariable ActivityCategory activity, @ModelAttribute ActivityDetailRequest activityDetailRequest) { + Long id = spotService.nearSpotId(activityDetailRequest.latitude(), activityDetailRequest.longitude(), + activity); + return BaseResponse.success(spotService.searchSpotDetail(id)); } @GetMapping("/weather") diff --git a/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivitySummaryResponse.java b/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivitySummaryResponse.java index bd741e59..ceaaf863 100644 --- a/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivitySummaryResponse.java +++ b/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivitySummaryResponse.java @@ -6,6 +6,7 @@ @Builder public record ActivitySummaryResponse( String spotName, - TotalIndex totalIndex + TotalIndex totalIndex, + Long spotId ) { } diff --git a/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivityWeatherResponse.java b/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivityWeatherResponse.java index 8b598ec9..0429d478 100644 --- a/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivityWeatherResponse.java +++ b/src/main/java/sevenstar/marineleisure/activity/dto/reponse/ActivityWeatherResponse.java @@ -4,6 +4,7 @@ public record ActivityWeatherResponse( String location, String windSpeed, String waveHeight, - String waterTemp + String waterTemp, + Long spotId ) { } diff --git a/src/main/java/sevenstar/marineleisure/activity/service/ActivityService.java b/src/main/java/sevenstar/marineleisure/activity/service/ActivityService.java index bb03ad14..97bdc229 100644 --- a/src/main/java/sevenstar/marineleisure/activity/service/ActivityService.java +++ b/src/main/java/sevenstar/marineleisure/activity/service/ActivityService.java @@ -61,12 +61,16 @@ private Map getLocalActivitySummary(BigDecimal SpotPreviewReadResponse preview = spotService.preview(latitude.floatValue(), longitude.floatValue()); responses.put("Fishing", - new ActivitySummaryResponse(preview.fishing().getName(), preview.fishing().getTotalIndex())); + new ActivitySummaryResponse(preview.fishing().getName(), preview.fishing().getTotalIndex(),preview.fishing() + .getSpotId())); responses.put("Mudflat", - new ActivitySummaryResponse(preview.mudflat().getName(), preview.mudflat().getTotalIndex())); + new ActivitySummaryResponse(preview.mudflat().getName(), preview.mudflat().getTotalIndex(),preview.mudflat() + .getSpotId())); responses.put("Surfing", - new ActivitySummaryResponse(preview.surfing().getName(), preview.surfing().getTotalIndex())); - responses.put("Scuba", new ActivitySummaryResponse(preview.scuba().getName(), preview.scuba().getTotalIndex())); + new ActivitySummaryResponse(preview.surfing().getName(), preview.surfing().getTotalIndex(),preview.surfing() + .getSpotId())); + responses.put("Scuba", new ActivitySummaryResponse(preview.scuba().getName(), preview.scuba().getTotalIndex(),preview.scuba() + .getSpotId())); // Fishing fishingBySpot = null; // Mudflat mudflatBySpot = null; @@ -151,25 +155,25 @@ private Map getGlobalActivitySummary() { if (fishingResult.isPresent()) { Fishing fishing = fishingResult.get(); OutdoorSpot spot = outdoorSpotRepository.findById(fishing.getSpotId()).get(); - responses.put("Fishing", new ActivitySummaryResponse(spot.getName(), fishing.getTotalIndex())); + responses.put("Fishing", new ActivitySummaryResponse(spot.getName(), fishing.getTotalIndex(),fishing.getSpotId())); } if (mudflatResult.isPresent()) { Mudflat mudflat = mudflatResult.get(); OutdoorSpot spot = outdoorSpotRepository.findById(mudflat.getSpotId()).get(); - responses.put("Mudflat", new ActivitySummaryResponse(spot.getName(), mudflat.getTotalIndex())); + responses.put("Mudflat", new ActivitySummaryResponse(spot.getName(), mudflat.getTotalIndex(),mudflat.getSpotId())); } if (scubaResult.isPresent()) { Scuba scuba = scubaResult.get(); OutdoorSpot spot = outdoorSpotRepository.findById(scuba.getSpotId()).get(); - responses.put("Scuba", new ActivitySummaryResponse(spot.getName(), scuba.getTotalIndex())); + responses.put("Scuba", new ActivitySummaryResponse(spot.getName(), scuba.getTotalIndex(),scuba.getSpotId())); } if (surfingResult.isPresent()) { Surfing surfing = surfingResult.get(); OutdoorSpot spot = outdoorSpotRepository.findById(surfing.getSpotId()).get(); - responses.put("Surfing", new ActivitySummaryResponse(spot.getName(), surfing.getTotalIndex())); + responses.put("Surfing", new ActivitySummaryResponse(spot.getName(), surfing.getTotalIndex(),surfing.getSpotId())); } return responses; @@ -234,7 +238,8 @@ public ActivityWeatherResponse getWeatherBySpot(Float latitude, Float longitude) nearSpot.getName(), fishing.getWindSpeedMax().toString(), fishing.getWaveHeightMax().toString(), - fishing.getSeaTempMax().toString() + fishing.getSeaTempMax().toString(), + nearSpot.getId() ); } diff --git a/src/main/java/sevenstar/marineleisure/favorite/repository/FavoriteRepository.java b/src/main/java/sevenstar/marineleisure/favorite/repository/FavoriteRepository.java index 5b94e480..356b7f37 100644 --- a/src/main/java/sevenstar/marineleisure/favorite/repository/FavoriteRepository.java +++ b/src/main/java/sevenstar/marineleisure/favorite/repository/FavoriteRepository.java @@ -36,4 +36,12 @@ List findFavoritesByMemberIdAndCursorId( Pageable pageable ); boolean existsByMemberIdAndSpotId(Long memberId, Long spotId); + + @Query(value = """ + SELECT m.email + FROM FavoriteSpot fs + JOIN Member m ON fs.memberId = m.id + WHERE fs.spotId = :spotId + """) + List findEmailByFavoriteBestSpot(Long spotId); } diff --git a/src/main/java/sevenstar/marineleisure/global/api/scheduler/SchedulerService.java b/src/main/java/sevenstar/marineleisure/global/api/scheduler/SchedulerService.java index 7ab75df6..5f55ea83 100644 --- a/src/main/java/sevenstar/marineleisure/global/api/scheduler/SchedulerService.java +++ b/src/main/java/sevenstar/marineleisure/global/api/scheduler/SchedulerService.java @@ -12,6 +12,7 @@ import sevenstar.marineleisure.global.api.kakao.service.PresetSchedulerService; import sevenstar.marineleisure.global.api.khoa.service.KhoaApiService; import sevenstar.marineleisure.global.api.openmeteo.dto.service.OpenMeteoService; +import sevenstar.marineleisure.global.mail.MailService; import sevenstar.marineleisure.spot.repository.SpotViewQuartileRepository; @Service @@ -24,7 +25,7 @@ public class SchedulerService { private final PresetSchedulerService presetSchedulerService; private final SpotViewQuartileRepository spotViewQuartileRepository; - + private final MailService mailService; private final Executor taskExecutor; /** @@ -55,6 +56,12 @@ public void scheduler() { // ๋ชจ๋“  ๋ณ‘๋ ฌ ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ CompletableFuture.allOf(openMeteoFuture, presetSchedulerFuture, spotViewQuartileFuture).join(); + try { + mailService.sendMailToHaveFavoriteBestSpot(today); + } catch (Exception e) { + log.error("Error sending mail to users with favorite best spots", e); + } + log.info("=== update data ==="); } } diff --git a/src/main/java/sevenstar/marineleisure/global/enums/ActivityCategory.java b/src/main/java/sevenstar/marineleisure/global/enums/ActivityCategory.java index 24431f12..d369f8f5 100644 --- a/src/main/java/sevenstar/marineleisure/global/enums/ActivityCategory.java +++ b/src/main/java/sevenstar/marineleisure/global/enums/ActivityCategory.java @@ -1,13 +1,21 @@ package sevenstar.marineleisure.global.enums; +import lombok.Getter; import sevenstar.marineleisure.global.exception.CustomException; import sevenstar.marineleisure.global.exception.enums.CommonErrorCode; +@Getter public enum ActivityCategory { - FISHING, - SURFING, - SCUBA, - MUDFLAT; + FISHING("๋‚š์‹œ"), + SURFING("์„œํ•‘"), + SCUBA("์Šค์ฟ ๋ฒ„๋‹ค์ด๋น™"), + MUDFLAT("๊ฐฏ๋ฒŒ์ฒดํ—˜"); + + private String koreanName; + + ActivityCategory(String koreanName) { + this.koreanName = koreanName; + } public static ActivityCategory parse(String category) { try { diff --git a/src/main/java/sevenstar/marineleisure/global/enums/TimePeriod.java b/src/main/java/sevenstar/marineleisure/global/enums/TimePeriod.java index 8bb90127..07e7a797 100644 --- a/src/main/java/sevenstar/marineleisure/global/enums/TimePeriod.java +++ b/src/main/java/sevenstar/marineleisure/global/enums/TimePeriod.java @@ -18,7 +18,8 @@ public static TimePeriod from(String value) { return timePeriod; } } - throw new IllegalArgumentException("Invalid TimePeriod value: " + value); + return TimePeriod.AM; + // throw new IllegalArgumentException("Invalid TimePeriod value: " + value); } } diff --git a/src/main/java/sevenstar/marineleisure/global/mail/MailService.java b/src/main/java/sevenstar/marineleisure/global/mail/MailService.java new file mode 100644 index 00000000..b4d86c99 --- /dev/null +++ b/src/main/java/sevenstar/marineleisure/global/mail/MailService.java @@ -0,0 +1,142 @@ +package sevenstar.marineleisure.global.mail; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import sevenstar.marineleisure.favorite.repository.FavoriteRepository; +import sevenstar.marineleisure.global.enums.ActivityCategory; +import sevenstar.marineleisure.global.enums.TotalIndex; +import sevenstar.marineleisure.spot.dto.EmailContent; +import sevenstar.marineleisure.spot.dto.detail.provider.ActivityProvider; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MailService { + private static final String MESSAGE_SUBJECT = "[MarineLeisure] ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ์ŠคํŒŸ์ด ์ตœ์ƒ์˜ ์ปจ๋””์…˜์ด์—์š”!"; + + private final JavaMailSender javaMailSender; + private final FavoriteRepository favoriteRepository; + private final List providers; + + public void sendMail(String to, String subject, String htmlContent) { + try { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "UTF-8"); + helper.setFrom("your_email@gmail.com"); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(htmlContent, true); // true = HTML + + javaMailSender.send(mimeMessage); + } catch (Exception e) { + log.error("๋ฉ”์ผ ์ „์†ก ์‹คํŒจ", e); + } + } + + + public void sendMailToHaveFavoriteBestSpot(LocalDate date) { + TotalIndex totalIndex = TotalIndex.VERY_GOOD; + List emailContents = new ArrayList<>(); + for (ActivityProvider provider : providers) { + emailContents.addAll(provider.findEmailContent(totalIndex, date)); + } + Map>> result = new HashMap<>(); + for (EmailContent emailContent : emailContents) { + List emails = favoriteRepository.findEmailByFavoriteBestSpot(emailContent.spotId()); + for (String email : emails) { + if (result.containsKey(email)) { + result.get(email).get(emailContent.category()).add(emailContent.spotName()); + } else { + Map> map = new EnumMap<>(ActivityCategory.class); + for (ActivityCategory value : ActivityCategory.values()) { + map.put(value, new HashSet<>()); + } + map.get(emailContent.category()).add(emailContent.spotName()); + result.put(email, map); + } + } + } + for (Map.Entry>> entry : result.entrySet()) { + sendMail(entry.getKey(), MESSAGE_SUBJECT, transformEmailContent(entry.getValue())); + } + } + + // private String transformEmailContent(Map> map) { + // StringBuilder sb = new StringBuilder(); + // sb.append("
"); + // sb.append("

์•ˆ๋…•ํ•˜์„ธ์š”, MarineLeisure์ž…๋‹ˆ๋‹ค ๐ŸŒŠ

"); + // sb.append("

๊ณ ๊ฐ๋‹˜์ด ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ์žฅ์†Œ ์ค‘, ์˜ค๋Š˜ ๊ฐ™์€ ๋‚  ์ตœ์ƒ์˜ ์ปจ๋””์…˜์„ ๋ณด์ด๋Š” ์ŠคํŒŸ๋“ค์„ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

"); + // + // sb.append("
    "); + // for (ActivityCategory category : ActivityCategory.values()) { + // Set spots = map.getOrDefault(category, Set.of()); + // String spotList = spots.isEmpty() ? "์—†์–ด์š” ๐Ÿ˜ข" : String.join(", ", spots); + // sb.append("
  • ") + // .append(category.getKoreanName()) + // .append("์— ์ข‹์€ ์ŠคํŒŸ: ") + // .append(spotList) + // .append("
  • "); + // } + // sb.append("
"); + // + // sb.append("

๐Ÿ‘‰ MarineLeisure ์•ฑ์—์„œ ์ž์„ธํžˆ ๋ณด๊ธฐ

"); + // sb.append("

์•ˆ์ „ํ•˜๊ณ  ์ฆ๊ฑฐ์šด ํ•˜๋ฃจ ๋ณด๋‚ด์„ธ์š” ๐Ÿ˜Š
MarineLeisure ๋“œ๋ฆผ

"); + // sb.append("
"); + // + // return sb.toString(); + // } + + private String transformEmailContent(Map> map) { + StringBuilder sb = new StringBuilder(); + + sb.append("
") + .append("
") + + .append("

๐ŸŒŠ MarineLeisure ์ถ”์ฒœ ์ŠคํŒŸ ์•Œ๋ฆผ

") + .append("

") + .append("๊ณ ๊ฐ๋‹˜์ด ์ฆ๊ฒจ์ฐพ๊ธฐํ•œ ํ•ด์–‘ ํ™œ๋™ ์ŠคํŒŸ ์ค‘, ์˜ค๋Š˜ ๊ฐ™์€ ๋‚  ์ตœ๊ณ ์˜ ์ปจ๋””์…˜์„ ๋ณด์ด๋Š” ์žฅ์†Œ๋ฅผ ์ถ”์ฒœ๋“œ๋ฆด๊ฒŒ์š”!") + .append("

"); + + for (ActivityCategory category : ActivityCategory.values()) { + Set spots = map.getOrDefault(category, Set.of()); + if (!spots.isEmpty()) { + sb.append("
") + .append("

") + .append("โœ”๏ธ ").append(category.getKoreanName()).append(" ์ถ”์ฒœ ์ŠคํŒŸ") + .append("

") + .append("
    "); + for (String spot : spots) { + sb.append("
  • ").append(spot).append("
  • "); + } + sb.append("
"); + } + } + + sb.append("") + + .append("

") + .append("์•ˆ์ „ํ•˜๊ณ  ์ฆ๊ฑฐ์šด ํ•˜๋ฃจ ๋ณด๋‚ด์„ธ์š”!
MarineLeisure ๋“œ๋ฆผ") + .append("

") + + .append("
"); + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/EmailContent.java b/src/main/java/sevenstar/marineleisure/spot/dto/EmailContent.java new file mode 100644 index 00000000..c1d49c98 --- /dev/null +++ b/src/main/java/sevenstar/marineleisure/spot/dto/EmailContent.java @@ -0,0 +1,10 @@ +package sevenstar.marineleisure.spot.dto; + +import sevenstar.marineleisure.global.enums.ActivityCategory; + +public record EmailContent( + Long spotId, + String spotName, + ActivityCategory category +) { +} diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/items/SurfingSpotDetail.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/items/SurfingSpotDetail.java index f248da1b..09f54ad5 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/items/SurfingSpotDetail.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/items/SurfingSpotDetail.java @@ -11,24 +11,30 @@ public class SurfingSpotDetail implements ActivitySpotDetail { private final LocalDate forecastDate; private final TimePeriod timePeriod; - private final float waveHeight; + // private final float waveHeight; private final int wavePeriod; - private final float windSpeed; - private final float seaTemp; + // private final float windSpeed; + // private final float seaTemp; private final TotalIndex totalIndex; private final int uvIndex; + private RangeDetail waveHeight; + private RangeDetail seaTemp; + private RangeDetail windSpeed; + public SurfingSpotDetail(LocalDate forecastDate, TimePeriod timePeriod, float waveHeight, int wavePeriod, float windSpeed, float seaTemp, TotalIndex totalIndex, int uvIndex) { this.forecastDate = forecastDate; this.timePeriod = timePeriod; - this.waveHeight = waveHeight; + // this.waveHeight = waveHeight; + this.waveHeight = new RangeDetail(waveHeight, waveHeight + 0.1f); this.wavePeriod = wavePeriod; - this.windSpeed = windSpeed; - this.seaTemp = seaTemp; + // this.windSpeed = windSpeed; + this.windSpeed = new RangeDetail(windSpeed, windSpeed + 0.1f); + // this.seaTemp = seaTemp; + this.seaTemp = new RangeDetail(seaTemp, seaTemp + 0.1f); this.totalIndex = totalIndex; this.uvIndex = uvIndex; } - } \ No newline at end of file diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ActivityProvider.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ActivityProvider.java index b06205e8..0792e01f 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ActivityProvider.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ActivityProvider.java @@ -18,8 +18,10 @@ import sevenstar.marineleisure.global.api.openmeteo.dto.item.UvIndexItem; import sevenstar.marineleisure.global.enums.ActivityCategory; import sevenstar.marineleisure.global.enums.FishingType; +import sevenstar.marineleisure.global.enums.TotalIndex; import sevenstar.marineleisure.global.utils.GeoUtils; import sevenstar.marineleisure.spot.domain.OutdoorSpot; +import sevenstar.marineleisure.spot.dto.EmailContent; import sevenstar.marineleisure.spot.repository.ActivityRepository; import sevenstar.marineleisure.spot.repository.OutdoorSpotRepository; @@ -43,6 +45,8 @@ public abstract class ActivityProvider { public abstract void update(LocalDate startDate, LocalDate endDate); + public abstract List findEmailContent(TotalIndex totalIndex, LocalDate forecastDate); + @Transactional protected OutdoorSpot createOutdoorSpot(KhoaItem item, FishingType fishingType) { return outdoorSpotRepository.findByLatitudeAndLongitudeAndCategory(item.getLatitude(), item.getLongitude(), diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/FishingProvider.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/FishingProvider.java index 638328bc..71911dbf 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/FishingProvider.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/FishingProvider.java @@ -23,6 +23,7 @@ import sevenstar.marineleisure.global.enums.TotalIndex; import sevenstar.marineleisure.global.utils.DateUtils; import sevenstar.marineleisure.spot.domain.OutdoorSpot; +import sevenstar.marineleisure.spot.dto.EmailContent; import sevenstar.marineleisure.spot.dto.projection.FishingReadProjection; import sevenstar.marineleisure.spot.mapper.SpotDetailMapper; import sevenstar.marineleisure.spot.repository.ActivityRepository; @@ -94,6 +95,11 @@ public void update(LocalDate startDate, LocalDate endDate) { } } + @Override + public List findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) { + return fishingRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate); + } + private List transform(List fishingForecasts) { List details = new ArrayList<>(); for (FishingReadProjection fishingForecast : fishingForecasts) { diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/MudflatProvider.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/MudflatProvider.java index 6b67783a..8810dd33 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/MudflatProvider.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/MudflatProvider.java @@ -19,6 +19,7 @@ import sevenstar.marineleisure.global.enums.TotalIndex; import sevenstar.marineleisure.global.utils.DateUtils; import sevenstar.marineleisure.spot.domain.OutdoorSpot; +import sevenstar.marineleisure.spot.dto.EmailContent; import sevenstar.marineleisure.spot.mapper.SpotDetailMapper; import sevenstar.marineleisure.spot.repository.ActivityRepository; @@ -73,6 +74,11 @@ public void update(LocalDate startDate, LocalDate endDate) { } } + @Override + public List findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) { + return mudflatRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate); + } + private List transform(List mudflatForecasts) { List details = new ArrayList<>(); for (Mudflat mudflatForecast : mudflatForecasts) { diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ScubaProvider.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ScubaProvider.java index 51464a42..182a973c 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ScubaProvider.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ScubaProvider.java @@ -21,6 +21,7 @@ import sevenstar.marineleisure.global.enums.TotalIndex; import sevenstar.marineleisure.global.utils.DateUtils; import sevenstar.marineleisure.spot.domain.OutdoorSpot; +import sevenstar.marineleisure.spot.dto.EmailContent; import sevenstar.marineleisure.spot.mapper.SpotDetailMapper; import sevenstar.marineleisure.spot.repository.ActivityRepository; @@ -76,6 +77,11 @@ public void update(LocalDate startDate, LocalDate endDate) { } } + @Override + public List findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) { + return scubaRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate); + } + private List transform(List scubaForecasts) { List details = new ArrayList<>(); for (Scuba scubaForecast : scubaForecasts) { diff --git a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/SurfingProvider.java b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/SurfingProvider.java index ff540612..1ea22c90 100644 --- a/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/SurfingProvider.java +++ b/src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/SurfingProvider.java @@ -19,6 +19,7 @@ import sevenstar.marineleisure.global.enums.TotalIndex; import sevenstar.marineleisure.global.utils.DateUtils; import sevenstar.marineleisure.spot.domain.OutdoorSpot; +import sevenstar.marineleisure.spot.dto.EmailContent; import sevenstar.marineleisure.spot.mapper.SpotDetailMapper; import sevenstar.marineleisure.spot.repository.ActivityRepository; @@ -72,6 +73,11 @@ public void update(LocalDate startDate, LocalDate endDate) { } } + @Override + public List findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) { + return surfingRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate); + } + private List transform(List surfingForecasts) { List details = new ArrayList<>(); for (Surfing surfingForecast : surfingForecasts) { diff --git a/src/main/java/sevenstar/marineleisure/spot/repository/ActivityRepository.java b/src/main/java/sevenstar/marineleisure/spot/repository/ActivityRepository.java index 76d1d953..57a18d9c 100644 --- a/src/main/java/sevenstar/marineleisure/spot/repository/ActivityRepository.java +++ b/src/main/java/sevenstar/marineleisure/spot/repository/ActivityRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.repository.query.Param; import sevenstar.marineleisure.global.enums.TotalIndex; +import sevenstar.marineleisure.spot.dto.EmailContent; @NoRepositoryBean public interface ActivityRepository extends JpaRepository { @@ -29,4 +30,13 @@ public interface ActivityRepository extends JpaRepository { """) List findForecasts(@Param("spotId") Long spotId, @Param("date") LocalDate date); + @Query(value = """ + SELECT new sevenstar.marineleisure.spot.dto.EmailContent(o.id,o.name,o.category) + FROM OutdoorSpot o + JOIN #{#entityName} e ON o.id=e.spotId + WHERE e.totalIndex = :totalIndex + AND e.forecastDate = :forecastDate + """) + List findEmailContentByTotalIndexAndForecastDate(TotalIndex totalIndex, LocalDate forecastDate); + } diff --git a/src/main/java/sevenstar/marineleisure/spot/service/SpotService.java b/src/main/java/sevenstar/marineleisure/spot/service/SpotService.java index 3bcb581a..0d70d781 100644 --- a/src/main/java/sevenstar/marineleisure/spot/service/SpotService.java +++ b/src/main/java/sevenstar/marineleisure/spot/service/SpotService.java @@ -14,4 +14,6 @@ public interface SpotService { SpotPreviewReadResponse preview(float latitude, float longitude); void upsertSpotViewStats(Long spotId); + + Long nearSpotId(float latitude, float longitude, ActivityCategory category); } diff --git a/src/main/java/sevenstar/marineleisure/spot/service/SpotServiceImpl.java b/src/main/java/sevenstar/marineleisure/spot/service/SpotServiceImpl.java index 95376b0f..6b773ccc 100644 --- a/src/main/java/sevenstar/marineleisure/spot/service/SpotServiceImpl.java +++ b/src/main/java/sevenstar/marineleisure/spot/service/SpotServiceImpl.java @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Pageable; @@ -143,4 +144,12 @@ public void upsertSpotViewStats(Long spotId) { spotViewStatsRepository.upsertViewStats(spotId, LocalDate.now()); } + @Override + public Long nearSpotId(float latitude, float longitude, ActivityCategory category) { + return outdoorSpotRepository.findNearSpot(latitude, longitude, category.name()) + .map(OutdoorSpot::getId) + .orElse(0L); + } + + } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 4c350077..0c48111d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -26,7 +26,7 @@ spring: show_sql: true dialect: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: none + ddl-auto: update defer-datasource-initialization: true ai: @@ -36,10 +36,20 @@ spring: model: gpt-3.5-turbo flyway: - enabled: true + enabled: false baseline-on-migrate: true # locations: classpath:db/migration - + mail: + host: smtp.gmail.com # SMTP ์„œ๋ฒ„ ํ˜ธ์ŠคํŠธ + port: 587 # SMTP ์„œ๋ฒ„ ํฌํŠธ + username: gwj16301 + password: ${EMAIL_PASSWORD} # SMTP ์„œ๋ฒ„ ๋น„๋ฐ€๋ฒˆํ˜ธ + properties: + mail: + smtp: + auth: true # ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ๋„ ์—ฌ๋ถ€ + starttls: + enable: true # starttls ํ™œ์„ฑํ™” ์—ฌ๋ถ€ api: # ๊ตญ๋ฆฝํ•ด์–‘์กฐ์‚ฌ์›(Korea Hydrographic and Oceanographic Agency, KHOA) khoa: