diff --git a/README.md b/README.md index 91177c5..f64a262 100644 --- a/README.md +++ b/README.md @@ -1 +1,453 @@ -# carrot_server +# ๐Ÿฅ•๋‹น๊ทผ๋งˆ์ผ“๐Ÿฅ• + +![readme_mockup2](https://github.com/MRoKGA/image/blob/main/%EC%A0%9C%EB%AA%A9%EC%9D%84%20%EC%9E%85%EB%A0%A5%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94..png?raw=true) + +- ๋ฐฐํฌ URL : https://d1elknx4d22bup.cloudfront.net (์ ๊ฒ€ ์ค‘) +- ๋ฐฐํฌ ์„œ๋ฒ„ : http://3.35.219.116:8080 (์ ๊ฒ€ ์ค‘) +- ์‹œ์—ฐ ์˜์ƒ : https://youtu.be/Ou8EJn5kFhk + +
+ +## ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ + +- โ€œ์ง€์—ญ ๊ธฐ๋ฐ˜ ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์„ ์‹ค์ œ ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ๊ตฌํ˜„ํ•œ ๋‹น๊ทผ๋งˆ์ผ“ ํด๋ก  ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.โ€ +- โ€œ์‹ค์ œ ์„œ๋น„์Šค ์ˆ˜์ค€์˜ ๋กœ๊ทธ์ธยท์ƒํ’ˆยท์ฑ„ํŒ…ยท์˜ˆ์•ฝยท์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ํ’€์Šคํƒ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.โ€ +- "์„œ๋น„์Šค ์ „์ฒด ํ๋ฆ„์„ ๊ฒฝํ—˜ํ•˜๊ธฐ ์œ„ํ•ด ์ง„ํ–‰ํ•œ ํด๋ก ์ฝ”๋”ฉ์ž…๋‹ˆ๋‹ค.โ€ + +
+ +## ํŒ€์› ๊ตฌ์„ฑ + +
+ +| **์ด๋™๊ตญ** | **ํ™์žฌํ˜ธ** | **์ด์˜์ค€** | +| :------: | :------: | :------: | +| [
@LeeDongGuk](https://github.com/leedongguk) | [
@ariana9rande](https://github.com/ariana9rande) | [
@euijunlee98](https://github.com/euijunlee98) | + +
+ +
+ +## 1. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ + +- Front : HTML, React, styled-components +- Back-end : Spring-Boot +- ๋ฒ„์ „ ๋ฐ ์ด์Šˆ๊ด€๋ฆฌ : Github, Github Issues, Github Project +- ํ˜‘์—… ํˆด : Discord, Notion, Github Wiki +- ์„œ๋น„์Šค ๋ฐฐํฌ ํ™˜๊ฒฝ : AWS +- ์‚ฌ์šฉ ๊ธฐ์ˆ  ๋ฐ API: COOL SMS API, KAKAO MAP, KAKAO PAY, SWAGGER, CI/CD, CHATGPT +- ๋””์ž์ธ : Figma +
+ +## 2. ์ฑ„ํƒํ•œ ๊ฐœ๋ฐœ ๊ธฐ์ˆ ๊ณผ ๋ธŒ๋žœ์น˜ ์ „๋žต + +### React, styled-component + +- React + - ์ปดํฌ๋„ŒํŠธํ™”๋ฅผ ํ†ตํ•ด ์ถ”ํ›„ ์œ ์ง€๋ณด์ˆ˜์™€ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค. + - ์œ ์ € ๋ฐฐ๋„ˆ, ์ƒ๋‹จ๊ณผ ํ•˜๋‹จ ๋ฐฐ๋„ˆ ๋“ฑ ์ค‘๋ณต๋˜์–ด ์‚ฌ์šฉ๋˜๋Š” ๋ถ€๋ถ„์ด ๋งŽ์•„ ์ปดํฌ๋„ŒํŠธํ™”๋ฅผ ํ†ตํ•ด ๋ฆฌ์†Œ์Šค ์ ˆ์•ฝ์ด ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค. + +- AWS ๊ธฐ๋ฐ˜ ๋ฐฐํฌ(EC2, S3, RDS) + - EC2๋ฅผ ํ™œ์šฉํ•ด React ํ”„๋ก ํŠธ์—”๋“œ์™€ Spring Boot ๋ฐฑ์—”๋“œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์šด์˜ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ ๋ฐฐํฌํ–ˆ์Šต๋‹ˆ๋‹ค. + - S3๋ฅผ ์ด์šฉํ•ด ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฐ ์ •์  ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ์ €์žฅ ๋น„์šฉ๊ณผ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค. + - RDS(MySQL)๋ฅผ ๋„์ž…ํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ์•ˆ์ •์ ์ธ ์ €์žฅ ๋ฐ ๋ฐฑ์—…/๊ด€๋ฆฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. + - CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•ด GitHub Actions ๊ธฐ๋ฐ˜ ์ž๋™ ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•จ์œผ๋กœ์จ ์ฝ”๋“œ๋ฅผ pushํ•˜๋Š” ์ฆ‰์‹œ ์ž๋™ ๋นŒ๋“œยท๋ฐฐํฌ๊ฐ€ ์ด๋ฃจ์–ด์ง€๋Š” ์ง€์†์  ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. + + - COOL SMS API (๋ฌธ์ž ๋ณธ์ธ์ธ์ฆ) + - ํšŒ์›๊ฐ€์ž… ๊ณผ์ •์—์„œ ๋ฌธ์ž ์ธ์ฆ ๋ฒˆํ˜ธ ๋ฐœ์†ก ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ์‚ฌ์šฉ์ž ์‹ ๋ขฐ์„ฑ๊ณผ ๋ณด์•ˆ ๋ ˆ๋ฒจ์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. + - ์ธ์ฆ ์ ˆ์ฐจ๋ฅผ REST API์™€ ์—ฐ๋™ํ•˜์—ฌ, ์œ ์ € ๊ฒฝํ—˜(UX) ์†์ƒ ์—†์ด ๋น ๋ฅด๊ฒŒ ์ธ์ฆ ์ ˆ์ฐจ๋ฅผ ์™„๋ฃŒํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. + + - Kakao Map API + - ์‚ฌ์šฉ์ž ์œ„์น˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒํ’ˆ์„ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋„๋ก Kakao Map์„ ์ด์šฉํ•ด ์ง€๋„ ๊ธฐ๋ฐ˜ UI ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + - ์ฃผ์†Œ ๊ฒ€์ƒ‰, ์ขŒํ‘œ ๋ณ€ํ™˜, ๋งˆ์ปค ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋“ฑ ์ง€๋„ ์„œ๋น„์Šค์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์—ฐ๋™ํ•ด ๋™๋„ค ๊ธฐ๋ฐ˜ ํ”Œ๋žซํผ์œผ๋กœ์„œ์˜ ํŠน์„ฑ์„ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. + + - Swagger๋ฅผ ํ™œ์šฉํ•œ API ๋ฌธ์„œํ™” + - Swagger UI๋ฅผ ํ†ตํ•ด ๋ฐฑ์—”๋“œ API ์ „์ฒด๋ฅผ ๋ฌธ์„œํ™”ํ•˜์—ฌ, ํŒ€์› ๊ฐ„์˜ API ์†Œํ†ต ๋น„์šฉ์„ ํฌ๊ฒŒ ์ค„์ด๊ณ  ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. + - API ์ŠคํŽ™ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ด ์œ ์ง€๋ณด์ˆ˜ ๊ณผ์ •์—์„œ๋„ ๋†’์€ ์ƒ์‚ฐ์„ฑ์„ ํ™•๋ณดํ–ˆ์Šต๋‹ˆ๋‹ค. + + - ChatGPT๋ฅผ ํ™œ์šฉํ•œ ์—๋Ÿฌ ๋กœ๊ทธ ์ž๋™ ๋ถ„์„ + - ํ”„๋ก ํŠธยท๋ฐฑ์—”๋“œ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜ ๋กœ๊ทธ๋ฅผ ChatGPT API๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ž๋™์œผ๋กœ ์—๋Ÿฌ ์›์ธ์„ ๋ถ„์„ํ•˜๊ณ  ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ œ์•ˆ๋ฐ›๋Š” ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. + - ์ด๋ฅผ ํ†ตํ•ด ๋””๋ฒ„๊น… ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ๋‹จ์ถ•์‹œํ‚ค๊ณ , ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ๊ณผ ์•ˆ์ •์„ฑ์„ ํ–ฅ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. + +
+ +## ๋…ธ์…˜๊ด€๋ฆฌ + +![readme_mockup2](https://github.com/MRoKGA/image/blob/main/screencapture-notion-so-245269b368be803a9bbbecb07a83a157-2025-11-27-18_08_22.png?raw=true) + +
+ +## 3. ์—ญํ•  ๋ถ„๋‹ด + +### ๐ŸŠ์ด๋™๊ตญ + +- **FULL-STACK** + +
+ +### ๐Ÿ‘ปํ™์žฌํ˜ธ + +- **BACK-END** + +
+ +### ๐Ÿ˜Ž์ด์˜์ค€ + +- **BACK-END** + +
+ +## 4. ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ ๋ฐ ์ž‘์—… ๊ด€๋ฆฌ + +### ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ + +- ์ „์ฒด ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ : 2025-09-01 ~ 2025-11-27 +- UI ๊ตฌํ˜„ : 2025-09-01 ~ 2025-09-08 +- ๊ธฐ๋Šฅ ๊ตฌํ˜„ : 2025-09-09 ~ 2025-11-05 +- ํ…Œ์ŠคํŠธ : 2025-11-05 ~ 2025-11-27 + +
+ +### ์ž‘์—… ๊ด€๋ฆฌ + +- GitHub ๋ฐ NOTION์„ ํ†ตํ•ด ์ง„ํ–‰ ์ƒํ™ฉ์„ ๊ณต์œ ํ–ˆ์Šต๋‹ˆ๋‹ค. +- ์ฃผ๊ฐ„ํšŒ์˜๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ์ž‘์—… ์ˆœ์„œ์™€ ๋ฐฉํ–ฅ์„ฑ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์„ ๋‚˜๋ˆ„๊ณ  NOTION์— ํšŒ์˜ ๋‚ด์šฉ์„ ๊ธฐ๋กํ–ˆ์Šต๋‹ˆ๋‹ค. + +
+ +## 5. ์‹ ๊ฒฝ ์“ด ๋ถ€๋ถ„ + +- ChatGPT๋ฅผ ํ™œ์šฉํ•œ ์—๋Ÿฌ ๋กœ๊ทธ ์ž๋™ ๋ถ„์„ +![readme_mockup2](https://github.com/MRoKGA/image/blob/main/%EC%97%90%EB%9F%AC%EA%B4%80%EB%A6%AC.png?raw=true) + +- SWAGGER๋ฅผ ํ™œ์šฉํ•œ API ๋ฌธ์„œ +![readme_mockup2](https://github.com/MRoKGA/image/blob/main/%EC%8A%A4%EC%9B%A8%EA%B1%B0.png?raw=true) + +
+ +## 6. ํŽ˜์ด์ง€๋ณ„ ๊ธฐ๋Šฅ + +### [์ดˆ๊ธฐํ™”๋ฉด] +- ๊ธฐ๋Šฅ ์„ค๋ช… + - ์„œ๋น„์Šค ์ ‘์† ์‹œ ๊ฐ€์žฅ ๋จผ์ € ๋‚˜ํƒ€๋‚˜๋Š” ํ™”๋ฉด + - ์‹œ์ž‘ํ•˜๊ธฐ(ํšŒ์›๊ฐ€์ž…) + - ๋กœ๊ทธ์ธ(ํ•ธ๋“œํฐ ์ธ์ฆ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ) + +| ์ดˆ๊ธฐํ™”๋ฉด | +|----------| +|![splash](https://github.com/MRoKGA/image/blob/main/1.png?raw=true)| + +
+ +### [ํšŒ์›๊ฐ€์ž…(๋™๋„ค์„ค์ •)] +- ์ž…๋ ฅ์ฐฝ์— ์ง€์—ญ์„ ์ž…๋ ฅํ•˜๋ฉด ์ง€์—ญ์ด ๊ฒ€์ƒ‰๋ฉ๋‹ˆ๋‹ค. +- ํ˜„์žฌ์œ„์น˜๋กœ ์ฐพ๊ธฐ๋กœ ํด๋ฆญ ์‹œ GPS๊ธฐ๋ฐ˜์œผ๋กœ ๋™๋„ค์„ค์ •์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + +| ํšŒ์›๊ฐ€์ž…(๋™๋„ค์„ค์ •) | +|----------| +|![join](https://github.com/MRoKGA/image/blob/main/1-2.png?raw=true)| + +
+ +### [ํšŒ์›๊ฐ€์ž…(ํ•ธ๋“œํฐ๋ฒˆํ˜ธ ์ž…๋ ฅ)] +- ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํ•ธ๋“œํฐ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +- ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ ํ™”๋ฉด์œผ๋กœ ๋„˜์–ด ๊ฐˆ ์ˆ˜ ์—…์Šต๋‹ˆ๋‹ค. + +| ํšŒ์›๊ฐ€์ž…(ํ•ธ๋“œํฐ๋ฒˆํ˜ธ ์ž…๋ ฅ) | +|----------| +|![join](https://github.com/MRoKGA/image/blob/main/1-3.png?raw=true)| + +
+ +### [ํšŒ์›๊ฐ€์ž…(ํ•ธ๋“œํฐ๋ฒˆํ˜ธ ์ธ์ฆ)] +- ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ๋กœ CoolSMS API๋ฅผ ํ†ตํ•ด ์ธ์ฆ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ๋ฐœ์†กํ•˜๊ณ , + ์ด ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ Redis ์„œ๋ฒ„์— ์ž„์‹œ ์ €์žฅ(5๋ถ„ TTL)ํ•œ ๋’ค ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ธ์ฆ๋ฒˆํ˜ธ์™€ Redis์— ์ €์žฅ๋œ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋ณธ์ธ์ธ์ฆ์„ ์™„๋ฃŒํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. +- ๊ธฐ์ˆ  ํ๋ฆ„
+ 1.์‚ฌ์šฉ์ž๊ฐ€ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ์ž…๋ ฅ โ†’ ์ธ์ฆ๋ฒˆํ˜ธ ์š”์ฒญ
+ 2.์„œ๋ฒ„๋Š” ๋žœ๋ค 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ ์ƒ์„ฑ
+ 3.CoolSMS API๋กœ ํ•ด๋‹น ๋ฒˆํ˜ธ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐœ์†ก
+ 4.์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ REDIS์— ์ €์žฅ
+ 5. ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ
+ 6. ์„œ๋ฒ„๋Š” REDIS์— ์ €์žฅ๋œ ๊ฐ’๊ณผ ๋น„๊ต
+ 7. ์ผ์น˜ -> ๋ณธ์ธ์ธ์ฆ ์ž๋™ ์„ฑ๊ณต
+ ๋ถˆ์ผ์น˜/๋งŒ๋ฃŒ -> ์ธ์ฆ ์‹คํŒจ ์ฒ˜๋ฆฌ
+ +| ํšŒ์›๊ฐ€์ž…(ํ•ธ๋“œํฐ๋ฒˆํ˜ธ ์ธ์ฆ) | +|----------| +|![setProfile](https://github.com/MRoKGA/image/blob/main/1-4.png?raw=true)| + +
+ +### [ํ”„๋กœํ•„ ์„ค์ •] +- ์นด๋ฉ”๋ผ ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ์ด๋ฏธ์ง€ ์„ ํƒ +- ์„ ํƒ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์€ ํ”„๋ก ํŠธ์—์„œ FormData๋กœ ๋ฐฑ์—”๋“œ ์ „์†ก +- ๋ฐฑ์—”๋“œ๋Š” ํŒŒ์ผ์„ AWS S3 ๋ฒ„ํ‚ท์— ์—…๋กœ๋“œ +- S3์— ์ €์žฅ๋œ ํŒŒ์ผ์˜ URL์„ DB์— ์ €์žฅํ•˜์—ฌ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋กœ ์‚ฌ์šฉ +- ์ด๋ฏธ์ง€๋ฅผ ๋ฐ”๊พธ๋ฉด ์ž๋™์œผ๋กœ โ€œ์™„๋ฃŒโ€ ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋จ +- ์ž…๋ ฅ์ฐฝ์— ์ƒˆ ๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•˜๋ฉด ์ค‘๋ณตํ™•์ธ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” +- ์ค‘๋ณตํ™•์ธ API ํ˜ธ์ถœ โ†’ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜ +- ์‚ฌ์šฉ ๊ฐ€๋Šฅ: ์ดˆ๋ก์ƒ‰ ๋ฉ”์‹œ์ง€, ์ €์žฅ ๊ฐ€๋Šฅ +- ์ค‘๋ณต๋จ: ๋นจ๊ฐ„์ƒ‰ ๋ฉ”์‹œ์ง€, ์ €์žฅ ๋ถˆ๊ฐ€ +- ์‹ค์ œ ์„œ๋น„์Šค์™€ ๋™์ผํ•˜๊ฒŒ โ€œ๋‹‰๋„ค์ž„ ๊ณ ์œ ์„ฑโ€์„ ํ™•๋ณด + +| ํ”„๋กœํ•„ ์„ค์ • | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/1-5.png?raw=true)| + +
+ +### [ํ™ˆ ํ™”๋ฉด] +- ์ƒ๋‹จ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ +- ์ง€์—ญ(๋™๋„ค) ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ +- ์ƒํ’ˆ ์ธ๋„ค์ผ + ์ •๋ณด ํ‘œ์‹œ +- ํ•˜๋‹จ Floating Button (์ƒํ’ˆ ๋“ฑ๋ก ๋ฒ„ํŠผ) +- ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ + +| ํ™ˆ ํ™”๋ฉด | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/2.png?raw=true)| + +
+ +### [์ƒํ’ˆ ์ƒ์„ธ] +- ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋“œ +- ํŒ๋งค์ž ์ •๋ณด ํ‘œ์‹œ +- ์ข‹์•„์š” ๊ธฐ๋Šฅ +- ์กฐํšŒ์ˆ˜ ๊ธฐ๋Šฅ +- ์ฑ„ํŒ…ํ•˜๊ธฐ ๊ธฐ๋Šฅ +- ์„ ํ˜ธ ๊ฑฐ๋ž˜ ์ง€์—ญ(์ง€๋„ ํ‘œ์‹œ) + +| ์ƒํ’ˆ ์ƒ์„ธ | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/2-1(%EC%98%81%EC%83%81).gif?raw=true)| + +
+ +### [์ƒํ’ˆ ๊ฒ€์ƒ‰] +- ์ƒํ’ˆ ๊ฒ€์ƒ‰ + +| ์ƒํ’ˆ ๊ฒ€์ƒ‰ | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/2-2(%EC%98%81%EC%83%81).gif?raw=true)| + +
+ +### [๋‚ด ๋ฌผ๊ฑด ํŒ”๊ธฐ] +- ์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (0/5) +- ์‹ค์ œ ์ด๋ฏธ์ง€๋Š” ๋ฐฑ์—”๋“œ๋กœ ์ „์†ก๋˜์–ด AWS S3 ๋ฒ„ํ‚ท์— ์ €์žฅ +- ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ +- ๊ฑฐ๋ž˜ ๋ฐฉ์‹ ์„ ํƒ (ํŒ๋งคํ•˜๊ธฐ / ๋‚˜๋ˆ”ํ•˜๊ธฐ) +- ์—ญ์ œ์•ˆ ๋ฐ›๊ธฐ(๊ฐ€๊ฒฉ ์ œ์•ˆ) ์˜ต์…˜ +- ๊ฑฐ๋ž˜ ํฌ๋ง ์žฅ์†Œ ์„ค์ • (Kakao Map API ํŒ์—…) + + +| ๋‚ด ๋ฌผ๊ฑด ํŒ”๊ธฐ | ์œ„์น˜ ์ฐพ๊ธฐ | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/2-3.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/2-4.png?raw=true)| + +
+ +### [๋™๋„ค์ƒํ™œ ํ”ผ๋“œ] +- ๋™๋„ค ๋ชจ์ž„/์ฃผ์ œ ์นดํ…Œ๊ณ ๋ฆฌ ๋…ธ์ถœ +- ์ „์ฒด / ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ +- ๊ฒŒ์‹œ๊ธ€ ๋ฆฌ์ŠคํŠธ +- ์ธ๊ธฐ ๊ฒŒ์‹œ๊ธ€(โ€œ๋ง›์ง‘ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค!โ€ ๋“ฑ) +- ์ข‹์•„์š” ๊ธฐ๋Šฅ +- ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ +- ๊ฒŒ์‹œ๊ธ€ ๋ณธ๋ฌธ + ์ด๋ฏธ์ง€ ํ‘œ์‹œ +- ์ง€๋„ ๊ธฐ๋ฐ˜ ์œ„์น˜ ํ‘œ์‹œ +- ๋Œ“๊ธ€ ๊ธฐ๋Šฅ (CRUD) +- ๋ณธ์ธ ๊ธ€ ์ˆ˜์ •/์‚ญ์ œ ๋ฉ”๋‰ด + + +| ๋™๋„ค์ƒํ™œ ํ”ผ๋“œ | ๋™๋„ค์ƒํ™œ ๊ฒŒ์‹œ๋ฌผ | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/3-1.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/3-2.png?raw=true)| + +
+ +### [๋™๋„ค์ƒํ™œ ๊ธ€์“ฐ๊ธฐ] +- ๊ฒŒ์‹œ๊ธ€ ์ฃผ์ œ ์„ ํƒ Ex)๋™๋„ค์ •๋ณด, ์ด์›ƒ๊ณผํ•จ๊ป˜, ์†Œ์‹ +- ์งˆ๋ฌธ๊ธ€ ์—ฌ๋ถ€ ์ฒดํฌ๋ฐ•์Šค +- ์ฃผ์ œ ํด๋ฆญ ์‹œ ์ฆ‰์‹œ ์„ ํƒ & ํŒ์—… ๋‹ซํž˜ +- ์„ ํƒ๋œ ์ฃผ์ œ๋Š” ๊ธ€์“ฐ๊ธฐ ์ƒ๋‹จ์— ํ‘œ์‹œ + + +| ๋™๋„ค์ƒํ™œ ๊ธ€์“ฐ๊ธฐ | ์นดํ…Œ๊ณ ๋ฆฌ ํŒ์—… | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/3-3.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/3-4.png?raw=true)| + +
+ +### [๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ] +- ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ +- ๋Œ€ํ‘œ ๋™๋„ค ํ‘œ์‹œ +- ๋ชจ์ž„ ์ด๋ฆ„ ์ž…๋ ฅ +- ๊ณต๊ฐœ๋ฒ”์œ„ ์„ค์ •(PUBLIC, PRIVATE) +- ๊ฐ€์ž…์ •์ฑ… ์„ ํƒ(OPEN, APPROVAL, CLOSED) + + +| ๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ | ๋ชจ์ž„ ์ธ๋„ค์ผ | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/3-5.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/3-6.png?raw=true)| + +
+ + + +### [๋™๋„ค๋ชจ์ž„ ์ƒ์„ธ ํ™”๋ฉด] +- ๋ชจ์ž„ ๋Œ€ํ‘œ ์ •๋ณด +- ๊ด€๋ฆฌ๊ธฐ๋Šฅ +- ๋ชจ์ž„ ํƒˆํ‡ด ๊ธฐ๋Šฅ +- ์ด๋ฒคํŠธ ๋ฆฌ์ŠคํŠธ ํ‘œ์‹œ +- ๊ฐ€์ž…์ •์ฑ… ์„ ํƒ(OPEN, APPROVAL, CLOSED) +- ๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ +- ์ง€๋„์—์„œ ์œ„์น˜ ์„ ํƒ ๊ธฐ๋Šฅ + + +| ๋ชจ์ž„ ๋งŒ๋“ค๊ธฐ | ๋ชจ์ž„ ์ธ๋„ค์ผ | ๋ชจ์ž„ ์ธ๋„ค์ผ | +|----------|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/3-7.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/3-8.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/3-9.png?raw=true)| + +
+ +### [๊ฑฐ๋ž˜ ์ฑ„ํŒ…] +- ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€ ์ „์†ก +- ํŒ๋งค์ž ๋งค๋„ˆ์˜จ๋„ ํ‘œ์‹œ +- ์ƒํ’ˆ ์ •๋ณด ๊ณ ์ • ์˜์—ญ +- ์•ฝ์†์žก๊ธฐ ๊ธฐ๋Šฅ(ํ•ต์‹ฌ) +- ์•ฝ์†์žก๊ธฐ ์ž…๋ ฅ ํผ ๊ธฐ๋Šฅ +- ์•ฝ์† ์ œ์•ˆ ๋ฉ”์‹œ์ง€ ์ž๋™ ์ƒ์„ฑ +- ์•ฝ์† ์ทจ์†Œ ๊ธฐ๋Šฅ +- ๋‹น๊ทผํŽ˜์ด(์นด์นด์˜คํŽ˜์ด ์—ฐ๋™) + + +| ์ฑ„ํŒ… | ์•ฝ์†์žก๊ธฐ | ๋ชจ์ž„ ์ธ๋„ค์ผ | +|----------|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-9.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/%EC%95%BD%EC%86%8D%EC%9E%A1%EA%B8%B0.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/5-8.png?raw=true)| + +| ๊ฑฐ๋ž˜1 | ๊ฑฐ๋ž˜2 | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-6.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/5-7.png?raw=true)|![login] + +| ๊ฑฐ๋ž˜3 | ๊ฑฐ๋ž˜4 | +|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/%EC%B9%B4%EC%B9%B4%EC%98%A4%ED%8E%98%EC%9D%B4%EA%B2%B0%EC%A0%9C.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/%EA%B2%B0%EC%A0%9C%20%EC%B9%B4%EC%B9%B4%EC%98%A4%ED%86%A1.png?raw=true)|![login] + +| ๊ฑฐ๋ž˜5 | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-10.png?raw=true)| + +
+ +### [๋™๋„ค์ง€๋„] +- ๋™๋„ค์ง€๋„ ์ถ”์ฒœ ์„œ๋น„์Šค +- KAKAO MAP ์‚ฌ์šฉ + +| ๋™๋„ค์ง€๋„ | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/3-11.png?raw=true)| + +
+ +### [ํ”„๋กœํ•„ ํ™”๋ฉด] +- ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ / ๋‹‰๋„ค์ž„ / ๊ณ„์ •์ •๋ณด ํ‘œ์‹œ +- ๋งค๋„ˆ์˜จ๋„ ๊ทธ๋ž˜ํ”„ ์‹œ๊ฐํ™” + +| ํ”„๋กœํ•„ ํ™”๋ฉด | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-1.png?raw=true)| + +
+ +### [๋‚˜์˜ ํŒ๋งค๋‚ด์—ญ] +- ํŒ๋งค์ค‘ / ์˜ˆ์•ฝ์ค‘ / ๊ฑฐ๋ž˜์™„๋ฃŒ ํƒญ ๊ตฌ๋ถ„ +- ์ƒํ’ˆ ์ˆ˜์ • / ์‚ญ์ œ / ๊ฑฐ๋ž˜์™„๋ฃŒ ์ฒ˜๋ฆฌ ๋ฒ„ํŠผ ์ œ๊ณต +- ์ฑ„ํŒ…์ˆ˜ยท๊ด€์‹ฌ์ˆ˜ยท์กฐํšŒ์ˆ˜ ํ‘œ์‹œ + +| ๋‚˜์˜ ํŒ๋งค๋‚ด์—ญ | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-2.png?raw=true)| + +
+ +### [๋‚˜์˜ ๊ตฌ๋งค๋‚ด์—ญ] +- ๋‚ด๊ฐ€ ๊ตฌ๋งคํ•œ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ ํ‘œ์‹œ +- ๋ฆฌ๋ทฐ ์ž‘์„ฑ ๋ฒ„ํŠผ ์ œ๊ณต +- ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ๋ณ„์  + ํ›„๊ธฐ ์ž…๋ ฅ ๊ฐ€๋Šฅ + +| ๋‚˜์˜ ๊ตฌ๋งค๋‚ด์—ญ | ๋‚˜์˜ ๊ตฌ๋งค๋‚ด์—ญ ํ›„๊ธฐ์ž‘์„ฑ | ๋‚˜์˜ ๊ตฌ๋งค๋‚ด์—ญ ํ›„๊ธฐ์™„๋ฃŒ | +|----------|----------|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-3.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/5-3-1.png?raw=true)|![login](https://github.com/MRoKGA/image/blob/main/5-3-2.png?raw=true)| + +
+ +### [๋‚˜์˜ ๊ด€์‹ฌ๋ชฉ๋ก] +- ๊ด€์‹ฌ ๋“ฑ๋กํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ํ‘œ์‹œ +- ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ ๊ฐ€๋Šฅ +- ์‹œ๊ฐ„ ์ •๋ณด(์˜ˆ: 78์ผ ์ „) ํ‘œ์‹œ + +| ๋‚˜์˜ ๊ด€์‹ฌ๋ชฉ๋ก | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-4.png?raw=true)| + +
+ +### [๋‚˜์˜ ์•ฝ์†ํ™”๋ฉด] +- ์บ˜๋ฆฐ๋” ๊ธฐ๋ฐ˜ ์ผ์ • ํ™•์ธ +- ์•ฝ์† ์ƒํƒœ๋ณ„ ํ•„ํ„ฐ(์ „์ฒด / ์ œ์•ˆ๋จ / ์ˆ˜๋ฝ๋จ / ๊ฑฐ์ ˆ๋จ / ์ทจ์†Œ๋จ) +- ์•ฝ์† ์ƒ์„ธ ์ •๋ณด ์ œ๊ณต + +| ๋‚˜์˜ ์•ฝ์†ํ™”๋ฉด | +|----------| +|![login](https://github.com/MRoKGA/image/blob/main/5-5.png?raw=true)| + +
+ +## ์–ด๋ ค์› ๋˜์  + +### ๐ŸŠ์ด๋™๊ตญ + +### ๋ฌธ์ œ์ƒํ™ฉ +- Chrome, Android์—์„œ๋Š” ์œ„์น˜ ๊ถŒํ•œ ์š”์ฒญ์ด ์ž˜ ๋™์ž‘ํ•จ +- ํ•˜์ง€๋งŒ iPhone Safari์—์„œ๋Š” ํ˜„์žฌ์œ„์น˜๋กœ ์ฐพ๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ๋„ ์•„๋ฌด ๋ฐ˜์‘ ์—†์Œ +- ์ฝ˜์†”์—” "GPS ์ ‘๊ทผ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์ฃผ์†Œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."๋งŒ ์ถœ๋ ฅ๋จ +- Kakao API์—์„œ 401 Unauthorized ์˜ค๋ฅ˜ ๋ฐœ์ƒ + +### ์›์ธ + - iOS Safari๋Š” HTTPS ํ™˜๊ฒฝ์ด ์•„๋‹Œ ๊ฒฝ์šฐ navigator.geolocation ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. + + +### ํ•ด๊ฒฐ๋ฐฉ๋ฒ• + - LocalTunnel ์‚ฌ์šฉํ•˜๊ธฐ + +### ๐ŸŠํ™์žฌํ˜ธ +- +### ๐ŸŠ์ด์˜์ค€ + +### ๋ฌธ์ œ์ƒํ™ฉ + 1. GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๋Š” ์ •์ƒ์ ์œผ๋กœ ์„ฑ๊ณต, ํ•˜์ง€๋งŒ ๋ฐฐํฌ ์™„๋ฃŒ ํ›„ ํผ๋ธ”๋ฆญ IP:8080 ์ ‘์† ๋ถˆ๊ฐ€ + 2. ๋ฐฐํฌ ์™„๋ฃŒ ํ›„ ์„œ๋ฒ„์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •์ƒ ์‹คํ–‰๋˜์ง€ ์•Š์Œ + 3. SMS ๋“ฑ ์™ธ๋ถ€ API ์—ฐ๋™ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๋˜๋Š” ํ˜ธ์ถœ ์‹œ ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ๋ฐœ์ƒ + +### ์›์ธ + 1. AWS ๋ณด์•ˆ ๊ทธ๋ฃน์— 8080 ํฌํŠธ ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™ ๋ˆ„๋ฝ + 2. ๋กœ์ปฌ ํ™˜๊ฒฝ ๊ธฐ์ค€ ์„ค์ •(application.yml)ํŒŒ์ผ์ด ๊ทธ๋Œ€๋กœ ๋ฐฐํฌ๋˜์–ด ์„œ๋ฒ„ ํ™˜๊ฒฝ๊ณผ ๋ถˆ์ผ์น˜ + 3. ๋ฏผ๊ฐ ์ •๋ณด(API Key, Secret)๊ฐ€ ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์„ค์ •๋˜์ง€ ์•Š์Œ. ๋กœ์ปฌ ์ธํ…”๋ฆฌ์ œ์ด ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •์—๋งŒ ์ž…๋ ฅ ํ•ด๋†“์Œ. + +### ํ•ด๊ฒฐ๋ฐฉ๋ฒ• + 1. AWS ๋ณด์•ˆ ๊ทธ๋ฃน์— 8080 ํฌํŠธ ์˜คํ”ˆ + 2. ๋ฐฐํฌ ์„œ๋ฒ„ ํ™˜๊ฒฝ์— ๋งž์ถ˜ application.yml ์„ค์ • ๋ฐ ํ™˜๊ฒฝ(๋กœ์ปฌ,๋ฐฐํฌ)๋ณ„ ํ”„๋กœํŒŒ์ผ ์ ์šฉ + 3. ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ๋ก ๋ฐ GitHub Actions์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฃผ์ž… ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌ + +## ์†Œ๊ฐ + +### ๐ŸŠ์ด๋™๊ตญ +- ใ…‡ใ…‡ใ…‡ + +### ๐ŸŠ์ด์˜์ค€ +- CI/CD ํ™˜๊ฒฝ์„ ์ง์ ‘ ๊ตฌ์ถ•ํ•˜๋ฉฐ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ์„ ๋งค์šฐ ํฌ๊ฒŒ ์ฒด๊ฐํ–ˆ๋‹ค. ์ดˆ๋ฐ˜๋ถ€ํ„ฐ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ํŒ€์›๋ผ๋ฆฌ ๊ณต์œ ํ•˜๋ฉฐ ๊ด€๋ฆฌํ•ด์•ผํ•œ๋‹ค๋Š” ๋ง์„ ๋จธ๋ฆฌ๊ฐ€ ์•„๋‹Œ ํ”ผ๋ถ€๋กœ ๋А๋ผ๊ฒŒ ๋˜์—ˆ๋‹ค. ๋ฐฐํฌ ๊ณผ์ •์—์„œ๋Š” ์‚ฌ์†Œํ•œ ์„ค์ • ํ•˜๋‚˜๊ฐ€ ์„œ๋น„์Šค ์ „์ฒด์— ์˜ํ–ฅ์„ ์ค˜ ๋‚˜์ค‘์—” ์–ด๋””์„œ๋ถ€ํ„ฐ ๊ณ ์ณ์•ผํ• ์ง€ ๊ฐ๋„ ์•ˆ ์˜ค๊ณ  ๋ง‰๋ง‰ํ–ˆ๋‹ค. ์•ž์œผ๋กœ ๊ผผ๊ผผํ•˜๊ฒŒ ํ•˜๋‚˜ํ•˜๋‚˜ ์„ค์ •ํ•˜๊ณ  ์ ๊ฒ€ํ•˜๋ฉฐ ๋„˜์–ด๊ฐ€์ž. +- ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ตฌํ˜„ + ์†Œํ”„ํŠธ ์‚ญ์ œ ๋กœ์ง ์„ค๊ณ„๋ฅผ ํ†ตํ•ด ์ผ์ƒ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋˜ ์ฑ„ํŒ… ์„œ๋น„์Šค์˜ ์ •๊ตํ•จ์„ ์ฒด๊ฐํ–ˆ๋‹ค. ์‚ญ์ œ ์‹œ์  ๊ธฐ์ค€ ๋ฐ์ดํ„ฐ ์กฐํšŒ, ์ฝ์Œ ์ฒ˜๋ฆฌ ๋“ฑ ์ƒํƒœ ๊ด€๋ฆฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ์„ค๊ณ„์™€ ๋ฐฑ์—”๋“œ ๋กœ์ง์˜ ๋””ํ…Œ์ผํ•จ์„ ๊ฒฝํ—˜ํ–ˆ๋‹ค. ๋ฟŒ๋“ฏํ–ˆ๋‹ค. + +### ๐ŸŠํ™์žฌํ˜ธ +- ใ…‡ใ…‡ใ…‡ diff --git a/src/main/java/com/mrokga/carrot_server/.DS_Store b/src/main/java/com/mrokga/carrot_server/.DS_Store index 8725dc6..fe5e08c 100644 Binary files a/src/main/java/com/mrokga/carrot_server/.DS_Store and b/src/main/java/com/mrokga/carrot_server/.DS_Store differ diff --git a/src/main/java/com/mrokga/carrot_server/auth/controller/AuthController.java b/src/main/java/com/mrokga/carrot_server/auth/controller/AuthController.java index 44096f5..33cbfda 100644 --- a/src/main/java/com/mrokga/carrot_server/auth/controller/AuthController.java +++ b/src/main/java/com/mrokga/carrot_server/auth/controller/AuthController.java @@ -34,6 +34,11 @@ public class AuthController { private final AuthService authService; private final UserService userService; + /** + * ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ SMS๋ฅผ ๋ฐœ์†กํ•˜๋Š” api + * @param phoneNumber ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์„ ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ + * @return ์„ฑ๊ณต ์‘๋‹ต DTO + */ @PostMapping("/send") @Operation(summary = "์ธ์ฆ๋ฒˆํ˜ธ sms ๋ฐœ์†ก", description = "์‚ฌ์šฉ์ž ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ sms ๋ฐœ์†ก") @ApiResponses(value = { @@ -45,6 +50,12 @@ public ResponseEntity> sendSms(@Parameter(description = " return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success")); } + /** + * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” api + * Redis์— ์ €์žฅ๋œ ์ธ์ฆ๋ฒˆํ˜ธ์™€ ๋น„๊ตํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ return + * @param request ํœด๋Œ€ํฐ๋ฒˆํ˜ธ์™€ ์ธ์ฆ๋ฒˆํ˜ธ + * @return ์ธ์ฆ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ์‘๋‹ต (200 OK, 400 BAD_REQUEST, 410 GONE) + */ @PostMapping("/verify") @Operation(summary = "์ธ์ฆ๋ฒˆํ˜ธ ์ธ์ฆ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ธ์ฆ๋ฒˆํ˜ธ์™€ redis์— ์ €์žฅ๋œ ๊ฐ’ ๋น„๊ต") @ApiResponses(value = { @@ -75,6 +86,11 @@ public ResponseEntity> verifyCode(@RequestBody VerifyCodeRe }; } + /** + * ๋‹‰๋„ค์ž„ ์ค‘๋ณต ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” api + * @param nickname ๊ฒ€์‚ฌํ•  ๋‹‰๋„ค์ž„ + * @return ์ค‘๋ณต ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ์‘๋‹ต (์ค‘๋ณต ์‹œ 400 BAD_REQUEST, ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์‹œ 200 OK) + */ @PostMapping("/validate-nickname") @Operation(summary = "๋‹‰๋„ค์ž„ ์ค‘๋ณต๊ฒ€์‚ฌ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋‹‰๋„ค์ž„์ด ์ค‘๋ณต๋˜์—ˆ๋Š”์ง€ ๊ฒ€์‚ฌ") @ApiResponses(value = { @@ -100,6 +116,11 @@ public ResponseEntity> validateNickname(@Parameter(descri } + /** + * ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ํšŒ์›๊ฐ€์ž…์„ ์ฒ˜๋ฆฌํ•˜๋Š” api + * @param request ํšŒ์›๊ฐ€์ž… ์ •๋ณด + * @return ์ƒ์„ฑ๋œ user entity ํฌํ•จ๋œ ์‘๋‹ต DTO + */ @PostMapping("/signup") @Operation(summary = "ํšŒ์›๊ฐ€์ž… ์š”์ฒญ", description = "ํšŒ์›๊ฐ€์ž… ์š”์ฒญ") @ApiResponses(value = { @@ -115,6 +136,11 @@ public ResponseEntity> signup(@RequestBody SignupRequestDto return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", user)); } + /** + * ์ธ์ฆ๋ฒˆํ˜ธ SMS๋ฅผ ์žฌ๋ฐœ์†กํ•˜๋Š” api + * @param phoneNumber ์žฌ๋ฐœ์†ก์„ ์š”์ฒญํ•  ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ + * @return ์„ฑ๊ณต ์‘๋‹ต DTO + */ @PostMapping("/resend") @Operation(summary = "์ธ์ฆ๋ฒˆํ˜ธ sms ์žฌ๋ฐœ์†ก", description = "์‚ฌ์šฉ์ž ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ sms ์žฌ๋ฐœ์†ก") @ApiResponses(value = { @@ -126,6 +152,12 @@ public ResponseEntity> resendSms(@Parameter(description = " return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success")); } + /** + * ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ api + * ์ธ์ฆ ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  jwt๋ฅผ ๋ฐœ๊ธ‰ํ•œ ๋’ค return + * @param request ์ „ํ™”๋ฒˆํ˜ธ ๋ฐ ์ž…๋ ฅ๋œ ์ธ์ฆ๋ฒˆํ˜ธ + * @return ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ํ† ํฐ๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์‘๋‹ต DTO, ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์‘๋‹ต + */ @PostMapping("/login") @Operation(summary = "๋กœ๊ทธ์ธ ์š”์ฒญ", description = "๋กœ๊ทธ์ธ ์š”์ฒญ") @ApiResponses(value = { diff --git a/src/main/java/com/mrokga/carrot_server/auth/service/AuthService.java b/src/main/java/com/mrokga/carrot_server/auth/service/AuthService.java index ab1f05b..52496cc 100644 --- a/src/main/java/com/mrokga/carrot_server/auth/service/AuthService.java +++ b/src/main/java/com/mrokga/carrot_server/auth/service/AuthService.java @@ -35,9 +35,14 @@ public class AuthService { private static final String ACCESS_TOKEN_PREFIX = "access_token:"; private static final String REFRESH_TOKEN_PREFIX = "refresh_token:"; + // SMS ๋ฐœ์‹  ๋ฒˆํ˜ธ @Value("${sms.sender}") private String sender; + /** + * ์ง€์ •๋œ ๋ฐœ์‹ ๋ฒˆํ˜ธ๋กœ ์ธ์ฆ๋ฒˆํ˜ธ SMS๋ฅผ ์ „์†กํ•˜๊ณ , ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ Redis์— ์ €์žฅ + * @param phoneNumber ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์„ ์ „ํ™”๋ฒˆํ˜ธ + */ public void sendSms(String phoneNumber) { String code = generateCode(); @@ -53,24 +58,35 @@ public void sendSms(String phoneNumber) { try { messageService.send(message); } catch (NurigoMessageNotReceivedException e) { + redisTemplate.delete(key); log.info("failed message list = {}", e.getFailedMessageList()); log.info("exception = {}", e.getMessage()); } catch (Exception e) { + redisTemplate.delete(key); log.info("exception = {}", e.getMessage()); } } + /** + * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ธ์ฆ๋ฒˆํ˜ธ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + * @param phoneNumber ์ „ํ™”๋ฒˆํ˜ธ (Redis Key ์กฐํšŒ์šฉ) + * @param code ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ธ์ฆ๋ฒˆํ˜ธ + * @return ์ธ์ฆ ๊ฒฐ๊ณผ {@link VerifyCodeResult} + */ public VerifyCodeResult verifyCode(String phoneNumber, String code) { log.info("[AuthService] verifyCode starts"); String key = SMS_PREFIX + phoneNumber; + // 1. Redis์—์„œ ํ•ด๋‹น ํœด๋Œ€ํฐ๋ฒˆํ˜ธ๋กœ ์ €์žฅ๋œ ์ธ์ฆ๋ฒˆํ˜ธ ์กฐํšŒ String saved = redisTemplate.opsForValue().get(key); log.info("saved = {}", saved); + // 2. ์ €์žฅ๋œ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๋งŒ๋ฃŒ๋กœ ํŒ๋‹จ) if (saved == null) { return VerifyCodeResult.EXPIRED; } + // 3. Redis์—์„œ ์กฐํšŒํ•œ ์ธ์ฆ๋ฒˆํ˜ธ์™€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ if(!saved.equals(code)) { return VerifyCodeResult.MISMATCH; } @@ -91,6 +107,10 @@ public static String generateCode() { return String.format("%06d", number); } + /** + * ๊ธฐ์กด ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์ƒˆ๋กœ์šด ์ธ์ฆ๋ฒˆํ˜ธ SMS๋ฅผ ์žฌ์ „์†ก + * @param phoneNumber ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์„ ์ „ํ™”๋ฒˆํ˜ธ + */ public void resendSms(String phoneNumber) { redisTemplate.delete(SMS_PREFIX + phoneNumber); @@ -98,6 +118,11 @@ public void resendSms(String phoneNumber) { sendSms(phoneNumber); } + /** + * Access Token๊ณผ Refresh Token์„ ๋ฐœ๊ธ‰ํ•˜๊ณ , Refresh Token์„ Redis์— ์ €์žฅ ํ›„ token์ด ๋‹ด๊ธด DTO๋ฅผ ๋ฐ˜ํ™˜ + * @param user ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์œ ์ € + * @return ๋ฐœ๊ธ‰๋œ ํ† ํฐ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด DTO + */ public TokenResponseDto issueAndReturnTokens(User user) { String accessToken = tokenProvider.generateAccessToken(user); String refreshToken = tokenProvider.generateRefreshToken(user); @@ -111,17 +136,30 @@ public TokenResponseDto issueAndReturnTokens(User user) { .build(); } + /** + * Refresh Token์„ ์‚ฌ์šฉํ•˜์—ฌ Access Token๊ณผ Refresh Token ๊ฐฑ์‹  + * @param user ํ† ํฐ์„ ๊ฐฑ์‹ ํ•  ์œ ์ € + * @param oldRefreshToken ๊ฐฑ์‹  ์š”์ฒญ ์‹œ ์‚ฌ์šฉ๋  ๊ธฐ์กด Refresh Token + * @return ์ƒˆ๋กญ๊ฒŒ ๋ฐœ๊ธ‰๋œ ํ† ํฐ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด DTO + */ public TokenResponseDto renew(User user, String oldRefreshToken) { String key = REFRESH_TOKEN_PREFIX + user.getId(); String storedRefreshToken = redisTemplate.opsForValue().get(key); + // 1. ์ €์žฅ๋œ ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜, ์š”์ฒญ๋œ ํ† ํฐ๊ณผ ์ €์žฅ๋œ ํ† ํฐ์ด ์ผ์น˜ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ํ† ํฐ ์ž์ฒด์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ if(storedRefreshToken == null || !storedRefreshToken.equals(oldRefreshToken) || !tokenProvider.validToken(oldRefreshToken)) { throw new RuntimeException("INVALID REFRESH TOKEN"); } + // 2. ์œ ํšจํ•œ ๊ฒฝ์šฐ, ์ƒˆ๋กœ์šด ํ† ํฐ ๋ฐœ๊ธ‰ ๋ฐ ์ €์žฅ return issueAndReturnTokens(user); } + /** + * Refresh Token์„ Redis์— ์ €์žฅ + * @param user + * @param refreshToken ์ €์žฅํ•  Refresh Token + */ public void saveRefreshToken(User user, String refreshToken) { String key = REFRESH_TOKEN_PREFIX + user.getId(); diff --git a/src/main/java/com/mrokga/carrot_server/chat/controller/ChatMessageController.java b/src/main/java/com/mrokga/carrot_server/chat/controller/ChatMessageController.java index 56bb41e..a01bcc5 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/controller/ChatMessageController.java +++ b/src/main/java/com/mrokga/carrot_server/chat/controller/ChatMessageController.java @@ -51,7 +51,7 @@ public List getMessages(@PathVariable Integer roomId) { /** - * โœ… WebSocket/STOMP ์šฉ Controller + * WebSocket/STOMP ์šฉ Controller * - @RestController ๋Œ€์‹  @Controller ์‚ฌ์šฉ * - /pub/chat/message ๋กœ ๋ฐœํ–‰๋œ STOMP ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹  */ diff --git a/src/main/java/com/mrokga/carrot_server/chat/entity/ChatRoom.java b/src/main/java/com/mrokga/carrot_server/chat/entity/ChatRoom.java index 1e70f53..1322201 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/entity/ChatRoom.java +++ b/src/main/java/com/mrokga/carrot_server/chat/entity/ChatRoom.java @@ -20,14 +20,17 @@ public class ChatRoom { @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; + // ์ƒํ’ˆ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; + // ํŒ๋งค์ž @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "seller_id", nullable = false) private User seller; + // ๊ตฌ๋งค ํฌ๋ง์ž @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "buyer_id", nullable = false) private User buyer; diff --git a/src/main/java/com/mrokga/carrot_server/chat/entity/QuickReply.java b/src/main/java/com/mrokga/carrot_server/chat/entity/QuickReply.java index 4350f18..021d390 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/entity/QuickReply.java +++ b/src/main/java/com/mrokga/carrot_server/chat/entity/QuickReply.java @@ -18,6 +18,7 @@ public class QuickReply { @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; + // ์œ ์ € ์•„์ด๋”” @Column(name = "user_id", nullable = false) private Integer userId; diff --git a/src/main/java/com/mrokga/carrot_server/chat/repository/AppointmentRepository.java b/src/main/java/com/mrokga/carrot_server/chat/repository/AppointmentRepository.java index 77862bb..44c43d0 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/repository/AppointmentRepository.java +++ b/src/main/java/com/mrokga/carrot_server/chat/repository/AppointmentRepository.java @@ -16,7 +16,7 @@ Optional findByChatRoom_IdAndStatusIn(Integer chatRoomId, Optional findByChatRoom_Id(Integer chatRoomId); - // โœ” ๋‚˜์˜ ์•ฝ์†(์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž์ด๊ฑฐ๋‚˜ ์ œ์•ˆ์ž=๋‚˜). ์ƒํƒœ ํ•„ํ„ฐ optional + // ๋‚˜์˜ ์•ฝ์†(์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž์ด๊ฑฐ๋‚˜ ์ œ์•ˆ์ž=๋‚˜). ์ƒํƒœ ํ•„ํ„ฐ optional @Query(""" SELECT a FROM Appointment a diff --git a/src/main/java/com/mrokga/carrot_server/chat/service/AppointmentService.java b/src/main/java/com/mrokga/carrot_server/chat/service/AppointmentService.java index c48a2f0..5f140c4 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/service/AppointmentService.java +++ b/src/main/java/com/mrokga/carrot_server/chat/service/AppointmentService.java @@ -18,6 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +// ์ฑ„ํŒ…๋ฐฉ ๋‚ด์—์„œ ์ด๋ค„์ง€๋Š” ๊ธฐ๋Šฅ +// ์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ ๋ฐ ์—ด๋žŒ์—์„œ ๋ณธ์ธ ์ธ์ฆ์„ ํ†ตํ•ด ์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์—ฌ๋ถ€๋ฅผ ๊ฐ€๋ ค๋†จ๊ธฐ์— +// ์—ฌ๊ธฐ์„œ๋Š” ๋”ฐ๋กœ ์ฑ„ํŒ… ์ฐธ์—ฌ์ž ์—ฌ๋ถ€๋ฅผ ๊ฐ€๋ฆฌ์ง€ ์•Š์Œ. (์ฆ‰ ์•ฝ์† CRUD ๊ถŒํ•œ ์—ฌ๋ถ€ ๊ฐ€๋ฆฌ์ง€ ์•Š์Œ. ์•„๋ฏธ ์กฐํšŒ ๋ฐ ์—ด๋žŒํ–ˆ์œผ๋ฉด ์•ฝ์†์— ๋Œ€ํ•œ ๊ถŒํ•œ๋„ ๋‹น์—ฐํžˆ ๋ถ€์—ฌ) @Service @RequiredArgsConstructor public class AppointmentService { @@ -41,6 +44,7 @@ private AppointmentResponseDto toDto(Appointment appointment) { .build(); } + // ์•ฝ์† ์ƒ์„ฑ @Transactional public AppointmentResponseDto create(Integer roomId, AppointmentRequestDto dto){ ChatRoom room = chatRoomRepository.findById(roomId) @@ -49,7 +53,7 @@ public AppointmentResponseDto create(Integer roomId, AppointmentRequestDto dto){ User proposer = userRepository.findById(dto.getProposerId()) .orElseThrow(() -> new EntityNotFoundException("AppointmentService.create(): ์œ ์ € ์—†์Œ")); - // โœ… ์ค‘๋ณต ์•ฝ์† ๋ฐฉ์ง€: ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์— PENDING/ACCEPTED ์ƒํƒœ ์•ฝ์†์ด ์žˆ์œผ๋ฉด ์ƒ์„ฑ ๋ถˆ๊ฐ€ + // ์ค‘๋ณต ์•ฝ์† ๋ฐฉ์ง€: ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์— PENDING/ACCEPTED ์ƒํƒœ ์•ฝ์†์ด ์žˆ์œผ๋ฉด ์ƒ์„ฑ ๋ถˆ๊ฐ€ appointmentRepository.findByChatRoom_IdAndStatusIn( roomId, java.util.List.of(AppointmentStatus.PENDING, AppointmentStatus.ACCEPTED) ).ifPresent(a -> { @@ -66,7 +70,7 @@ public AppointmentResponseDto create(Integer roomId, AppointmentRequestDto dto){ Appointment saved = appointmentRepository.save(appointment); - // โœ… ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ + // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ํ›„ ์ „์†ก String content = String.format("%s๋‹˜์ด %s %s์— ๋งŒ๋‚˜์ž๊ณ  ์•ฝ์†์„ ์ œ์•ˆํ–ˆ์Šต๋‹ˆ๋‹ค.", proposer.getNickname(), dto.getMeetingTime().toLocalDate(), @@ -76,6 +80,7 @@ public AppointmentResponseDto create(Integer roomId, AppointmentRequestDto dto){ return toDto(saved); } + // ์•ฝ์† ์ˆ˜๋ฝ @Transactional public AppointmentResponseDto acceptAppointment(Integer appointmentId) { Appointment appointment = appointmentRepository.findById(appointmentId) @@ -98,13 +103,14 @@ public AppointmentResponseDto acceptAppointment(Integer appointmentId) { productService.changeStatus(dto); - // โœ… ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ + // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ํ›„ ์ „์†ก String content = "์•ฝ์†์ด ์ˆ˜๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ƒํƒœ๊ฐ€ ์˜ˆ์•ฝ์ค‘์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค."; chatMessageService.sendSystemMessage(room, content); return toDto(appointment); } + // ์•ฝ์† ๊ฑฐ์ ˆ @Transactional public AppointmentResponseDto rejectAppointment(Integer appointmentId) { Appointment appointment = appointmentRepository.findById(appointmentId) @@ -112,13 +118,14 @@ public AppointmentResponseDto rejectAppointment(Integer appointmentId) { appointment.setStatus(AppointmentStatus.REJECTED); - // โœ… ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ + // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ํ›„ ์ „์†ก String content = "์•ฝ์†์ด ๊ฑฐ์ ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; chatMessageService.sendSystemMessage(appointment.getChatRoom(), content); return toDto(appointment); } + // ์•ฝ์† ์ทจ์†Œ @Transactional public AppointmentResponseDto cancelAppointment(Integer appointmentId) { Appointment appointment = appointmentRepository.findById(appointmentId) @@ -129,7 +136,7 @@ public AppointmentResponseDto cancelAppointment(Integer appointmentId) { ChatRoom room = appointment.getChatRoom(); Product product = room.getProduct(); - // โœ… changeStatus ํ˜ธ์ถœ (Transaction๊นŒ์ง€ ์ •๋ฆฌ) + // changeStatus ํ˜ธ์ถœ (Transaction๊นŒ์ง€ ์ •๋ฆฌ) ChangeStatusRequestDto dto = ChangeStatusRequestDto.builder() .productId(product.getId()) .sellerId(room.getSeller().getId()) @@ -140,13 +147,14 @@ public AppointmentResponseDto cancelAppointment(Integer appointmentId) { productService.changeStatus(dto); - // โœ… ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ + // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ํ›„ ์ „์†ก String content = "์•ฝ์†์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ƒํƒœ๊ฐ€ ํŒ๋งค์ค‘์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค."; chatMessageService.sendSystemMessage(room, content); return toDto(appointment); } + // ์•ฝ์† ์กฐํšŒ @Transactional(readOnly = true) public AppointmentResponseDto getAppointmentByChatRoomId(Integer roomId) { Appointment appointment = appointmentRepository.findByChatRoom_Id(roomId) diff --git a/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageReadService.java b/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageReadService.java index 271878d..2fcc937 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageReadService.java +++ b/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageReadService.java @@ -18,9 +18,13 @@ public class ChatMessageReadService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; + // ์ฝ์Œ ์ฒ˜๋ฆฌ @Transactional(propagation = Propagation.REQUIRES_NEW) public void markAsRead(Integer roomId, Integer messageId, Integer userId) { - // ๊ถŒํ•œ/๊ฒ€์ฆ + /* ๊ถŒํ•œ ํ™•์ธ + 1. ๋‹ค๋ฅธ ๋ฐฉ ๋ฉ”์‹œ์ง€ ์ฝ์Œ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€ + 2. ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ์—ฌ๋ถ€ ํ™•์ธ + */ ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> new IllegalArgumentException("์ฑ„ํŒ…๋ฐฉ ์—†์Œ")); diff --git a/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageService.java b/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageService.java index 9587e95..6c2d9cd 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageService.java +++ b/src/main/java/com/mrokga/carrot_server/chat/service/ChatMessageService.java @@ -25,7 +25,7 @@ public class ChatMessageService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; private final ChatMessageReadService chatMessageReadService; - private final SimpMessageSendingOperations messagingTemplate; // โœ… ์‹ค์‹œ๊ฐ„ ์ „์†ก ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + private final SimpMessageSendingOperations messagingTemplate; // ์‹ค์‹œ๊ฐ„ ์ „์†ก ๊ธฐ๋Šฅ ์ถ”๊ฐ€ // ๋ฉ”์„ธ์ง€ ๊ฐ์ฒด DTO ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ ๋ฉ”์†Œ๋“œ private MessageResponseDto toResponse(ChatMessage message){ @@ -37,7 +37,11 @@ private MessageResponseDto toResponse(ChatMessage message){ dto.setMessage(message.getMessage()); dto.setCreatedAt(message.getCreatedAt()); - // ๋ถ€๋ชจ(๋‹ต์žฅ ๋Œ€์ƒ) ์žˆ์œผ๋ฉด ์š”์•ฝ ์ฑ„์šฐ๊ธฐ + /* ๋ถ€๋ชจ(๋‹ต์žฅ ๋Œ€์ƒ) ์žˆ์œผ๋ฉด ์š”์•ฝ ์ฑ„์šฐ๊ธฐ + ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒ๋Œ€๋ฐฉ ๋ฉ”์„ธ์ง€์— ๋Œ€ํ•œ ๋‹ต์žฅ ๋ฉ”์„ธ์ง€์ผ ๊ฒฝ์šฐ, + ์ƒ๋Œ€๋ฐฉ ๋ฉ”์„ธ์ง€(๋ถ€๋ชจ ๋ฉ”์„ธ์ง€) ์š”์•ฝ์นธ์ด ๋‹ต์žฅ ๋ฉ”์‹œ์ง€(์ž์‹ ๋ฉ”์„ธ์ง€) ์œ„์— ์žˆ๋Š” + UI๋ฅผ ์œ„ํ•œ ๋ถ€๋ชจ ๋ฉ”์„ธ์ง€ ์š”์•ฝ ์†์„ฑ. + */ ChatMessage p = message.getParentMessage(); if (p != null) { dto.setReplyToMessageId(p.getId()); @@ -52,19 +56,19 @@ private MessageResponseDto toResponse(ChatMessage message){ return dto; } - // ๋‹ต์žฅ ๋ฉ”์„ธ์ง€, ๋ถ€๋ชจ ๋ฉ”์„ธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ 50์ž + // ๋‹ต์žฅ ๋ฉ”์„ธ์ง€, ๋ถ€๋ชจ ๋ฉ”์„ธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์•ฝ 50์ž private String abbreviate(String s, int max) { if (s == null) return null; return s.length() <= max ? s : s.substring(0, max) + "โ€ฆ"; } - // โœ… ๊ธฐ์กด sendMessage: REST API์šฉ (๋ฉ”์‹œ์ง€ ์ €์žฅ๋งŒ ์ˆ˜ํ–‰) + // ๊ธฐ์กด sendMessage: REST API์šฉ (๋ฉ”์‹œ์ง€ ์ €์žฅ๋งŒ ์ˆ˜ํ–‰) @Transactional public MessageResponseDto sendMessage(MessageRequestDto dto, Integer senderId){ return saveMessage(dto, senderId); } - // โœ… ์ƒˆ๋กœ์šด sendMessageAndBroadcast: WebSocket ์ „์šฉ (์ €์žฅ + ์‹ค์‹œ๊ฐ„ ์ „์†ก) + // ์ƒˆ๋กœ์šด sendMessageAndBroadcast: WebSocket ์ „์šฉ (์ €์žฅ + ์‹ค์‹œ๊ฐ„ ์ „์†ก) @Transactional public void sendMessageAndBroadcast(MessageRequestDto dto, Integer senderId){ MessageResponseDto response = saveMessage(dto, senderId); @@ -109,7 +113,6 @@ public MessageResponseDto saveMessage(MessageRequestDto dto, Integer senderId){ if (!parent.getChatRoom().getId().equals(room.getId())) { throw new IllegalArgumentException("๋‹ค๋ฅธ ์ฑ„ํŒ…๋ฐฉ์˜ ๋ฉ”์‹œ์ง€์—๋Š” ๋‹ต์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - // (์„ ํƒ) parent๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์œผ๋ฉด ๊ธˆ์ง€ํ• ์ง€ ํ—ˆ์šฉํ• ์ง€ ์ •์ฑ… ๊ฒฐ์ • } ChatMessage message = chatMessageRepository.save( @@ -139,21 +142,29 @@ public List getMessages(Integer roomId, Integer requesterId) throw new AccessDeniedException("์ฑ„ํŒ…๋ฐฉ ๋ฉ”์„ธ์ง€ ์กฐํšŒ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); } - // ๋‚ด ์ปคํŠธ๋ผ์ธ ๊ฐ€์ ธ์˜ค๊ธฐ (์—†์œผ๋ฉด 0) + /* ๋‚ด ์‚ญ์ œ ์ง€์  ๊ฐ€์ ธ์˜ค๊ธฐ (์—†์œผ๋ฉด 0) + => ์ฆ‰, ์ด์ „์— ์ฑ„ํŒ…๋ฐฉ์„ ์‚ญ์ œํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์‚ญ์ œ ์ง€์  ์ดํ›„์˜ ๋ฉ”์„ธ์ง€ ๋ถ€ํ„ฐ ๋ณด์—ฌ์คŒ. + ์‚ญ์ œํ•œ ์  ์—†์œผ๋ฉด ์ปคํŠธ๋ผ์ธ์€ 0์œผ๋กœ ์ „์ฒด ๋ฉ”์„ธ์ง€๋ฅผ ๋‹ค ๋ณด์—ฌ์คŒ. + */ Integer cutoff = chatRoomRepository.getDeleteCutoffForUser(roomId, requesterId); int cutoffId = cutoff == null ? 0 : cutoff; - // โœ… ์ปคํŠธ๋ผ์ธ ์ดํ›„ ๋ฉ”์‹œ์ง€๋งŒ ์กฐํšŒ + // ์‚ญ์ œ ์ง€์  ์ดํ›„ ๋ฉ”์‹œ์ง€๋งŒ ์กฐํšŒ List messages = chatMessageRepository.findAfterCutoff(roomId, cutoffId); - // ===== ์—ฌ๊ธฐ์„œ ์ฝ์Œ ์ฒ˜๋ฆฌ ===== + /* ===== ์—ฌ๊ธฐ์„œ ์ฝ์Œ ์ฒ˜๋ฆฌ ===== + ์กฐํšŒํ–ˆ์œผ๋ฉด ๋‹น์—ฐํžˆ ์ฝ์Œ ์ฒ˜๋ฆฌ + */ if (!messages.isEmpty()) { int lastMessageId = messages.get(messages.size() - 1).getId(); - // ์ด๊ฑฐ markAsRead ์„œ๋น„์Šค ํ˜ธ์ถœ + // markAsRead ์„œ๋น„์Šค ํ˜ธ์ถœ chatMessageReadService.markAsRead(room.getId(), lastMessageId, requesterId); } - // ====== ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ '์ฝ์Œ' ํŒ์ • ====== + /* ====== '๋งˆ์ง€๋ง‰' ๋ฉ”์„ธ์ง€ ์ฝ์Œ ํ‘œ์‹œ ====== + UI์— ๋งˆ์ง€๋ง‰ ๋ฉ”์„ธ์ง€ ์ฝ์Œ ํ‘œ์‹œ ๋„์šธ ๋•Œ, ๋‚ด ๋ฉ”์„ธ์ง€๋ฅผ ์ƒ๋Œ€๊ฐ€ ์ฝ์—ˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œ. + ๋‚ด๊ฐ€ ์ƒ๋Œ€ ๋ฉ”์„ธ์ง€๋ฅผ ์ฝ์—ˆ์„ ๋•Œ ๊ตณ์ด ์ฝ์Œ ํ‘œ์‹œ๋ฅผ ๋„์šธ ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ. + */ Integer opponentReadId = Objects.equals(requesterId, buyerId) ? room.getSellerLastReadMessageId() : room.getBuyerLastReadMessageId(); @@ -161,7 +172,7 @@ public List getMessages(Integer roomId, Integer requesterId) Integer lastMsgId = (lastMsg != null) ? lastMsg.getId() : null; boolean lastIsMine = (lastMsg != null) && Objects.equals(lastMsg.getUser().getId(), requesterId); - // ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๊ฑฐ๋ผ๋ฉด, ์ƒ๋Œ€ ํฌ์ธํ„ฐ๊ฐ€ ๊ทธ ID ์ด์ƒ์ธ์ง€๋กœ ํŒ์ • + // ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๊ฑฐ๋ผ๋ฉด, ์ƒ๋Œ€ ๋ฉ”์„ธ์ง€ ์ฝ์Œ ํฌ์ธํ„ฐ๊ฐ€ ๊ทธ ID ์ด์ƒ์ธ์ง€๋กœ ํŒ์ • boolean lastMsgReadByOpponent = lastIsMine && opponentReadId != null && lastMsgId != null && opponentReadId >= lastMsgId; @@ -172,7 +183,7 @@ public List getMessages(Integer roomId, Integer requesterId) return messages.stream().map(m -> { MessageResponseDto dto = toResponse(m); - // ํ”„๋ก ํŠธ ๋ฒ„๋ธ” ์ •๋ ฌ/์ƒ‰ ๊ตฌ๋ถ„์— ์œ ์šฉ + // ํ”„๋ก ํŠธ ์ฑ„ํŒ… ๋ฒ„๋ธ” ์ •๋ ฌ/์ƒ‰ ๊ตฌ๋ถ„์— ์œ ์šฉ boolean mine = Objects.equals(m.getUser().getId(), requesterId); dto.setMine(mine); @@ -185,10 +196,11 @@ public List getMessages(Integer roomId, Integer requesterId) } + //์‹œ์Šคํ…œ ๋ฉ”์„ธ์ง€ ์ „์†ก @Transactional public ChatMessage sendSystemMessage(ChatRoom room, String content) { - // โœ… ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด "System" ์‚ฌ์šฉ + // ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด "System" ์‚ฌ์šฉ User systemUser = userRepository.findByNickname("SYSTEM"); if (systemUser == null) { throw new RuntimeException("์‹œ์Šคํ…œ ๊ณ„์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); diff --git a/src/main/java/com/mrokga/carrot_server/chat/service/ChatRoomService.java b/src/main/java/com/mrokga/carrot_server/chat/service/ChatRoomService.java index 97dffa3..6b3c3d1 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/service/ChatRoomService.java +++ b/src/main/java/com/mrokga/carrot_server/chat/service/ChatRoomService.java @@ -39,6 +39,7 @@ private ChatRoomResponseDto toResponse(ChatRoom room) { return dto; } + // ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ @Transactional public ChatRoomResponseDto createOrGetRoom(Integer me, ChatRoomRequestDto dto){ Product product = productRepository.findById(dto.getProductId()) @@ -48,7 +49,7 @@ public ChatRoomResponseDto createOrGetRoom(Integer me, ChatRoomRequestDto dto){ final User seller; final User buyer; - // ํŒ๋งค์ž ์„ ํ†ก + // 1. ํŒ๋งค์ž ์„ ํ†ก if(Objects.equals(me, ownerId)){ if(dto.getBuyerId() == null){ throw new IllegalArgumentException("๊ตฌ๋งค์ž ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.(ํŒ๋งค์ž ์„ ํ†ก)"); @@ -58,7 +59,7 @@ public ChatRoomResponseDto createOrGetRoom(Integer me, ChatRoomRequestDto dto){ buyer = userRepository.findById(dto.getBuyerId()) .orElseThrow(() -> new IllegalArgumentException("๊ตฌ๋งค์ž ์—†์Œ")); } - // ๊ตฌ๋งค์ž ์„ ํ†ก + // 2. ๊ตฌ๋งค์ž ์„ ํ†ก else{ buyer = userRepository.findById(me) .orElseThrow(() -> new IllegalArgumentException("๊ตฌ๋งค์ž ์—†์Œ")); @@ -101,13 +102,17 @@ public ChatRoomResponseDto createOrGetRoom(Integer me, ChatRoomRequestDto dto){ } } + // ์ฑ„ํŒ…๋ฐฉ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ @Transactional(readOnly = true) public List getRoomByUser(Integer userId){ List rooms = chatRoomRepository.findByBuyer_IdOrSeller_Id(userId, userId); return rooms.stream().map(room -> { ChatRoomResponseDto dto = toResponse(room); // ๊ธฐ์กด ํ•„๋“œ ์„ธํŒ… - // ๋ฐฉ์˜ ๋งˆ์ง€๋ง‰ ๋ณด์ด๋Š” ๋ฉ”์‹œ์ง€ (์ปคํŠธ๋ผ์ธ ์ดํ›„) + /* + ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์˜ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ (์ปคํŠธ๋ผ์ธ ์ดํ›„) + => UI์— ๊ฐ ์ฑ„ํŒ…๋ฐฉ๋งˆ๋‹ค ๋งˆ์ง€๋ง‰ ๋ฉ”์„ธ์ง€ ์š”์•ฝ ํ›„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด + */ chatRoomRepository.findLastVisibleMessageId(room.getId(), userId) .ifPresent(lastId -> { chatMessageRepository.findById(lastId).ifPresent(last -> { @@ -116,13 +121,17 @@ public List getRoomByUser(Integer userId){ dto.setLastMessageAt(last.getCreatedAt()); dto.setLastMessagePreview( last.getMessageType() == MessageType.TEXT - ? abbreviate(last.getMessage(), 50) + ? abbreviate(last.getMessage()) : "[์ด๋ฏธ์ง€]" ); }); }); - // ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ id (์ปคํŠธ๋ผ์ธ ์ดํ›„ ๊ธฐ์ค€) + /* + ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ id (๋‚ด ์ปคํŠธ๋ผ์ธ ์ดํ›„ ๊ธฐ์ค€) + ์ฑ„ํŒ…๋ฐฉ์„ ์‚ญ์ œํ•˜์ง€ ์•Š์•˜์œผ๋ฉด ์ปคํŠธ๋ผ์ธ 0, ์‚ญ์ œํ•œ ์  ์žˆ๋‹ค๋ฉด ์‚ญ์ œ ์ง€์  ๋ฉ”์„ธ์ง€ ๋ฐ˜ํ™˜ + (๋‚ด๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ณด๋‚ด๊ณ  ๋‚ด๊ฐ€ ์ฑ„ํŒ…๋ฐฉ์„ ์‚ญ์ œํ•œ ๊ฒฝ์šฐ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ ๋Œ€๋น„ํ•ด์„œ) + */ Integer cutoff = chatRoomRepository.getDeleteCutoffForUser(room.getId(), userId); int cutoffId = cutoff == null ? 0 : cutoff; Integer lastMyMessageId = chatMessageRepository.findAfterCutoff(room.getId(), cutoffId).stream() @@ -136,6 +145,7 @@ public List getRoomByUser(Integer userId){ ? room.getSellerLastReadMessageId() : room.getBuyerLastReadMessageId(); + // ๋งˆ์ง€๋ง‰ ๋ฉ”์„ธ์ง€ ์ฝ์Œ ์—ฌ๋ถ€ ํ‘œ์‹œ boolean seen = (lastMyMessageId != null) && (opponentRead != null) && (opponentRead >= lastMyMessageId); dto.setLastMessageSeen(seen); @@ -145,23 +155,30 @@ public List getRoomByUser(Integer userId){ /** * ์ฑ„ํŒ…๋ฐฉ ์†Œํ”„ํŠธ ์‚ญ์ œ (๋‚ด ์ชฝ์—์„œ๋งŒ ์‚ญ์ œ) - * - DB์—์„œ ์‹ค์ œ๋กœ ์ง€์šฐ์ง€ ์•Š๊ณ  ๋‚ด ์ปคํŠธ๋ผ์ธ์„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ id๋กœ ์„ค์ • + * - DB์—์„œ ์‹ค์ œ๋กœ ์ง€์šฐ์ง€ ์•Š๊ณ  ๋‚ด๊ฐ€ ์‚ญ์ œํ•œ ์ง€์ (์ปคํŠธ๋ผ์ธ)์„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ id๋กœ ์„ค์ • * - ์ดํ›„ ์ด ๋ฐฉ์€ ๋‚ด ๋ชฉ๋ก์—์„œ ์•ˆ ๋ณด์ด๊ณ , ๋‚˜์ค‘์— ์ƒˆ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ค๋ฉด ๋‹ค์‹œ ๋“ฑ์žฅ */ @Transactional public void deleteRoom(Integer roomId, Integer userId) { + //์ฑ„ํŒ…๋ฐฉ ์กฐํšŒ ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> new IllegalArgumentException("์ฑ„ํŒ…๋ฐฉ ์—†์Œ")); Integer buyerId = chatRoom.getBuyer().getId(); Integer sellerId = chatRoom.getSeller().getId(); + // ๊ถŒํ•œ ํ™•์ธ if (!Objects.equals(userId, buyerId) && !Objects.equals(userId, sellerId)) { throw new AccessDeniedException("์ฑ„ํŒ…๋ฐฉ ์‚ญ์ œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); } + // ๋งˆ์ง€๋ง‰ ๋ฉ”์„ธ์ง€ ์กฐํšŒ int lastMsgId = chatMessageRepository.findTopIdByRoomId(roomId).orElse(0); + /* ํ•ด๋‹น ์ง€์ ๊นŒ์ง€๋Š” ์ฝ์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์„ค์ • ํ›„, ํ•ด๋‹น ์ง€์ ์„ ์‚ญ์ œ ์ง€์ ์œผ๋กœ ์„ค์ • + 1. ๋‚ด๊ฐ€ buyer์ผ ๊ฒฝ์šฐ + 2. ๋‚ด๊ฐ€ seller์ผ ๊ฒฝ์šฐ + */ if (Objects.equals(userId, buyerId)) { chatRoomRepository.markBuyerDeleted(roomId, buyerId, lastMsgId); chatRoomRepository.bumpBuyerLastRead(roomId, lastMsgId); @@ -171,8 +188,9 @@ public void deleteRoom(Integer roomId, Integer userId) { } } - private String abbreviate(String s, int max) { + // ๋ฉ”์„ธ์ง€ ์š”์•ฝ (50์ž ์ด์ƒ์ผ ๊ฒฝ์šฐ ์ดํ›„ ๋‚ด์šฉ ...์œผ๋กœ) + private String abbreviate(String s) { if (s == null) return null; - return s.length() <= max ? s : s.substring(0, max) + "โ€ฆ"; + return s.length() <= 50 ? s : s.substring(0, 50) + "โ€ฆ"; } } diff --git a/src/main/java/com/mrokga/carrot_server/chat/service/QuickReplyService.java b/src/main/java/com/mrokga/carrot_server/chat/service/QuickReplyService.java index 935c6d5..32122cf 100644 --- a/src/main/java/com/mrokga/carrot_server/chat/service/QuickReplyService.java +++ b/src/main/java/com/mrokga/carrot_server/chat/service/QuickReplyService.java @@ -22,6 +22,7 @@ public class QuickReplyService { private final ChatMessageRepository chatMessageRepository; private final QuickReplyRepository quickReplyRepository; + // ์ž์ฃผ ์“ฐ๋Š” ๋ฌธ๊ตฌ ์ถ”๊ฐ€ @Transactional public QuickReplyAddResponseDto addQuikReply(Integer messageId){ Integer userId = QuickReplyUtils.currentUserIdOrThrow(); @@ -73,6 +74,7 @@ public QuickReplyAddResponseDto addQuikReply(Integer messageId){ } } + // ์ž์ฃผ ์“ฐ๋Š” ๋ฌธ๊ตฌ ์กฐํšŒ @Transactional(readOnly = true) public List listMine() { Integer userId = QuickReplyUtils.currentUserIdOrThrow(); @@ -82,6 +84,7 @@ public List listMine() { .toList(); } + // ์ž์ฃผ ์“ฐ๋Š” ๋ฌธ๊ตฌ ์‚ญ์ œ @Transactional public void deleteMine(Integer id) { Integer userId = QuickReplyUtils.currentUserIdOrThrow(); diff --git a/src/main/java/com/mrokga/carrot_server/community/controller/PostController.java b/src/main/java/com/mrokga/carrot_server/community/controller/PostController.java index 20a948b..9ba6268 100644 --- a/src/main/java/com/mrokga/carrot_server/community/controller/PostController.java +++ b/src/main/java/com/mrokga/carrot_server/community/controller/PostController.java @@ -45,7 +45,7 @@ private Integer getCurrentUserId() { return Integer.valueOf(auth.getName()); } - // โœ… 2-Step ๋ฐฉ์‹ (JSON-only) + // 2-Step ๋ฐฉ์‹ (JSON-only) @PostMapping @Operation(summary = "๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ", description = "์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.") public ResponseEntity> createPost(@RequestBody CreatePostRequestDto dto){ @@ -54,7 +54,7 @@ public ResponseEntity> createPost(@RequestBody CreatePostReque return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success",null)); } - // โœ… 1-Step ๋ฐฉ์‹ (๋ฉ€ํ‹ฐํŒŒํŠธ) + // 1-Step ๋ฐฉ์‹ (๋ฉ€ํ‹ฐํŒŒํŠธ) @PostMapping(value = "/multipart", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ(๋ฉ€ํ‹ฐํŒŒํŠธ: JSON + ์ด๋ฏธ์ง€ ํŒŒ์ผ)", diff --git a/src/main/java/com/mrokga/carrot_server/community/service/PostService.java b/src/main/java/com/mrokga/carrot_server/community/service/PostService.java index 2992328..0e8b941 100644 --- a/src/main/java/com/mrokga/carrot_server/community/service/PostService.java +++ b/src/main/java/com/mrokga/carrot_server/community/service/PostService.java @@ -61,11 +61,11 @@ public void createPost(CreatePostRequestDto dto){ .dealPlaceLng(dto.getDealPlaceLng()) .build(); - // 2. ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด PostImage ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ ํ›„ ๋งคํ•‘ + // 2. ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด PostImage(Post ํฌํ•จํ•œ ์—”ํ‹ฐํ‹ฐ) ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ ํ›„ ๋งคํ•‘ if (dto.getImages() != null && !dto.getImages().isEmpty()) { List postImages = dto.getImages().stream() .map(imgDto -> PostImage.builder() - .post(post) + .post(post) // 1๋ฒˆ์—์„œ ์ƒ์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€ ์—”ํ‹ฐํ‹ฐ .imageUrl(imgDto.getImageUrl()) .sortOrder(imgDto.getSortOrder() != null ? imgDto.getSortOrder() : 0) .isThumbnail(imgDto.getIsThumbnail() != null && imgDto.getIsThumbnail()) @@ -97,14 +97,14 @@ public void editPost(EditPostRequestDto dto, Integer me){ post.setContent(dto.getContent()); - // โœ… ์žฅ์†Œ ์—…๋ฐ์ดํŠธ: null ์ด๋ฉด โ€œ๋ณ€๊ฒฝ ์•ˆํ•จโ€, ๋นˆ๋ฌธ์ž๋ฉด โ€œ์‚ญ์ œโ€ + // ์žฅ์†Œ ์—…๋ฐ์ดํŠธ: null ์ด๋ฉด โ€œ๋ณ€๊ฒฝ ์•ˆํ•จโ€, ๋นˆ๋ฌธ์ž๋ฉด โ€œ์‚ญ์ œโ€ if (dto.getDealPlace() != null || dto.getDealPlaceLat() != null || dto.getDealPlaceLng() != null) { post.setDealPlace(dto.getDealPlace()); post.setDealPlaceLat(dto.getDealPlaceLat()); post.setDealPlaceLng(dto.getDealPlaceLng()); } - // โœ… ์ด๋ฏธ์ง€ ๊ต์ฒด ๋กœ์ง + // ์ด๋ฏธ์ง€ ๊ต์ฒด ๋กœ์ง if (dto.getImages() != null) { // ๊ธฐ์กด ์ด๋ฏธ์ง€๋“ค List oldImages = post.getImages(); @@ -114,7 +114,7 @@ public void editPost(EditPostRequestDto dto, Integer me){ .map(img -> img.getImageUrl()) .toList(); - // ์‚ญ์ œ๋  ์ด๋ฏธ์ง€ ์ถ”์ถœ = old(DB) list ์—๋Š” ์žˆ๋Š”๋ฐ new list ์—๋Š” ์—†์Œ + // ์‚ญ์ œ๋  ์ด๋ฏธ์ง€ ์ถ”์ถœ = old list(DB) ์—๋Š” ์žˆ๋Š”๋ฐ new list ์—๋Š” ์—†๋Š” ์ด๋ฏธ์ง€๋“ค List toDelete = oldImages.stream() .filter(img -> !newUrls.contains(img.getImageUrl())) .toList(); @@ -125,7 +125,7 @@ public void editPost(EditPostRequestDto dto, Integer me){ post.getImages().remove(img); }); - // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ด๋ฏธ์ง€ = new list ์—๋Š” ์žˆ๋Š”๋ฐ old(DB) list ์—๋Š” ์—†์Œ + // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ด๋ฏธ์ง€ = new list ์—๋Š” ์žˆ๋Š”๋ฐ old list(DB) ์—๋Š” ์—†๋Š” ์ด๋ฏธ์ง€๋“ค dto.getImages().forEach(imgDto -> { boolean exists = oldImages.stream() .anyMatch(img -> img.getImageUrl().equals(imgDto.getImageUrl())); @@ -151,7 +151,7 @@ public void deletePost(Integer postId, Integer me){ throw new SecurityException("PostService.deletePost(): ์‚ญ์ œ ๊ถŒํ•œ ์—†์Œ"); } - // โœ… S3์—์„œ ์ด๋ฏธ์ง€ ์‚ญ์ œ + // S3์—์„œ ์ด๋ฏธ์ง€ ์‚ญ์ œ post.getImages().forEach(img -> awsS3Service.deleteFileByUrl(img.getImageUrl())); // ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€ ์ข‹์•„์š” ์‚ญ์ œ @@ -170,7 +170,11 @@ public void deletePost(Integer postId, Integer me){ postRepository.delete(post); } - // ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ(ํŽ˜์ด์ง•) + // ๊ฒŒ์‹œ๊ธ€ ์ง€์—ญ & ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํšŒ(ํŽ˜์ด์ง•) + /** + * ์ง€์—ญ์€ ํ•„์ˆ˜, ์นดํ…Œ๊ณ ๋ฆฌ๋Š” nullable + * ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๋กœ ์ง€์—ญ๋งŒ ํ•„ํ„ฐ / ์ง€์—ญ+์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ + */ @Transactional(readOnly = true) public Page getPostList(Integer regionId, Integer categoryId, String keyword, Pageable pageable){ Region region = regionRepository.findById(regionId) @@ -216,11 +220,13 @@ public PostDetailResponseDto getPostDetail(Integer postId, Integer me){ List comments = commentRepository.findByPostIdOrderByCreatedAtAsc(postId); - // ๋‚ด๊ฐ€ ์ข‹์•„์š” ๋ˆ„๋ฅธ ๋Œ“๊ธ€ ID๋“ค์„ ํ•œ ๋ฒˆ์— ์กฐํšŒ + // ๋‚ด๊ฐ€ ์ข‹์•„์š” ๋ˆ„๋ฅธ ๋Œ“๊ธ€ ID ํ•œ ๋ฒˆ์— ์กฐํšŒ List commentIds = comments.stream().map(Comment::getId).toList(); List likedIds = commentLikeRepository.findLikedCommentIds(me, commentIds); Set likedIdSet = likedIds.stream().collect(Collectors.toSet()); + // ๊ฒŒ์‹œ๊ธ€์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€๋“ค ์กฐํšŒ + // ์ด๋–„ ๋‚ด๊ฐ€ ์ข‹์•„์š” ๋ˆ„๋ฅธ ๋Œ“๊ธ€ ID ํ•œ ๋ฒˆ์— ์กฐํšŒํ•œ ๊ฒƒ๋“ค๋กœ ๋‚ด ์ข‹์•„์š” ์—ฌ๋ถ€ ํŒ๋‹จ List commentDtos = comments.stream() .map(c -> CommentResponseDto.builder() .id(c.getId()) @@ -228,11 +234,12 @@ public PostDetailResponseDto getPostDetail(Integer postId, Integer me){ .nickname(c.getUser().getNickname()) .content(c.getContent()) .likeCount(c.getLikeCount()) - .likedByMe(likedIdSet.contains(c.getId())) + .likedByMe(likedIdSet.contains(c.getId())) // ๋Œ“๊ธ€ ์ข‹์•„์š” ์—ฌ๋ถ€ .createdAt(c.getCreatedAt()) .build()) .toList(); + // ๊ฒŒ์‹œ๊ธ€ ์ข‹์•„์š” ์—ฌ๋ถ€ boolean likedByMe = postLikeRepository.findByUserIdAndPostId(me, postId) != null; return PostDetailResponseDto.builder() @@ -282,6 +289,7 @@ public void togglePostLike(Integer postId, Integer me){ } + // ๊ฒŒ์‹œ๊ธ€ ์ง€์—ญ๋ณ„ ์กฐํšŒ @Transactional(readOnly = true) public Page getPostsByRegion(Integer regionId, Pageable pageable) { Region region = regionRepository.findById(regionId) diff --git a/src/main/java/com/mrokga/carrot_server/config/SecurityConfig.java b/src/main/java/com/mrokga/carrot_server/config/SecurityConfig.java index e08c250..fb2f8b6 100644 --- a/src/main/java/com/mrokga/carrot_server/config/SecurityConfig.java +++ b/src/main/java/com/mrokga/carrot_server/config/SecurityConfig.java @@ -28,7 +28,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers( "/actuator/health", "/actuator/info", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/api/theTest", "/__test-error", - "/api/**" + "/api/**", + "/payment/kakao/success", + "/payment/kakao/cancel", + "/payment/kakao/fail" ).permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // ์ถ”๊ฐ€ .anyRequest().authenticated() diff --git a/src/main/java/com/mrokga/carrot_server/group/controller/GroupController.java b/src/main/java/com/mrokga/carrot_server/group/controller/GroupController.java index 8f78fb8..218793b 100644 --- a/src/main/java/com/mrokga/carrot_server/group/controller/GroupController.java +++ b/src/main/java/com/mrokga/carrot_server/group/controller/GroupController.java @@ -201,14 +201,16 @@ public ResponseEntity leave(@PathVariable Integer id) { @Operation(summary = "๊ฐ€์ž… ์š”์ฒญ ๋ชฉ๋ก", description = "OWNER/MANAGER๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ. status=PENDING/APPROVED/REJECTED") @GetMapping("/{id}/requests") - public Page requests(@PathVariable Integer id, - @RequestParam(defaultValue = "PENDING") String status, - @ParameterObject @PageableDefault(size = 20) Pageable pg) { + public Page requests(@PathVariable Integer id, + @RequestParam(defaultValue = "PENDING") String status, + @ParameterObject @PageableDefault(size = 20) Pageable pg) { + var actor = membershipRepository.findByGroupIdAndUserId(id, me()) .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN)); if (actor.getRole() == GroupMembership.Role.MEMBER) throw new ResponseStatusException(HttpStatus.FORBIDDEN); - return joinRequestRepository.findByGroupIdAndStatus(id, GroupJoinRequest.Status.valueOf(status), pg); + + return groupService.listJoinRequests(id, GroupJoinRequest.Status.valueOf(status), pg); } @Operation(summary = "๊ฐ€์ž… ์Šน์ธ", description = "OWNER/MANAGER ๊ถŒํ•œ ํ•„์š”") diff --git a/src/main/java/com/mrokga/carrot_server/group/dto/GroupJoinRequestResponse.java b/src/main/java/com/mrokga/carrot_server/group/dto/GroupJoinRequestResponse.java new file mode 100644 index 0000000..4209e75 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/group/dto/GroupJoinRequestResponse.java @@ -0,0 +1,20 @@ +package com.mrokga.carrot_server.group.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GroupJoinRequestResponse { + private Integer id; + private Integer groupId; + private Integer userId; + private String userNickname; + private String status; // PENDING/APPROVED/REJECTED + private String message; + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/com/mrokga/carrot_server/group/repository/GroupJoinRequestRepository.java b/src/main/java/com/mrokga/carrot_server/group/repository/GroupJoinRequestRepository.java index 4cbe5a7..40cbf9b 100644 --- a/src/main/java/com/mrokga/carrot_server/group/repository/GroupJoinRequestRepository.java +++ b/src/main/java/com/mrokga/carrot_server/group/repository/GroupJoinRequestRepository.java @@ -2,8 +2,11 @@ import com.mrokga.carrot_server.group.entity.GroupJoinRequest; import org.springframework.data.domain.*; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; public interface GroupJoinRequestRepository extends JpaRepository { - Page findByGroupIdAndStatus(Integer groupId, GroupJoinRequest.Status status, Pageable pageable); + @EntityGraph(attributePaths = {"user", "group"}) + Page findByGroupIdAndStatus(Integer groupId, + GroupJoinRequest.Status status, Pageable pageable); } diff --git a/src/main/java/com/mrokga/carrot_server/group/service/GroupService.java b/src/main/java/com/mrokga/carrot_server/group/service/GroupService.java index 9a4c27e..52f36d4 100644 --- a/src/main/java/com/mrokga/carrot_server/group/service/GroupService.java +++ b/src/main/java/com/mrokga/carrot_server/group/service/GroupService.java @@ -248,6 +248,23 @@ public GroupEventResponse toEventResponse(GroupEvent ev, Integer meId) { .build(); } + @Transactional(readOnly = true) + public Page listJoinRequests(Integer groupId, + GroupJoinRequest.Status status, Pageable pg) { + + return joinRequestRepository + .findByGroupIdAndStatus(groupId, status, pg) + .map(req -> GroupJoinRequestResponse.builder() + .id(req.getId()) + .groupId(req.getGroup().getId()) + .userId(req.getUser().getId()) + .userNickname(req.getUser().getNickname()) + .status(req.getStatus().name()) + .message(req.getMessage()) + .createdAt(req.getCreatedAt()) + .build()); + } + } diff --git a/src/main/java/com/mrokga/carrot_server/payment/controller/PaymentController.java b/src/main/java/com/mrokga/carrot_server/payment/controller/PaymentController.java index 403eb5a..a3782f9 100644 --- a/src/main/java/com/mrokga/carrot_server/payment/controller/PaymentController.java +++ b/src/main/java/com/mrokga/carrot_server/payment/controller/PaymentController.java @@ -8,10 +8,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; @RestController @RequiredArgsConstructor -@RequestMapping("/api/payment") +@RequestMapping("/payment") @Tag(name = "Payment API", description = "๊ฒฐ์ œ ๊ด€๋ จ API") @Slf4j public class PaymentController { @@ -26,10 +27,12 @@ public KakaoReadyResponse kakaoReady(@PathVariable Integer transactionId) { // ๊ฒฐ์ œ ์„ฑ๊ณต @GetMapping("/kakao/success") - public KakaoApproveResponse kakaoSuccess(@RequestParam("transactionId") Integer transactionId, - @RequestParam("tid") String tid, - @RequestParam("pg_token") String pgToken) { - return paymentService.kakaoApprovePayment(transactionId, tid, pgToken); + public RedirectView kakaoSuccess(@RequestParam("transactionId") Integer transactionId, + @RequestParam("pg_token") String pgToken) { + + paymentService.kakaoApprovePayment(transactionId, pgToken); + + return new RedirectView("http://localhost:3000/payment/success?transactionId=" + transactionId); } // ๊ฒฐ์ œ ์ทจ์†Œ diff --git a/src/main/java/com/mrokga/carrot_server/payment/service/PaymentService.java b/src/main/java/com/mrokga/carrot_server/payment/service/PaymentService.java index 320e6f2..cadfcd6 100644 --- a/src/main/java/com/mrokga/carrot_server/payment/service/PaymentService.java +++ b/src/main/java/com/mrokga/carrot_server/payment/service/PaymentService.java @@ -9,6 +9,7 @@ import com.mrokga.carrot_server.payment.repository.PaymentRepository; import com.mrokga.carrot_server.transaction.entity.Transaction; import com.mrokga.carrot_server.transaction.repository.TransactionRepository; +import com.mrokga.carrot_server.transaction.service.TransactionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -33,6 +34,7 @@ public class PaymentService { private final PaymentRepository paymentRepository; private final TransactionRepository transactionRepository; + private final TransactionService transactionService; @Value("${kakaopay.host}") private String kakaoHost; @@ -48,6 +50,11 @@ public class PaymentService { private final RestTemplate restTemplate = new RestTemplate(); + /** + * ์นด์นด์˜คํŽ˜์ด API ํ†ต์‹ ์„ ์œ„ํ•œ ๊ณตํ†ต ํ—ค๋” ์ƒ์„ฑ method + * Authorization header์™€ Content-Type ์„ค์ • + * @return HttpHeaders ๊ฐ์ฒด + */ private HttpHeaders buildKakaoHeaders() { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "KakaoAK " + kakaoAdminKey); @@ -55,21 +62,30 @@ private HttpHeaders buildKakaoHeaders() { return headers; } - // โœ… ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ค€๋น„ + /** + * ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ค€๋น„๋ฅผ ์š”์ฒญํ•˜๊ณ , DB์— ๊ฒฐ์ œ ์ •๋ณด๋ฅผ ์ €์žฅ + * @param transactionId ๊ฑฐ๋ž˜ ID + * @return ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ค€๋น„ ์‘๋‹ต DTO + */ public KakaoReadyResponse kakaoReadyPayment(Integer transactionId) { + // 1. ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ Transaction transaction = transactionRepository.findById(transactionId) .orElseThrow(() -> new IllegalArgumentException("Transaction not found")); + // 2. ์ด๋ฏธ ๊ฒฐ์ œ๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ (์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€) if (paymentRepository.findByTransaction(transaction).isPresent()) { throw new IllegalStateException("์ด๋ฏธ ๊ฒฐ์ œ๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ ๊ฑฐ๋ž˜์ž…๋‹ˆ๋‹ค."); } + // 3. ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ค€๋น„ API URL String url = kakaoHost + "/v1/payment/ready"; + // 4. ์š”์ฒญ ํ—ค๋” ์„ค์ • HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "KakaoAK " + kakaoAdminKey); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 5. ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • MultiValueMap params = new LinkedMultiValueMap<>(); params.add("cid", kakaoCid); params.add("partner_order_id", String.valueOf(transaction.getId())); @@ -82,17 +98,20 @@ public KakaoReadyResponse kakaoReadyPayment(Integer transactionId) { params.add("cancel_url", kakaoBaseUrl + "/cancel"); params.add("fail_url", kakaoBaseUrl + "/fail"); + // 6. API ์š”์ฒญ HttpEntity> request = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.postForEntity(url, request, Map.class); Map body = response.getBody(); + // 7. ์‘๋‹ต ํ™•์ธ ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ if (response.getStatusCode().isError() || body == null) { throw new IllegalStateException("์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ค€๋น„ ์‹คํŒจ: " + body.get("msg")); } log.info("[KakaoPay] Ready - transactionId={}, tid={}", transactionId, body.get("tid")); + // 8. DB์— ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ (์ƒํƒœ: READY) Payment payment = Payment.builder() .transaction(transaction) .method(PaymentMethod.KAKAOPAY) @@ -103,6 +122,7 @@ public KakaoReadyResponse kakaoReadyPayment(Integer transactionId) { .build(); paymentRepository.save(payment); + // 9. ํด๋ผ์ด์–ธํŠธ์— Redirect ์ •๋ณด ๋ฐ˜ํ™˜ return KakaoReadyResponse.builder() .tid((String) body.get("tid")) .redirectPcUrl((String) body.get("next_redirect_pc_url")) @@ -111,24 +131,37 @@ public KakaoReadyResponse kakaoReadyPayment(Integer transactionId) { .build(); } - // โœ… ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์Šน์ธ - public KakaoApproveResponse kakaoApprovePayment(Integer transactionId, String tid, String pgToken) { + /** + * ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์Šน์ธ ์š”์ฒญ + * ๊ฒฐ์ œ ๊ธˆ์•ก ๊ฒ€์ฆ ํ›„ ์ƒํƒœ๋ฅผ APPROVED๋กœ ์—…๋ฐ์ดํŠธ + * @param transactionId ๊ฑฐ๋ž˜ ID + * @param pgToken ๊ฒฐ์ œ ์Šน์ธ ์š”์ฒญ์„ ์œ„ํ•œ ํ† ํฐ + * @return ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์Šน์ธ ์‘๋‹ต DTO + */ + public KakaoApproveResponse kakaoApprovePayment(Integer transactionId, String pgToken) { + // 1. ํŠธ๋žœ์žญ์…˜, ๊ฒฐ์ œ ์ •๋ณด ์กฐํšŒ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ Transaction transaction = transactionRepository.findById(transactionId) .orElseThrow(() -> new IllegalArgumentException("Transaction not found")); Payment payment = paymentRepository.findByTransaction(transaction) .orElseThrow(() -> new IllegalArgumentException("Payment not found")); + String tid = payment.getTid(); + + // 2. ์ด๋ฏธ ์Šน์ธ๋œ ๊ฒฐ์ œ์ธ์ง€ ํ™•์ธ(์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€) if (payment.getStatus() == PaymentStatus.APPROVED) { throw new IllegalStateException("์ด๋ฏธ ์Šน์ธ๋œ ๊ฒฐ์ œ์ž…๋‹ˆ๋‹ค."); } + // 3. ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์Šน์ธ API URL String url = kakaoHost + "/v1/payment/approve"; + // 4. ์š”์ฒญ ํ—ค๋” ์„ค์ • HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "KakaoAK " + kakaoAdminKey); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 5. ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • MultiValueMap params = new LinkedMultiValueMap<>(); params.add("cid", kakaoCid); params.add("tid", tid); @@ -136,26 +169,36 @@ public KakaoApproveResponse kakaoApprovePayment(Integer transactionId, String ti params.add("partner_user_id", String.valueOf(transaction.getBuyer().getId())); params.add("pg_token", pgToken); + // 6. API ์š”์ฒญ HttpEntity> request = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.postForEntity(url, request, Map.class); Map body = response.getBody(); + if (response.getStatusCode().isError() || body == null) { + throw new IllegalStateException("์นด์นด์˜คํŽ˜์ด ์Šน์ธ ์‹คํŒจ"); + } + // 7. ๊ฒฐ์ œ ๊ธˆ์•ก ๊ฒ€์ฆ int kakaoAmount = (Integer) ((Map) body.get("amount")).get("total"); int expectedAmount = transaction.getProduct().getPrice(); + // ๊ฒฐ์ œ ๊ธˆ์•ก์ด DB์— ๊ธฐ๋ก๋œ ๊ธฐ๋Œ€ ๊ธˆ์•ก๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ if (kakaoAmount != expectedAmount) { throw new IllegalStateException("๊ฒฐ์ œ ๊ธˆ์•ก ๋ถˆ์ผ์น˜ (expected=" + expectedAmount + ", kakao=" + kakaoAmount + ")"); } - log.info("[KakaoPay] Approved - transactionId={}, tid={}", transactionId, tid); + // ๋‚ด๋ถ€ ๊ฑฐ๋ž˜ ์™„๋ฃŒ ์ฒ˜๋ฆฌ (์ƒํ’ˆ SOLD) + transactionService.approve(transactionId); + // 8. DB ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๊ฒฐ์ œ ์ƒํƒœ: APPROVED) payment.setStatus(PaymentStatus.APPROVED); payment.setCompletedAt(LocalDateTime.now()); paymentRepository.save(payment); + // 9. ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ transaction.setCompletedAt(LocalDateTime.now()); transactionRepository.save(transaction); + // 10. ์Šน์ธ ์‘๋‹ต DTO ๋ฐ˜ํ™˜ return KakaoApproveResponse.builder() .aid((String) body.get("aid")) .tid((String) body.get("tid")) @@ -165,25 +208,38 @@ public KakaoApproveResponse kakaoApprovePayment(Integer transactionId, String ti .build(); } - // โœ… ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ทจ์†Œ + /** + * ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ + * @param tid ์นด์นด์˜คํŽ˜์ด ๊ฑฐ๋ž˜ ๊ณ ์œ ๋ฒˆํ˜ธ + * @param cancelAmount ์ทจ์†Œํ•  ๊ธˆ์•ก + * @return ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ทจ์†Œ ์‘๋‹ต DTO + */ public KakaoCancelResponse kakaoCancelPayment(String tid, int cancelAmount) { + // 1. ์นด์นด์˜คํŽ˜์ด ๊ฒฐ์ œ ์ทจ์†Œ API URL String url = kakaoHost + "/v1/payment/cancel"; + // 2. ์š”์ฒญ ํ—ค๋” ์„ค์ • HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "KakaoAK " + kakaoAdminKey); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 3. ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ • MultiValueMap params = new LinkedMultiValueMap<>(); params.add("cid", kakaoCid); params.add("tid", tid); params.add("cancel_amount", String.valueOf(cancelAmount)); params.add("cancel_tax_free_amount", "0"); + // 4. API ์š”์ฒญ HttpEntity> request = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.postForEntity(url, request, Map.class); Map body = response.getBody(); + if (response.getStatusCode().isError() || body == null) { + throw new IllegalStateException("์นด์นด์˜คํŽ˜์ด ์ทจ์†Œ ์‹คํŒจ"); + } + // 5. DB์—์„œ ๊ฒฐ์ œ ์ •๋ณด ์กฐํšŒ ๋ฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (์ƒํƒœ: CANCELED) Payment payment = paymentRepository.findByTid(tid) .orElseThrow(() -> new IllegalArgumentException("Payment not found")); @@ -191,8 +247,13 @@ public KakaoCancelResponse kakaoCancelPayment(String tid, int cancelAmount) { payment.setCompletedAt(LocalDateTime.now()); paymentRepository.save(payment); - log.info("[KakaoPay] Canceled - tid={}", tid); + // ์˜ˆ์•ฝ ์ทจ์†Œ ์ฒ˜๋ฆฌ(์ƒํ’ˆ ON_SALE ๋ณต๊ท€) + Transaction tx = payment.getTransaction(); + if (tx != null) { + transactionService.cancel(tx.getId()); + } + // 6. ์ทจ์†Œ ์‘๋‹ต DTO ๋ฐ˜ํ™˜ return KakaoCancelResponse.builder() .tid((String) body.get("tid")) .canceledAmount((Integer) ((Map) body.get("canceled_amount")).get("total")) diff --git a/src/main/java/com/mrokga/carrot_server/product/controller/ProductController.java b/src/main/java/com/mrokga/carrot_server/product/controller/ProductController.java index 99d9e65..dc50f20 100644 --- a/src/main/java/com/mrokga/carrot_server/product/controller/ProductController.java +++ b/src/main/java/com/mrokga/carrot_server/product/controller/ProductController.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.mrokga.carrot_server.aws.service.AwsS3Service; import com.mrokga.carrot_server.product.dto.request.ProductImageRequestDto; +import com.mrokga.carrot_server.product.dto.response.ChangeStatusResponseDto; import com.mrokga.carrot_server.product.dto.response.ProductDetailResponseDto; import com.mrokga.carrot_server.api.dto.ApiResponseDto; import com.mrokga.carrot_server.product.dto.request.ChangeStatusRequestDto; @@ -15,11 +16,14 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -40,7 +44,12 @@ public class ProductController { private final ProductService productService; private final AwsS3Service awsS3Service; - // ๊ธฐ์กด JSON ๋ฐฉ์‹ ์œ ์ง€ (@RequestBody) โ€” 2-Step ๋“ฑ๋ก์šฉ + /** + * ์ƒํ’ˆ ๋“ฑ๋ก api (์ด๋ฏธ์ง€ URL์„ JSON ๋‚ด๋ถ€์— ํฌํ•จํ•˜๋Š” 2-step ๋ฐฉ์‹) + * ์ด๋ฏธ์ง€๋Š” ๋ฏธ๋ฆฌ S3์— ์—…๋กœ๋“œ๋˜์–ด URL ํ˜•ํƒœ๋กœ ์š”์ฒญ ๋ณธ๋ฌธ์— ํฌํ•จ๋˜์–ด์•ผ ํ•จ + * @param req ์ƒํ’ˆ ์ƒ์„ฑ ์š”์ฒญ DTO (์ด๋ฏธ์ง€ URL ํฌํ•จ) + * @return ๋“ฑ๋ก๋œ ์ƒํ’ˆ ์ •๋ณด + */ @PostMapping @Operation(summary = "์ƒํ’ˆ ๋“ฑ๋ก(JSON, ์ด๋ฏธ์ง€ URL ํฌํ•จ)", description = "์ด๋ฏธ์ง€๋ฅผ ๋จผ์ € /file/upload๋กœ ์˜ฌ๋ฆฌ๊ณ , ๋ฐ˜ํ™˜๋œ S3 URL์„ ๋ณธ API์— images.imageUrl๋กœ ์ „๋‹ฌํ•˜์„ธ์š”.") @ApiResponse(responseCode = "200", description = "์ƒํ’ˆ ๋“ฑ๋ก ์„ฑ๊ณต", content = @Content(schema = @Schema(implementation = ApiResponseDto.class))) @@ -49,7 +58,14 @@ public ResponseEntity> create(@RequestBody CreateProductReques return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", product)); } - // ์‹ ๊ทœ: ๋ฉ€ํ‹ฐํŒŒํŠธ ํ•œ๋ฐฉ ๋“ฑ๋ก(JSON + ํŒŒ์ผ) + /** + * ์ƒํ’ˆ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋Š” api + * ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ S3์— ์—…๋กœ๋“œ ํ›„, ๋ฐ˜ํ™˜๋œ URL์„ DTO์— ์ฃผ์ž…ํ•˜์—ฌ ์ƒํ’ˆ ๋“ฑ๋ก ์„œ๋น„์Šค์— ์ „๋‹ฌ + * @param metaJson ์ƒํ’ˆ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ JSON ๋ฌธ์ž์—ด (CreateProductRequestDto) + * @param images ์—…๋กœ๋“œํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ ๋ฆฌ์ŠคํŠธ + * @return ๋“ฑ๋ก๋œ ์ƒํ’ˆ ์ •๋ณด + * @throws Exception JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ๋˜๋Š” ํŒŒ์ผ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜ + */ @PostMapping(value = "/multipart", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "์ƒํ’ˆ ๋“ฑ๋ก(๋ฉ€ํ‹ฐํŒŒํŠธ: JSON + ์ด๋ฏธ์ง€ ํŒŒ์ผ)", @@ -108,15 +124,36 @@ public ResponseEntity> createMultipart( return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", product)); } + /** + * ์ƒํ’ˆ์˜ ๊ฑฐ๋ž˜ ์ƒํƒœ(TradeStatus)๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” api + * @param req ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ DTO + * @return ๋ณ€๊ฒฝ๋œ ๊ฑฐ๋ž˜ ์ƒํƒœ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ์‘๋‹ต DTO + */ @Operation(summary = "์ƒํ’ˆ ๊ฑฐ๋ž˜ ์ƒํƒœ ๋ณ€๊ฒฝ", description = "์ƒํ’ˆ ๊ฑฐ๋ž˜ ์ƒํƒœ ๋ณ€๊ฒฝ") @PutMapping("/status") public ResponseEntity changeStatus(@RequestBody ChangeStatusRequestDto req) { productService.changeStatus(req); + // ์ƒํ’ˆ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์‘๋‹ต DTO์— ์ €์žฅ + ChangeStatusResponseDto response = productService.changeStatus(req); + + // ์ƒํƒœ ๋ณ€๊ฒฝ ํ›„ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฆฌํ„ด + Product product = productService.getProductDetail(req.getProductId()); + + if (product != null) { + + return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", response)); + } + return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", null)); } + /** + * ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” api + * @param userId ์‚ฌ์šฉ์ž์˜ ID + * @return ํŽ˜์ด์ง€๋„ค์ด์…˜๋œ ์ƒํ’ˆ ๋ชฉ๋ก DTO + */ @Operation(summary = "์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ", description = "ํ˜„์žฌ ์œ„์น˜์— ๋…ธ์ถœ ์„ค์ •ํ•œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ") @GetMapping("/list") public ResponseEntity> getProductList(@RequestParam int userId) { @@ -124,6 +161,11 @@ public ResponseEntity> getProductList(@RequestParam int userId return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", list)); } + /** + * ์ƒํ’ˆ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” api + * @param id ์กฐํšŒํ•  ์ƒํ’ˆ์˜ ID + * @return ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด DTO + */ @Operation(summary = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", description = "์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ") @GetMapping("/{id}") public ResponseEntity> getProductDetail(@Parameter(description = "์ƒํ’ˆ ID", example = "7") @PathVariable int id) { @@ -137,6 +179,12 @@ public ResponseEntity> getProductDetail(@Parameter(description return ResponseEntity.ok(ApiResponseDto.error(HttpStatus.NOT_FOUND.value(), "not found")); } + /** + * ํŠน์ • ์ƒํ’ˆ์„ ์ฐœํ•˜๋Š” api. ํ† ๊ธ€ ๋ฐฉ์‹ + * @param userId ์‚ฌ์šฉ์ž ID + * @param productId ์ฐœํ•˜๊ธฐ ๋Œ€์ƒ ์ƒํ’ˆ ID + * @return ์„ฑ๊ณต ์‘๋‹ต (์ƒํƒœ ๋ณ€๊ฒฝ ์™„๋ฃŒ) + */ @Operation(summary = "์ฐœํ•˜๊ธฐ", description = "์ฐœํ•˜๊ธฐ") @PostMapping("/{productId}/favorite") public ResponseEntity favoriteProduct(@Parameter(description = "์œ ์ € ID", example = "7") @RequestParam int userId, @Parameter(description = "์ƒํ’ˆ ID", example = "7") @PathVariable int productId) { @@ -145,12 +193,41 @@ public ResponseEntity favoriteProduct(@Parameter(description = "์œ ์ € ID", e return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", null)); } - @Operation(summary = "์ƒํ’ˆ ์ด๋ฆ„ ๊ฒ€์ƒ‰", description = "์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ƒํ’ˆ ์ด๋ฆ„์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก ๊ฒ€์ƒ‰") - @GetMapping("/search/{keyword}") - public ResponseEntity searchProduct(@PathVariable String keyword, - @PageableDefault(size = 10, sort = "createdAt") Pageable pageable) { + /** + * ์ƒํ’ˆ ์ด๋ฆ„ ํ‚ค์›Œ๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋Š” api + * @param keyword ๊ฒ€์ƒ‰ํ•  ์ƒํ’ˆ ์ด๋ฆ„ ํ‚ค์›Œ๋“œ + * @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param size ํ•œ ํŽ˜์ด์ง€๋‹น ์ƒํ’ˆ ๊ฐœ์ˆ˜ + * @param sort ์ •๋ ฌ ๊ธฐ์ค€ ํ•„๋“œ + * @return ํŽ˜์ด์ง€๋„ค์ด์…˜๋œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ DTO + */ + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "์ƒํ’ˆ ๊ฒ€์ƒ‰ ์„ฑ๊ณต", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiResponseDto.ApiPageProductResponse.class))) + }) + public ResponseEntity searchProduct(@Parameter(description = "๊ฒ€์ƒ‰ํ•  ํ‚ค์›Œ๋“œ", example = "test") @PathVariable String keyword, + @Parameter(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘)", example = "0") @RequestParam(defaultValue = "0") int page, + @Parameter(description = "ํ•œ ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "10") @RequestParam(defaultValue = "10") int size, + @Parameter(description = "์ •๋ ฌ ๊ธฐ์ค€ (์˜ˆ: createdAt)", example = "createdAt") @RequestParam(defaultValue = "createdAt") String sort) { + // Pageable ๊ฐ์ฒด ์ƒ์„ฑ: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์„ฑ + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt")); Page results = productService.searchProduct(keyword, pageable); return ResponseEntity.ok(ApiResponseDto.success(HttpStatus.OK.value(), "success", results)); } + + @Operation( + summary = "์ƒํ’ˆ ์‚ญ์ œ", + description = "์ƒํ’ˆ ๋“ฑ๋ก์ž(ํŒ๋งค์ž)๋งŒ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + ) + @DeleteMapping("/{id}") + public ResponseEntity> deleteProduct( + @PathVariable int id, + @Parameter(description = "ํŒ๋งค์ž(์š”์ฒญ์ž) ID", example = "11") + @RequestParam int sellerId + ) { + productService.deleteProduct(id, sellerId); + return ResponseEntity.ok( + ApiResponseDto.success(HttpStatus.OK.value(), "success", null) + ); + } } diff --git a/src/main/java/com/mrokga/carrot_server/product/controller/ReviewController.java b/src/main/java/com/mrokga/carrot_server/product/controller/ReviewController.java new file mode 100644 index 0000000..373df93 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/controller/ReviewController.java @@ -0,0 +1,57 @@ +// package com.mrokga.carrot_server.product.controller; +package com.mrokga.carrot_server.product.controller; + +import com.mrokga.carrot_server.product.dto.request.ReviewCreateRequest; +import com.mrokga.carrot_server.product.dto.response.ReviewResponse; +import com.mrokga.carrot_server.product.service.ReviewService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.*; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +@Tag(name = "ํ›„๊ธฐ(Review)") +public class ReviewController { + + private final ReviewService reviewService; + + private Integer me() { + var a = SecurityContextHolder.getContext().getAuthentication(); + if (a == null || !a.isAuthenticated() || "anonymousUser".equals(a.getPrincipal())) + throw new RuntimeException("UNAUTHORIZED"); + return Integer.valueOf(a.getName()); + } + + /** ํ›„๊ธฐ ์ž‘์„ฑ(๊ตฌ๋งค์ž) */ + @PostMapping("/reviews") + public ResponseEntity create(@RequestBody ReviewCreateRequest req) { + return ResponseEntity.ok(reviewService.createReview(me(), req)); + } + + /** ๋‚ด๊ฐ€ ๋ฐ›์€ ํ›„๊ธฐ(ํŒ๋งค์ž ์ž…์žฅ) */ + @GetMapping("/users/{userId}/reviews/received") + public Page received(@PathVariable Integer userId, + @ParameterObject @PageableDefault(size=20, sort = "id", direction=Sort.Direction.DESC) Pageable pg) { + return reviewService.listReceived(userId, pg); + } + + /** ๋‚ด๊ฐ€ ์“ด ํ›„๊ธฐ(๊ตฌ๋งค์ž ์ž…์žฅ) */ + @GetMapping("/users/{userId}/reviews/written") + public Page written(@PathVariable Integer userId, + @ParameterObject @PageableDefault(size=20, sort = "id", direction=Sort.Direction.DESC) Pageable pg) { + return reviewService.listWritten(userId, pg); + } + + /** ์ƒํ’ˆ๋ณ„ ํ›„๊ธฐ(์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) */ + @GetMapping("/product/{productId}/reviews") + public Page byProduct(@PathVariable Integer productId, + @ParameterObject @PageableDefault(size=20, sort = "id", direction=Sort.Direction.DESC) Pageable pg) { + return reviewService.listByProduct(productId, pg); + } +} diff --git a/src/main/java/com/mrokga/carrot_server/product/dto/request/ReviewCreateRequest.java b/src/main/java/com/mrokga/carrot_server/product/dto/request/ReviewCreateRequest.java new file mode 100644 index 0000000..577a98d --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/dto/request/ReviewCreateRequest.java @@ -0,0 +1,21 @@ +package com.mrokga.carrot_server.product.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + + +@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +@Schema(name = "ReviewCreateRequest") +public class ReviewCreateRequest { + @Schema(description="๊ตฌ๋งค ๊ฑฐ๋ž˜ ID", example="123", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer transactionId; + + @Schema(description="์ƒํ’ˆ ID", example="15", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer productId; + + @Schema(description="๋ณ„์ (1~5)", example="5", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer rating; + + @Schema(description="ํ›„๊ธฐ ๋‚ด์šฉ(์„ ํƒ)", example="์นœ์ ˆํ•˜๊ณ  ์•ฝ์†๋„ ์ž˜ ์ง€ํ‚ค์…จ์–ด์š”!") + private String content; +} \ No newline at end of file diff --git a/src/main/java/com/mrokga/carrot_server/product/dto/response/ReviewResponse.java b/src/main/java/com/mrokga/carrot_server/product/dto/response/ReviewResponse.java new file mode 100644 index 0000000..b5b7884 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/dto/response/ReviewResponse.java @@ -0,0 +1,21 @@ +package com.mrokga.carrot_server.product.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +@Schema(name = "ReviewResponse") +public class ReviewResponse { + private Integer id; + private Integer transactionId; + private Integer productId; + private Integer buyerId; + private String buyerNickname; + private Integer sellerId; + private String sellerNickname; + private Integer rating; + private String content; + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/com/mrokga/carrot_server/product/entity/Review.java b/src/main/java/com/mrokga/carrot_server/product/entity/Review.java new file mode 100644 index 0000000..e513a1e --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/entity/Review.java @@ -0,0 +1,47 @@ +// package com.mrokga.carrot_server.product.entity; +package com.mrokga.carrot_server.product.entity; + +import com.mrokga.carrot_server.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "review", + uniqueConstraints = @UniqueConstraint(columnNames = {"transaction_id"})) +@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +public class Review { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** ๊ฑฐ๋ž˜(๊ตฌ๋งค) ์‹๋ณ„์ž: ๊ตฌ๋งค๋‚ด์—ญ DTO์— ์žˆ๋˜ transactionId */ + @Column(name = "transaction_id", nullable = false) + private Integer transactionId; + + /** ์–ด๋–ค ์ƒํ’ˆ์— ๋Œ€ํ•œ ํ›„๊ธฐ์ธ์ง€(์กฐํšŒ ํŽธ์˜๋ฅผ ์œ„ํ•ด ์ €์žฅ) */ + @Column(name = "product_id", nullable = false) + private Integer productId; + + /** ํ›„๊ธฐ ์ž‘์„ฑ์ž(๊ตฌ๋งค์ž) */ + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "buyer_id", nullable = false) + private User buyer; + + /** ํ›„๊ธฐ ๋Œ€์ƒ์ž(ํŒ๋งค์ž) */ + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "seller_id", nullable = false) + private User seller; + + /** ๋ณ„์  1~5 */ + @Column(name = "rating", nullable = false) + private int rating; + + /** ์ฝ”๋ฉ˜ํŠธ(์˜ต์…˜) */ + @Column(name = "content", length = 500) + private String content; + + @CreationTimestamp + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/mrokga/carrot_server/product/repository/ReviewRepository.java b/src/main/java/com/mrokga/carrot_server/product/repository/ReviewRepository.java new file mode 100644 index 0000000..7e5ea09 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/repository/ReviewRepository.java @@ -0,0 +1,14 @@ +// package com.mrokga.carrot_server.product.repository; +package com.mrokga.carrot_server.product.repository; + +import com.mrokga.carrot_server.product.entity.Review; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { + boolean existsByTransactionId(Integer txId); + Page findBySeller_Id(Integer sellerId, Pageable pg); // ๋ฐ›์€ ํ›„๊ธฐ + Page findByBuyer_Id(Integer buyerId, Pageable pg); // ๋‚ด๊ฐ€ ์“ด ํ›„๊ธฐ + Page findByProductId(Integer productId, Pageable pg); +} diff --git a/src/main/java/com/mrokga/carrot_server/product/service/ProductService.java b/src/main/java/com/mrokga/carrot_server/product/service/ProductService.java index 65e254a..ee63b26 100644 --- a/src/main/java/com/mrokga/carrot_server/product/service/ProductService.java +++ b/src/main/java/com/mrokga/carrot_server/product/service/ProductService.java @@ -19,6 +19,7 @@ import com.mrokga.carrot_server.transaction.repository.TransactionRepository; import com.mrokga.carrot_server.user.entity.User; import com.mrokga.carrot_server.user.repository.UserRepository; +import com.openai.models.beta.threads.runs.Run; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -45,13 +46,20 @@ public class ProductService { private final ProductExposureRegionRepository productExposureRegionRepository; private final UserRegionRepository userRegionRepository; private final NotificationService notificationService; + private final ReviewRepository reviewRepository; + /** + * ์ƒˆ๋กœ์šด ์ƒํ’ˆ์„ ๋“ฑ๋ก + * + * @param req ์ƒํ’ˆ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ DTO + * @return ์ €์žฅ๋œ Product ์—”ํ‹ฐํ‹ฐ + */ @Transactional public Product createProduct(CreateProductRequestDto req) { try { log.info("[ProductService.createProduct] req = {}", req); - // 1) ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ + // 1) ๊ธฐ๋ณธ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ User user = userRepository.findById(req.getUserId()) .orElseThrow(() -> new EntityNotFoundException("[ProductService.createProduct] User not found")); @@ -61,23 +69,26 @@ public Product createProduct(CreateProductRequestDto req) { Category category = categoryRepository.findById(req.getCategoryId()) .orElseThrow(() -> new EntityNotFoundException("[ProductService.createProduct] Category not found")); - // 2) ์ด๋ฏธ์ง€ ๋งคํ•‘ (์—ฌ๋Ÿฌ ์žฅ) + // 2) ์ด๋ฏธ์ง€ ๋งคํ•‘ ๋ฐ ์ฒ˜๋ฆฌ (์ˆœ์„œ, ์ธ๋„ค์ผ ์ง€์ •) List productImages = null; if (req.getImages() != null && !req.getImages().isEmpty()) { AtomicInteger index = new AtomicInteger(0); - // ์ธ๋„ค์ผ ์ง€์ • ์—ฌ๋ถ€ ์ฒดํฌ + // ์š”์ฒญ์—์„œ ์ธ๋„ค์ผ(isThumbnail=true)์ด ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ boolean hasThumb = req.getImages().stream() .anyMatch(i -> Boolean.TRUE.equals(i.getIsThumbnail())); productImages = req.getImages().stream() + // sortOrder ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ .sorted(Comparator.comparingInt(ProductImageRequestDto::getSortOrder)) .map(imageDto -> ProductImage.builder() .imageUrl(imageDto.getImageUrl()) .sortOrder(index.getAndIncrement()) .isThumbnail( hasThumb + // ์ธ๋„ค์ผ์ด ์ง€์ •๋˜์–ด ์žˆ๋‹ค๋ฉด ๊ทธ ๊ฐ’์„ ๋”ฐ๋ฅด๊ณ  ? Boolean.TRUE.equals(imageDto.getIsThumbnail()) - : index.get() == 1 // ์ธ๋„ค์ผ ์ง€์ • ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ๋งŒ true + // ์ง€์ •์ด ์•ˆ ๋˜์–ด ์žˆ์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€(index.get() == 1)๋ฅผ ์ธ๋„ค์ผ๋กœ ์ž๋™ ์„ค์ • + : index.get() == 1 ) .build()) .toList(); @@ -93,7 +104,7 @@ public Product createProduct(CreateProductRequestDto req) { .build(); } - // 4) Product ์ƒ์„ฑ + // 4) Product ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ Product product = Product.builder() .user(user) .region(region) @@ -107,7 +118,7 @@ public Product createProduct(CreateProductRequestDto req) { .preferredLocation(preferredLocation) .build(); - // ์—ฐ๊ด€๊ด€๊ณ„ ์„ค์ • + // ์—ฐ๊ด€๊ด€๊ณ„ ์„ค์ • (ProductImage์™€ Product ๋งตํ•‘) if (productImages != null) { productImages.forEach(img -> img.setProduct(product)); } @@ -135,6 +146,7 @@ public Product createProduct(CreateProductRequestDto req) { Favorite favorite = favoriteRepository.findByUserIdAndCategoryId(req.getUserId(), req.getCategoryId()); + // 7) ์นดํ…Œ๊ณ ๋ฆฌ ์•Œ๋ฆผ ์ „์†ก (์ฐœํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์•Œ๋ฆผ) if (favorite != null) { notificationService.sendCategoryProductNotification(user, product); } @@ -147,7 +159,9 @@ public Product createProduct(CreateProductRequestDto req) { } } - /*** + /** + * ์ƒํ’ˆ์˜ ๊ฑฐ๋ž˜ ์ƒํƒœ(TradeStatus)๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ๊ด€๋ จ ํŠธ๋žœ์žญ์…˜(Transaction) ์ •๋ณด๋ฅผ ์—…๋ฐ์ดํŠธ + * * ํŒ๋งค์ค‘: ์˜ˆ์•ฝ์ค‘/๊ฑฐ๋ž˜์™„๋ฃŒ ๋‘˜ ๋‹ค ์ „์ด๊ฐ€๋Šฅ * ํŒ๋งค์ค‘ -> ์˜ˆ์•ฝ์ค‘: status ์˜ˆ์•ฝ์ค‘์œผ๋กœ ๋ณ€๊ฒฝ, transaction ์ƒ์„ฑ, buyer_id๊ฐ€ null์ด ์•„๋‹ˆ๋ฉด ์ถ”๊ฐ€ * ํŒ๋งค์ค‘ -> ๊ฑฐ๋ž˜์™„๋ฃŒ: status ๊ฑฐ๋ž˜์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ, transaction ์ƒ์„ฑ, buyer_id๊ฐ€ null์ด ์•„๋‹ˆ๋ฉด ์ถ”๊ฐ€, completed_at ์ถ”๊ฐ€ @@ -157,9 +171,14 @@ public Product createProduct(CreateProductRequestDto req) { * ์˜ˆ์•ฝ์ค‘ -> ๊ฑฐ๋ž˜์™„๋ฃŒ: status ๊ฑฐ๋ž˜์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ, buyer_id๊ฐ€ null์ด๋ฉด ์‚ญ์ œ, null์ด ์•„๋‹ˆ๋ฉด ๋ณ€๊ฒฝ * * ๊ฑฐ๋ž˜์™„๋ฃŒ -> ํŒ๋งค์ค‘: transaction ์‚ญ์ œ, ๋ฆฌ๋ทฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์‚ญ์ œ, status ํŒ๋งค์ค‘์œผ๋กœ ๋ณ€๊ฒฝ + * + * @param req ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ DTO (์ƒํ’ˆ ID, ๋ณ€๊ฒฝํ•  ์ƒํƒœ, ๊ตฌ๋งค์ž ID ๋“ฑ ํฌํ•จ) + * @return ๋ณ€๊ฒฝ๋œ ์ƒํƒœ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” ์‘๋‹ต DTO + * @throws EntityNotFoundException ์ƒํ’ˆ, ํŒ๋งค์ž, ๊ตฌ๋งค์ž ๋˜๋Š” ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒ */ @Transactional public ChangeStatusResponseDto changeStatus(ChangeStatusRequestDto req) { + log.info("[ProductService.changeStatus] req = {}", req); Product product = productRepository.findById(req.getProductId()).orElseThrow(() -> new EntityNotFoundException("Product not found")); @@ -167,117 +186,132 @@ public ChangeStatusResponseDto changeStatus(ChangeStatusRequestDto req) { TradeStatus status = product.getStatus(); - switch (status) { - case ON_SALE -> { + Transaction transaction = null; - Transaction transaction = Transaction.builder() + switch (status) { + case ON_SALE -> { // ํ˜„์žฌ: ํŒ๋งค์ค‘ + // ํŒ๋งค์ค‘ -> ์˜ˆ์•ฝ์ค‘ or ๊ฑฐ๋ž˜์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ ์‹œ ์ƒˆ๋กœ์šด transaction ์ƒ์„ฑ + transaction = Transaction.builder() .product(product) .seller(user) .build(); + // ์š”์ฒญ๋œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ product.setStatus(req.getStatus()); + // ์š”์ฒญ์— ๊ตฌ๋งค์ž๊ฐ€ ์ง€์ •๋œ ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜์— ๊ตฌ๋งค์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ if (req.getBuyerId() != null) { transaction.setBuyer(userRepository.findById(req.getBuyerId()).orElseThrow(() -> new EntityNotFoundException("Buyer not found"))); } + // ๊ฑฐ๋ž˜ ์™„๋ฃŒ(SOLD)๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ ์™„๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • if (req.getStatus().equals(TradeStatus.SOLD) && req.getCompletedAt() != null) { transaction.setCompletedAt(req.getCompletedAt()); } transactionRepository.save(transaction); - - return ChangeStatusResponseDto.builder() - .productId(transaction.getProduct().getId()) - .sellerId(transaction.getSeller().getId()) - .buyerId(transaction.getBuyer().getId()) - .status(product.getStatus()) - .completedAt(transaction.getCompletedAt()) - .build(); } - case RESERVED -> { + case RESERVED -> { // ํ˜„์žฌ: ์˜ˆ์•ฝ์ค‘ - Transaction transaction = transactionRepository.findById(req.getProductId()).orElseThrow(() -> new EntityNotFoundException("Transaction not found")); + // product ID๋กœ ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ + transaction = transactionRepository.findByProductId(req.getProductId()).orElseThrow(() -> new EntityNotFoundException("Transaction not found")); + log.info("[ProductService.changeStatus] transaction = {}", transaction); if (req.getStatus().equals(TradeStatus.ON_SALE)) { + // ํŒ๋งค์ค‘์œผ๋กœ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ transactionRepository.delete(transaction); } else if (req.getStatus().equals(TradeStatus.SOLD)) { - if (req.getBuyerId() == null) { - transaction.setBuyer(null); - } else { - transaction.setBuyer(userRepository.findById(req.getBuyerId()).orElseThrow(() -> new EntityNotFoundException("Buyer not found"))); + // ๊ฑฐ๋ž˜์™„๋ฃŒ๋กœ ๋ณ€๊ฒฝ ์‹œ ๊ตฌ๋งค์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ + transaction.setBuyer(userRepository.findById(req.getBuyerId()).orElseThrow(() -> new EntityNotFoundException("Buyer not found"))); + + // ๊ฑฐ๋ž˜ ์™„๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • + if (req.getCompletedAt() != null) { + transaction.setCompletedAt(req.getCompletedAt()); } } product.setStatus(req.getStatus()); - - return ChangeStatusResponseDto.builder() - .productId(transaction.getProduct().getId()) - .sellerId(transaction.getSeller().getId()) - .buyerId(transaction.getBuyer().getId()) - .status(product.getStatus()) - .completedAt(transaction.getCompletedAt()) - .build(); } - case SOLD -> { - Transaction transaction = transactionRepository.findById(req.getProductId()).orElseThrow(() -> new EntityNotFoundException("Transaction not found")); + case SOLD -> { // ํ˜„์žฌ: ๊ฑฐ๋ž˜์™„๋ฃŒ + // ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ์กฐํšŒ + transaction = transactionRepository.findByProductId(req.getProductId()).orElseThrow(() -> new EntityNotFoundException("Transaction not found")); + + // ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜์— ๋ฆฌ๋ทฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ์ฒ˜๋ฆฌ + if (reviewRepository.existsByTransactionId(transaction.getId())) { + throw new RuntimeException("๋ฆฌ๋ทฐ๊ฐ€ ์ž‘์„ฑ๋œ ํŠธ๋žœ์žญ์…˜์€ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€"); + } if (req.getStatus().equals(TradeStatus.ON_SALE)) { + + // ํŒ๋งค์ค‘์œผ๋กœ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ transactionRepository.delete(transaction); - //TODO ๋‹น๊ทผ์—์„œ๋Š” ๋ฆฌ๋ทฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ฆฌ๋ทฐ ์‚ญ์ œ ๊ฒฝ๊ณ ๊ฐ€ ๋œธ. ์–ด๋–ป๊ฒŒ ํ• ์ง€ ๊ฒฐ์ • ํ•„์š” - // ๋‹น๊ทผ์ฒ˜๋Ÿผ ํ•˜๋ ค๋ฉด review ์œ ๋ฌด ํ™•์ธ ํ›„ ํ”„๋ก ํŠธ์™€ ํ•œ์ฐจ๋ก€ ๋” ํ†ต์‹  ํ•„์š” -// if (transaction.getBuyer() != null) { -// Review review = reviewRepository.findBySellerIdAndBuyerId(); -// -// if (review != null) { -// return ~~~ -// } -// } - - product.setStatus(req.getStatus()); - - return ChangeStatusResponseDto.builder() - .productId(transaction.getProduct().getId()) - .sellerId(transaction.getSeller().getId()) - .buyerId(transaction.getBuyer().getId()) - .status(product.getStatus()) - .completedAt(transaction.getCompletedAt()) - .build(); + } else if (req.getStatus().equals(TradeStatus.RESERVED)) { + // ๊ตฌ๋งค์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ + transaction.setBuyer(userRepository.findById(req.getBuyerId()).orElseThrow(() -> new EntityNotFoundException("Buyer not found"))); } + // ๊ธฐ์กด ๊ฑฐ๋ž˜ ์™„๋ฃŒ ์‹œ๊ฐ ์‚ญ์ œ + transaction.setCompletedAt(null); + + product.setStatus(req.getStatus()); } default -> throw new RuntimeException("Invalid status [" + status + "]"); } - return null; + return ChangeStatusResponseDto.builder() + .productId(transaction.getProduct().getId()) + .sellerId(transaction.getSeller().getId()) + .buyerId(transaction.getBuyer().getId()) + .status(product.getStatus()) + .completedAt(transaction.getCompletedAt()) + .build(); + } + /** + * ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  ์กฐํšŒ์ˆ˜ 1 ์ฆ๊ฐ€ + * + * @param id ์กฐํšŒํ•  ์ƒํ’ˆ์˜ ID + * @return ์กฐํšŒ๋œ Product ์—”ํ‹ฐํ‹ฐ + * @throws EntityNotFoundException ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒ + */ @Transactional public Product getProductDetail(int id) { + // ๋ชจ๋“  ์—ฐ๊ด€ ๊ด€๊ณ„ entity๋“ค์„ ํ•œ ๋ฒˆ์— fetchํ•˜์—ฌ N+1 ๋ฌธ์ œ ๋ฐฉ์ง€ Product product = productRepository.findByIdWithAllRelations(id).orElseThrow(() -> new EntityNotFoundException("[ProductService.getProductDetail] Product not found")); + // ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ product.increaseViewCount(); return product; } + /** + * ์นดํ…Œ๊ณ ๋ฆฌ ๋˜๋Š” ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ฐœ(Favorite) ์ƒํƒœ๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param userId ์‚ฌ์šฉ์ž ID + * @param type ์ฐœํ•˜๊ธฐ ๋Œ€์ƒ ํƒ€์ž… ('C' - Category, 'P' - Product) + * @param targetId ๋Œ€์ƒ ์—”ํ‹ฐํ‹ฐ์˜ ID (Category ID ๋˜๋Š” Product ID) + * @throws EntityNotFoundException ์‚ฌ์šฉ์ž, ์นดํ…Œ๊ณ ๋ฆฌ ๋˜๋Š” ์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒ + */ @Transactional public void toggleFavorite(int userId, String type, int targetId) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("[ProductService.toggleFavorite] User not found")); switch (type.toUpperCase()) { - case "C" -> { + case "C" -> { // ์นดํ…Œ๊ณ ๋ฆฌ ์ฐœํ•˜๊ธฐ Category category = categoryRepository.findById(targetId).orElseThrow(() -> new EntityNotFoundException("[ProductService.toggleFavorite] Category not found")); Favorite favorite = favoriteRepository.findByUserIdAndCategoryId(userId, category.getId()); + // ์ฐœํ•˜๊ธฐ ๋“ฑ๋ก if (favorite == null) { favoriteRepository.save(Favorite.builder() .category(category) @@ -285,15 +319,17 @@ public void toggleFavorite(int userId, String type, int targetId) { .build()); } else { + // ์ฐœํ•˜๊ธฐ ์ทจ์†Œ favoriteRepository.delete(favorite); } } - case "P" -> { + case "P" -> { // ์ƒํ’ˆ ์ฐœํ•˜๊ธฐ Product product = productRepository.findById(targetId).orElseThrow(() -> new EntityNotFoundException("[ProductService.toggleFavorite] Product not found")); Favorite favorite = favoriteRepository.findByUserIdAndProductId(userId, targetId); + // ์ฐœํ•˜๊ธฐ ๋“ฑ๋ก + count ์ฆ๊ฐ€ if (favorite == null) { favoriteRepository.save(Favorite.builder() .product(product) @@ -302,6 +338,7 @@ public void toggleFavorite(int userId, String type, int targetId) { product.increaseFavoriteCount(); } else { + // ์ฐœํ•˜๊ธฐ ์ทจ์†Œ + count ๊ฐ์†Œ favoriteRepository.delete(favorite); product.decreaseFavoriteCount(); } @@ -311,18 +348,37 @@ public void toggleFavorite(int userId, String type, int targetId) { } } + /** + * ํŠน์ • ์‚ฌ์šฉ์ž์˜ ํ™œ์„ฑ ๋…ธ์ถœ ์ง€์—ญ์„ ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ํ•ด๋‹น ์ง€์—ญ์— ๋…ธ์ถœ ์„ค์ •๋œ ์ƒํ’ˆ ๋ชฉ๋ก DTO + * @throws EntityNotFoundException ํ™œ์„ฑ ์‚ฌ์šฉ์ž ์ง€์—ญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒ + */ @Transactional - public List getProductList(int userId) { - UserRegion userRegion = userRegionRepository.findActiveByUserId(userId) - .orElseThrow(() -> new EntityNotFoundException("[ProductService.getProductList] UserRegion not found")); - return productRepository.findAllListItemByExposureRegion(userRegion.getRegion()); + public List getProductList(int userId) { + + // ์‚ฌ์šฉ์ž ID๋กœ ํ™œ์„ฑ(Active) ์ง€์—ญ ์ •๋ณด๋ฅผ ์กฐํšŒ + UserRegion userRegion = userRegionRepository.findActiveByUserId(userId).orElseThrow(() -> new EntityNotFoundException("[ProductService.getProductList] UserRegion not found")); + + // ํ•ด๋‹น ์ง€์—ญ์— ๋…ธ์ถœํ•˜๋„๋ก ์„ค์ •๋œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ + return productRepository.findAllDtoByExposureRegion(userRegion.getRegion()); } + /** + * ์ƒํ’ˆ ์ œ๋ชฉ์„ ๊ธฐ์ค€์œผ๋กœ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ํŽ˜์ด์ง€๋„ค์ด์…˜๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param keyword ๊ฒ€์ƒ‰ํ•  ํ‚ค์›Œ๋“œ + * @param pageable ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ •๋ณด (ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ) + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ด๊ธด ํŽ˜์ด์ง€ DTO + */ @Transactional(readOnly = true) public Page searchProduct(String keyword, Pageable pageable) { + // ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ํŽ˜์ด์ง€ ๋ฐ˜ํ™˜ if (keyword == null || keyword.trim().isEmpty()) { - return Page.empty(pageable); // ๋นˆ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ + return Page.empty(pageable); } + // ์ œ๋ชฉ์— ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜์—ฌ ์กฐํšŒ return productRepository.findAllByTitleContaining(keyword, pageable); } diff --git a/src/main/java/com/mrokga/carrot_server/product/service/ReviewService.java b/src/main/java/com/mrokga/carrot_server/product/service/ReviewService.java new file mode 100644 index 0000000..0bdf4c6 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/product/service/ReviewService.java @@ -0,0 +1,113 @@ +// package com.mrokga.carrot_server.product.service; +package com.mrokga.carrot_server.product.service; + +import com.mrokga.carrot_server.product.dto.request.ReviewCreateRequest; +import com.mrokga.carrot_server.product.dto.response.ReviewResponse; +import com.mrokga.carrot_server.product.entity.Product; +import com.mrokga.carrot_server.product.entity.Review; +import com.mrokga.carrot_server.product.enums.TradeStatus; +import com.mrokga.carrot_server.product.repository.ProductRepository; +import com.mrokga.carrot_server.product.repository.ReviewRepository; +import com.mrokga.carrot_server.user.entity.User; +import com.mrokga.carrot_server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final ProductRepository productRepository; + private final UserRepository userRepository; + // private final ProductTransactionRepository txRepository; // ์‹ค์ œ ์‚ฌ์šฉ ์ค‘์ธ ๊ฑฐ๋ž˜ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋กœ ๊ต์ฒด + + private double deltaByRating(int rating) { + return switch (rating) { + case 1 -> -2.0; + case 2 -> -1.0; + case 3 -> 0.0; + case 4 -> +1.0; + case 5 -> +2.0; + default -> throw new IllegalArgumentException("rating must be 1~5"); + }; + } + + private double clamp(double v, double min, double max) { + return Math.max(min, Math.min(max, v)); + } + + @Transactional + public ReviewResponse createReview(int meId, ReviewCreateRequest req) { + if (req.getRating() == null || req.getRating() < 1 || req.getRating() > 5) + throw new IllegalArgumentException("๋ณ„์ ์€ 1~5์ž…๋‹ˆ๋‹ค."); + if (reviewRepository.existsByTransactionId(req.getTransactionId())) + throw new IllegalStateException("์ด๋ฏธ ํ•ด๋‹น ๊ฑฐ๋ž˜์— ๋Œ€ํ•œ ํ›„๊ธฐ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."); + + // 1) ๊ฑฐ๋ž˜/์ƒํ’ˆ ํ™•์ธ (์‹ค์ œ ๊ตฌํ˜„์— ๋งž๊ฒŒ ๋ฐ”๊พธ์„ธ์š”) + Product product = productRepository.findById(req.getProductId()).orElseThrow(); + // ProductTransaction tx = txRepository.findById(req.getTransactionId()).orElseThrow(); + + // ๊ฐ€์ •: meId == tx.getBuyer().getId() + // if (!tx.getBuyer().getId().equals(meId)) throw new IllegalStateException("๊ตฌ๋งค์ž๋งŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); + // if (tx.getStatus() != TradeStatus.SOLD) throw new IllegalStateException("๊ฑฐ๋ž˜์™„๋ฃŒ ํ›„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); + + // ๊ฑฐ๋ž˜ ํ…Œ์ด๋ธ”์ด ์—†๋‹ค๋ฉด: ์ƒํ’ˆ์ด SOLD์ธ์ง€์™€, meId๊ฐ€ ์‹ค์ œ ๊ตฌ๋งค์ž์ธ์ง€ ๊ฒ€์‚ฌํ•˜๋Š” ๋ณ„๋„ ๋กœ์ง ํ•„์š” + if (product.getStatus() != TradeStatus.SOLD) throw new IllegalStateException("๊ฑฐ๋ž˜์™„๋ฃŒ ํ›„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); + + User buyer = userRepository.findById(meId).orElseThrow(); + User seller = product.getUser(); // ์ƒํ’ˆ์˜ ํŒ๋งค์ž + + // 2) ์ €์žฅ + Review saved = reviewRepository.save( + Review.builder() + .transactionId(req.getTransactionId()) + .productId(product.getId()) + .buyer(buyer) + .seller(seller) + .rating(req.getRating()) + .content(req.getContent()) + .build() + ); + + // 3) ๋งค๋„ˆ์ ์ˆ˜ ๋ฐ˜์˜ + double delta = deltaByRating(req.getRating()); + double next = clamp((seller.getMannerTemperature() == null ? 36.5 : seller.getMannerTemperature()) + delta, 0.0, 100.0); + seller.setMannerTemperature(next); + userRepository.save(seller); + + return toResponse(saved); + } + + @Transactional(readOnly = true) + public Page listReceived(Integer sellerId, Pageable pg) { + return reviewRepository.findBySeller_Id(sellerId, pg).map(this::toResponse); + } + + @Transactional(readOnly = true) + public Page listWritten(Integer buyerId, Pageable pg) { + return reviewRepository.findByBuyer_Id(buyerId, pg).map(this::toResponse); + } + + @Transactional(readOnly = true) + public Page listByProduct(Integer productId, Pageable pg) { + return reviewRepository.findByProductId(productId, pg).map(this::toResponse); + } + + private ReviewResponse toResponse(Review r) { + return ReviewResponse.builder() + .id(r.getId()) + .transactionId(r.getTransactionId()) + .productId(r.getProductId()) + .buyerId(r.getBuyer().getId()) + .buyerNickname(r.getBuyer().getNickname()) + .sellerId(r.getSeller().getId()) + .sellerNickname(r.getSeller().getNickname()) + .rating(r.getRating()) + .content(r.getContent()) + .createdAt(r.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/mrokga/carrot_server/region/controller/MyRegionController.java b/src/main/java/com/mrokga/carrot_server/region/controller/MyRegionController.java new file mode 100644 index 0000000..4b32e3b --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/region/controller/MyRegionController.java @@ -0,0 +1,33 @@ +package com.mrokga.carrot_server.region.controller; + +import com.mrokga.carrot_server.api.dto.ApiResponseDto; +import com.mrokga.carrot_server.region.dto.ChangeRegionRequest; +import com.mrokga.carrot_server.region.dto.UserRegionResponse; +import com.mrokga.carrot_server.region.service.UserRegionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/me/region") +@Tag(name = "My Region API", description = "๋‚ด ๋™๋„ค ๊ด€๋ฆฌ API") +public class MyRegionController { + + private final UserRegionService userRegionService; + + @PutMapping + @Operation(summary = "๋Œ€ํ‘œ ๋™๋„ค ๋ณ€๊ฒฝ", description = "์‚ฌ์šฉ์ž์˜ ๋Œ€ํ‘œ(๊ธฐ๋ณธ) ๋™๋„ค๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.") + public ResponseEntity> changeMyRegion( + @RequestParam Integer userId, // โœ… ์‹ค์ œ ์šด์˜์—์„  SecurityContext์—์„œ ๊บผ๋‚ด์„ธ์š” + @RequestBody ChangeRegionRequest request + ){ + UserRegionResponse result = userRegionService.changePrimaryRegion(userId, request); + return ResponseEntity.ok( + ApiResponseDto.success(HttpStatus.OK.value(), "๋™๋„ค๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", result) + ); + } +} diff --git a/src/main/java/com/mrokga/carrot_server/region/dto/ChangeRegionRequest.java b/src/main/java/com/mrokga/carrot_server/region/dto/ChangeRegionRequest.java new file mode 100644 index 0000000..53b29ad --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/region/dto/ChangeRegionRequest.java @@ -0,0 +1,11 @@ +package com.mrokga.carrot_server.region.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class ChangeRegionRequest { + @Schema(description = "๋ณ€๊ฒฝํ•  ๋™๋„ค์˜ ํ’€๋„ค์ž„", example = "์„œ์šธ ๋™์ž‘๊ตฌ ๋Œ€๋ฐฉ๋™") + private String regionFullName; +} \ No newline at end of file diff --git a/src/main/java/com/mrokga/carrot_server/region/dto/UserRegionResponse.java b/src/main/java/com/mrokga/carrot_server/region/dto/UserRegionResponse.java new file mode 100644 index 0000000..71d185a --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/region/dto/UserRegionResponse.java @@ -0,0 +1,27 @@ +package com.mrokga.carrot_server.region.dto; + +import com.mrokga.carrot_server.region.entity.Region; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +public class UserRegionResponse { + private Integer regionId; + private String regionName; + private String regionFullName; + private Boolean isPrimary; + private Boolean isActive; + private LocalDateTime verifiedAt; + + public static UserRegionResponse of(Region r, boolean primary, boolean active, LocalDateTime verifiedAt){ + return UserRegionResponse.builder() + .regionId(r.getId()) + .regionName(r.getName()) + .regionFullName(r.getFullName()) + .isPrimary(primary) + .isActive(active) + .verifiedAt(verifiedAt) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mrokga/carrot_server/region/repository/UserRegionRepository.java b/src/main/java/com/mrokga/carrot_server/region/repository/UserRegionRepository.java index 2d3fe36..fef463e 100644 --- a/src/main/java/com/mrokga/carrot_server/region/repository/UserRegionRepository.java +++ b/src/main/java/com/mrokga/carrot_server/region/repository/UserRegionRepository.java @@ -12,8 +12,21 @@ @Repository public interface UserRegionRepository extends JpaRepository { - Optional findActiveByUserId(int userId); + // ํ™œ์„ฑํ™”๋œ ๋™๋„ค(ํ•„์š”์‹œ) + Optional findActiveByUserId(Integer userId); + // โœ… ํ˜„์žฌ ๋Œ€ํ‘œ ๋™๋„ค + @Query(""" + select ur + from UserRegion ur + where ur.user.id = :userId and ur.isPrimary = true + """) + Optional findPrimaryByUserId(@Param("userId") Integer userId); + + // โœ… ์‚ฌ์šฉ์ž + ์ง€์—ญ ๋งคํ•‘ ์กด์žฌ ์—ฌ๋ถ€ + Optional findByUserIdAndRegion_Id(Integer userId, Integer regionId); + + // ๋ชฉ๋ก ์กฐํšŒ (์ง€์—ญ join ํฌํ•จ) @Query(""" select ur from UserRegion ur diff --git a/src/main/java/com/mrokga/carrot_server/region/service/UserRegionService.java b/src/main/java/com/mrokga/carrot_server/region/service/UserRegionService.java new file mode 100644 index 0000000..7fa262e --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/region/service/UserRegionService.java @@ -0,0 +1,59 @@ +package com.mrokga.carrot_server.region.service; + +import com.mrokga.carrot_server.region.dto.ChangeRegionRequest; +import com.mrokga.carrot_server.region.dto.UserRegionResponse; +import com.mrokga.carrot_server.region.entity.Region; +import com.mrokga.carrot_server.region.entity.UserRegion; +import com.mrokga.carrot_server.region.repository.RegionRepository; +import com.mrokga.carrot_server.region.repository.UserRegionRepository; +import com.mrokga.carrot_server.user.entity.User; +import com.mrokga.carrot_server.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class UserRegionService { + + private final RegionRepository regionRepository; + private final UserRegionRepository userRegionRepository; + private final UserService userService; // ํ˜„์žฌ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์กฐํšŒ์šฉ + + /** ๋Œ€ํ‘œ ๋™๋„ค ๋ณ€๊ฒฝ (์—†์œผ๋ฉด ์ƒ์„ฑ-ํ™œ์„ฑํ™”) */ + @Transactional + public UserRegionResponse changePrimaryRegion(Integer userId, ChangeRegionRequest req){ + // 1) ์œ ์ €/์ง€์—ญ ์กฐํšŒ + User user = userService.getUserById(userId); + Region region = regionRepository.findByFullName(req.getRegionFullName()) + .orElseGet(() -> regionRepository.findByName(req.getRegionFullName()) + .orElseThrow(() -> new IllegalArgumentException("ํ•ด๋‹น ๋™๋„ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."))); + + // 2) ๊ธฐ์กด ๋Œ€ํ‘œ ํ•ด์ œ + userRegionRepository.findPrimaryByUserId(userId).ifPresent(ur -> { + ur.setIsPrimary(false); + ur.setUpdatedAt(LocalDateTime.now()); + }); + + // 3) ๊ธฐ์กด ๋งคํ•‘ ์žˆ์œผ๋ฉด ํ™œ์„ฑ/๋Œ€ํ‘œ๋กœ, ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + UserRegion ur = userRegionRepository.findByUserIdAndRegion_Id(userId, region.getId()) + .orElse(UserRegion.builder() + .user(user) + .region(region) + .isActive(true) + .isPrimary(true) + .verifiedAt(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .build()); + + ur.setIsActive(true); + ur.setIsPrimary(true); + if(ur.getVerifiedAt() == null) ur.setVerifiedAt(LocalDateTime.now()); + ur.setUpdatedAt(LocalDateTime.now()); + userRegionRepository.save(ur); + + return UserRegionResponse.of(region, true, true, ur.getVerifiedAt()); + } +} diff --git a/src/main/java/com/mrokga/carrot_server/transaction/repository/TransactionRepository.java b/src/main/java/com/mrokga/carrot_server/transaction/repository/TransactionRepository.java index 37cde76..f29fce6 100644 --- a/src/main/java/com/mrokga/carrot_server/transaction/repository/TransactionRepository.java +++ b/src/main/java/com/mrokga/carrot_server/transaction/repository/TransactionRepository.java @@ -9,6 +9,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface TransactionRepository extends JpaRepository { @@ -32,4 +34,6 @@ public interface TransactionRepository extends JpaRepository findPurchasedItemsByBuyerId(Integer buyerId, Pageable pageable); + + Optional findByProductId(Integer productId); } diff --git a/src/main/java/com/mrokga/carrot_server/transaction/service/TransactionService.java b/src/main/java/com/mrokga/carrot_server/transaction/service/TransactionService.java new file mode 100644 index 0000000..966b3dd --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/transaction/service/TransactionService.java @@ -0,0 +1,94 @@ +// src/main/java/com/mrokga/carrot_server/transaction/service/TransactionService.java +package com.mrokga.carrot_server.transaction.service; + +import com.mrokga.carrot_server.product.entity.Product; +import com.mrokga.carrot_server.product.enums.TradeStatus; +import com.mrokga.carrot_server.product.repository.ProductRepository; +import com.mrokga.carrot_server.transaction.entity.Transaction; +import com.mrokga.carrot_server.transaction.repository.TransactionRepository; +import com.mrokga.carrot_server.user.entity.User; +import com.mrokga.carrot_server.user.repository.UserRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class TransactionService { + + private final TransactionRepository txRepo; + private final ProductRepository productRepo; + private final UserRepository userRepo; + + /** ๊ฑฐ๋ž˜ ์‹œ์ž‘: ์ƒํ’ˆ์„ ์˜ˆ์•ฝ์ค‘์œผ๋กœ ๋งŒ๋“ค๊ณ  ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ */ + @Transactional + public Transaction start(Integer buyerId, Integer productId) { + User buyer = userRepo.findById(buyerId) + .orElseThrow(() -> new EntityNotFoundException("buyer not found")); + Product product = productRepo.findById(productId) + .orElseThrow(() -> new EntityNotFoundException("product not found")); + User seller = product.getUser(); + + if (product.getStatus() != TradeStatus.ON_SALE) { + throw new IllegalStateException("์ƒํ’ˆ์ด ํŒ๋งค์ค‘ ์ƒํƒœ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค."); + } + if (seller.getId().equals(buyerId)) { + throw new IllegalStateException("๋ณธ์ธ ์ƒํ’ˆ์€ ๊ตฌ๋งคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + product.setStatus(TradeStatus.RESERVED); + productRepo.save(product); + + Transaction tx = Transaction.builder() + .product(product) + .buyer(buyer) + .seller(seller) + .completedAt(null) // ์˜ˆ์•ฝ ์ƒํƒœ + .build(); + return txRepo.save(tx); + } + + /** ๊ฒฐ์ œ ์Šน์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ: ์ƒํ’ˆ ํŒ๋งค์™„๋ฃŒ + ๊ฑฐ๋ž˜ ์™„๋ฃŒ์‹œ๊ฐ„ ๊ธฐ๋ก */ + @Transactional + public Transaction approve(Integer txId) { + Transaction tx = txRepo.findById(txId) + .orElseThrow(() -> new EntityNotFoundException("transaction not found")); + + if (tx.getCompletedAt() != null) return tx; // ๋ฉฑ๋“ฑ์„ฑ + + Product p = tx.getProduct(); + p.setStatus(TradeStatus.SOLD); + productRepo.save(p); + + tx.setCompletedAt(LocalDateTime.now()); + return txRepo.save(tx); + } + + /** ๊ฒฐ์ œ ์‹คํŒจ/์ทจ์†Œ: ์ƒํ’ˆ ๋‹ค์‹œ ํŒ๋งค์ค‘์œผ๋กœ */ + @Transactional + public Transaction cancel(Integer txId) { + Transaction tx = txRepo.findById(txId) + .orElseThrow(() -> new EntityNotFoundException("transaction not found")); + + // ์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ฑฐ๋ž˜๋Š” ์ทจ์†Œ ๋ถˆ๊ฐ€(ํ•„์š” ์‹œ ์˜ˆ์™ธ์ •์ฑ… ๋ณ€๊ฒฝ) + if (tx.getCompletedAt() != null) { + throw new IllegalStateException("์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ฑฐ๋ž˜์ž…๋‹ˆ๋‹ค."); + } + + Product p = tx.getProduct(); + p.setStatus(TradeStatus.ON_SALE); + productRepo.save(p); + + // completedAt ์€ ๊ทธ๋Œ€๋กœ null ์œ ์ง€(์˜ˆ์•ฝ ์ทจ์†Œ) + return txRepo.save(tx); + } + + @Transactional(readOnly = true) + public Transaction get(Integer txId) { + return txRepo.findById(txId) + .orElseThrow(() -> new EntityNotFoundException("transaction not found")); + } +} diff --git a/src/main/java/com/mrokga/carrot_server/transaction/web/TransactionController.java b/src/main/java/com/mrokga/carrot_server/transaction/web/TransactionController.java new file mode 100644 index 0000000..dd3e219 --- /dev/null +++ b/src/main/java/com/mrokga/carrot_server/transaction/web/TransactionController.java @@ -0,0 +1,71 @@ +// src/main/java/com/mrokga/carrot_server/transaction/web/TransactionController.java +package com.mrokga.carrot_server.transaction.web; + +import com.mrokga.carrot_server.transaction.entity.Transaction; +import com.mrokga.carrot_server.transaction.service.TransactionService; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; // โ˜… ์ถ”๊ฐ€ +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/transactions") +public class TransactionController { + + private final TransactionService txService; + + /** ๊ฑฐ๋ž˜ ์‹œ์ž‘(์˜ˆ์•ฝ) */ + @PostMapping + public ResponseEntity start(@AuthenticationPrincipal UserDetails principal, + @RequestBody StartReq req) { + if (principal == null) { + return ResponseEntity.status(401).body("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + + // TokenProvider.generateToken ์—์„œ subject = user.getId() + Integer buyerId = Integer.valueOf(principal.getUsername()); + + Transaction tx = txService.start(buyerId, req.getProductId()); + return ResponseEntity.ok(new TxRes(tx)); + } + + /** ๋‹จ๊ฑด ์กฐํšŒ(๋””๋ฒ„๊ทธ/ํ™•์ธ์šฉ) */ + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable Integer id) { + Transaction tx = txService.get(id); + return ResponseEntity.ok(new TxRes(tx)); + } + + /** ์˜ˆ์•ฝ ์ทจ์†Œ(๋””๋ฒ„๊ทธ/์ˆ˜๋™์ทจ์†Œ์šฉ) */ + @PostMapping("/{id}/cancel") + public ResponseEntity cancel(@PathVariable Integer id) { + Transaction tx = txService.cancel(id); + return ResponseEntity.ok(new TxRes(tx)); + } + + @Data + public static class StartReq { @NotNull private Integer productId; } + + @Data + public static class TxRes { + private Integer id; + private Integer productId; + private Integer buyerId; + private Integer sellerId; + private String productStatus; + private String completedAt; + + public TxRes(Transaction t) { + this.id = t.getId(); + this.productId = t.getProduct().getId(); + this.buyerId = t.getBuyer() != null ? t.getBuyer().getId() : null; + this.sellerId = t.getSeller() != null ? t.getSeller().getId() : null; + this.productStatus = t.getProduct().getStatus().name(); + this.completedAt = t.getCompletedAt() == null ? null : t.getCompletedAt().toString(); + } + } +} diff --git a/src/main/java/com/mrokga/carrot_server/user/repository/UserRepository.java b/src/main/java/com/mrokga/carrot_server/user/repository/UserRepository.java index 372273c..af82382 100644 --- a/src/main/java/com/mrokga/carrot_server/user/repository/UserRepository.java +++ b/src/main/java/com/mrokga/carrot_server/user/repository/UserRepository.java @@ -8,4 +8,6 @@ public interface UserRepository extends JpaRepository { User findByNickname(String nickname); User findByPhoneNumber(String phoneNumber); + + } diff --git a/src/main/java/com/mrokga/carrot_server/user/service/UserService.java b/src/main/java/com/mrokga/carrot_server/user/service/UserService.java index b976744..d5473d7 100644 --- a/src/main/java/com/mrokga/carrot_server/user/service/UserService.java +++ b/src/main/java/com/mrokga/carrot_server/user/service/UserService.java @@ -28,8 +28,16 @@ public class UserService { @Value("${default-profile-image-url}") private String defaultProfileImageUrl; + /** + * ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž๋ฅผ ๋“ฑ๋กํ•˜๊ณ , ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ดˆ๊ธฐ ์ง€์—ญ ์ •๋ณด ์„ค์ • + * + * @param dto ํšŒ์›๊ฐ€์ž… ์š”์ฒญ ๋ฐ์ดํ„ฐ DTO + * @return ์ €์žฅ๋œ User ์—”ํ‹ฐํ‹ฐ + * @throws EntityNotFoundException ์ง€์—ญ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒ + */ @Transactional public User signup(SignupRequestDto dto) { + // 1. User ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๋ฐ ์ €์žฅ User user = User.builder() .phoneNumber(dto.getPhoneNumber()) .nickname(dto.getNickname()) @@ -39,9 +47,11 @@ public User signup(SignupRequestDto dto) { userRepository.save(user); + // 2. ์ง€์—ญ ์ •๋ณด ์กฐํšŒ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ Region region = regionRepository.findByFullName(dto.getRegion()).orElseThrow(() -> new EntityNotFoundException("[UserService.signup] Region not found")); // List userRegionList = userRegionRepository.findAllByUserId(user.getId()); + // 3. UserRegion ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๋ฐ ์ €์žฅ UserRegion userRegion = UserRegion.builder() .user(user) .region(region) @@ -56,13 +66,31 @@ public User signup(SignupRequestDto dto) { return user; } + /** + * ๋‹‰๋„ค์ž„ ์ค‘๋ณต ์—ฌ๋ถ€ ๋ฆฌํ„ด + * + * @param nickname ํ™•์ธํ•  ๋‹‰๋„ค์ž„ + * @return ๋‹‰๋„ค์ž„์ด ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ public boolean isDuplicateNickname(String nickname) { User user = userRepository.findByNickname(nickname); return user != null; } + /** + * ์ „ํ™”๋ฒˆํ˜ธ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ + * + * @param phoneNumber ์กฐํšŒํ•  ์ „ํ™”๋ฒˆํ˜ธ + * @return ์กฐํšŒ๋œ User ์—”ํ‹ฐํ‹ฐ, ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜ + */ public User getUserByPhoneNumber(String phoneNumber) { return userRepository.findByPhoneNumber(phoneNumber); } + + @Transactional(readOnly = true) + public User getUserById(Integer userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId)); + } }