From 8dfec947f8ddb0616fa6ef64dbe59329579b390a Mon Sep 17 00:00:00 2001 From: hgkim Date: Thu, 26 Jun 2025 09:05:01 +0900 Subject: [PATCH 01/16] feat: implement user block functionality with API endpoints and caching Signed-off-by: hgkim --- BLOCK_FEATURE_USAGE.md | 241 ++++++ USER_BLOCK_IMPLEMENTATION.md | 687 ++++++++++++++++++ git.environment-variables | 2 +- ...352\263\204_\354\203\235\354\204\261.http" | 31 + ...353\266\200_\355\231\225\354\235\270.http" | 15 + ...353\241\235_\354\241\260\355\232\214.http" | 15 + ...352\263\204_\354\241\260\355\232\214.http" | 15 + ...352\263\204_\355\225\264\354\240\234.http" | 15 + ...\355\205\214\354\212\244\355\212\270.http" | 47 ++ ...\355\205\214\354\212\244\355\212\270.http" | 6 + http/block/README.md | 107 +++ .../global/annotation/BlockFilter.java | 26 + .../global/aspect/BlockFilterAspect.java | 228 ++++++ .../global/cache/local/LocalCacheType.java | 3 +- .../controller/BlockTestController.java | 248 +++++++ .../global/security/SecurityContextUtil.java | 12 + .../global/service/BlockService.java | 262 +++++++ .../review/controller/ReviewController.java | 3 + .../app/bottlenote/user/domain/UserBlock.java | 70 ++ .../user/domain/UserBlockRepository.java | 71 ++ .../repository/JpaUserBlockRepository.java | 37 + 21 files changed, 2139 insertions(+), 2 deletions(-) create mode 100644 BLOCK_FEATURE_USAGE.md create mode 100644 USER_BLOCK_IMPLEMENTATION.md create mode 100644 "http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" create mode 100644 "http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" create mode 100644 "http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" create mode 100644 "http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" create mode 100644 "http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" create mode 100644 "http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" create mode 100644 "http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" create mode 100644 http/block/README.md create mode 100644 src/main/java/app/bottlenote/global/annotation/BlockFilter.java create mode 100644 src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java create mode 100644 src/main/java/app/bottlenote/global/controller/BlockTestController.java create mode 100644 src/main/java/app/bottlenote/global/service/BlockService.java create mode 100644 src/main/java/app/bottlenote/user/domain/UserBlock.java create mode 100644 src/main/java/app/bottlenote/user/domain/UserBlockRepository.java create mode 100644 src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java diff --git a/BLOCK_FEATURE_USAGE.md b/BLOCK_FEATURE_USAGE.md new file mode 100644 index 000000000..0f1d9e175 --- /dev/null +++ b/BLOCK_FEATURE_USAGE.md @@ -0,0 +1,241 @@ +# ๐Ÿšซ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ + +## ๐ŸŽฏ ๊ตฌํ˜„ ์™„๋ฃŒ๋œ ๊ธฐ๋Šฅ๋“ค + +### โœ… 1. BlockFilter ์–ด๋…ธํ…Œ์ด์…˜ +- **์œ„์น˜**: `app.bottlenote.global.annotation.BlockFilter` +- **์šฉ๋„**: ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์ฐจ๋‹จ ํ•„ํ„ฐ๋ง ์ ์šฉ + +### โœ… 2. BlockFilterAspect AOP +- **์œ„์น˜**: `app.bottlenote.global.aspect.BlockFilterAspect` +- **์šฉ๋„**: ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ์ž๋™ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ + +### โœ… 3. BlockService +- **์œ„์น˜**: `app.bottlenote.global.service.BlockService` +- **์šฉ๋„**: ์ฐจ๋‹จ ๊ด€๊ณ„ ๊ด€๋ฆฌ (ํ˜„์žฌ ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜) + +### โœ… 4. ReviewController ์ ์šฉ ์ƒ˜ํ”Œ +- **์ ์šฉ๋œ ๋ฉ”์„œ๋“œ**: `getReviews()`, `getDetailReview()` + +### โœ… 5. BlockTestController +- **์œ„์น˜**: `app.bottlenote.global.controller.BlockTestController` +- **์šฉ๋„**: ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +--- + +## ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• + +### 1๏ธโƒฃ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• +```java +// ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +@BlockFilter(userField = "authorId", type = BlockType.BLUR) +@GetMapping("/reviews") +public ResponseEntity getReviews() { + // ๊ธฐ์กด ๋กœ์ง ๊ทธ๋Œ€๋กœ ์œ ์ง€ +} +``` + +### 2๏ธโƒฃ ์ฐจ๋‹จ ํƒ€์ž… ์„ ํƒ +```java +// ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +@BlockFilter(type = BlockType.BLUR) + +// ์™„์ „ ์ œ์™ธ +@BlockFilter(type = BlockType.EXCLUDE) + +// ์ ‘๊ทผ ์ฐจ๋‹จ (403 ์—๋Ÿฌ) +@BlockFilter(type = BlockType.ACCESS_DENY) +``` + +### 3๏ธโƒฃ ์‚ฌ์šฉ์ž ID ํ•„๋“œ ์ง€์ • +```java +// ๊ธฐ๋ณธ๊ฐ’: authorId +@BlockFilter(userField = "authorId") + +// ๋‹ค๋ฅธ ํ•„๋“œ๋ช… ์‚ฌ์šฉ +@BlockFilter(userField = "userId") +@BlockFilter(userField = "writerId") +``` + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ• + +### 1. ์„œ๋ฒ„ ์‹คํ–‰ +```bash +./gradlew bootRun +``` + +### 2. ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ +```bash +# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จ +curl -X POST "http://localhost:8080/api/v1/test/block/add?blockerId=1&blockedId=2" +``` + +### 3. ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ +```bash +# ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ +curl "http://localhost:8080/api/v1/test/block/list/1" +``` + +### 4. ๋ฆฌ๋ทฐ ์กฐํšŒ ํ…Œ์ŠคํŠธ +```bash +# JWT ํ† ํฐ์œผ๋กœ ๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์—์„œ ๋ฆฌ๋ทฐ ์กฐํšŒ +curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + "http://localhost:8080/api/v1/reviews/1" +``` + +### 5. ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ +```bash +curl -X DELETE "http://localhost:8080/api/v1/test/block/remove?blockerId=1&blockedId=2" +``` + +--- + +## ๐Ÿ“‹ ๋™์ž‘ ํ™•์ธ ํฌ์ธํŠธ + +### โœ… ๋กœ๊ทธ ํ™•์ธ +``` +2024-01-15 10:30:00 INFO - ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ: 1 -> 2 +2024-01-15 10:31:00 DEBUG - ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ: 1 ๋ช… +2024-01-15 10:32:00 INFO - ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉ ์™„๋ฃŒ - ์ฐจ๋‹จ ๋ชฉ๋ก: 1 ๊ฐœ +``` + +### โœ… ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋ณ€ํ™” +**์ฐจ๋‹จ ์ „:** +```json +{ + "success": true, + "data": [ + { + "reviewId": 123, + "content": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "authorId": 2, + "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" + } + ] +} +``` + +**์ฐจ๋‹จ ํ›„:** +```json +{ + "success": true, + "data": [ + { + "reviewId": 123, + "content": "*** ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค ***", + "authorId": 2, + "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", + "isBlocked": true, + "blockReason": "BLOCKED_USER" + } + ] +} +``` + +--- + +## ๐Ÿ”ง ๋‹ค๋ฅธ ์ปจํŠธ๋กค๋Ÿฌ์— ์ ์šฉํ•˜๊ธฐ + +### 1. Import ์ถ”๊ฐ€ +```java +import app.bottlenote.global.annotation.BlockFilter; +import app.bottlenote.global.annotation.BlockFilter.BlockType; +``` + +### 2. ์–ด๋…ธํ…Œ์ด์…˜ ์ ์šฉ +```java +// ๋Œ“๊ธ€ ๋ชฉ๋ก ์กฐํšŒ +@BlockFilter(userField = "authorId", type = BlockType.BLUR) +@GetMapping("/comments") +public ResponseEntity getComments() { ... } + +// ํŒ”๋กœ์›Œ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ) +@BlockFilter(userField = "userId", type = BlockType.EXCLUDE) +@GetMapping("/followers") +public ResponseEntity getFollowers() { ... } + +// ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ (์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ ‘๊ทผ ๊ธˆ์ง€) +@BlockFilter(userField = "userId", type = BlockType.ACCESS_DENY) +@GetMapping("/mypage/{userId}") +public ResponseEntity getMyPage(@PathVariable Long userId) { ... } +``` + +--- + +## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ + +### 1. ํ˜„์žฌ ์ œํ•œ์‚ฌํ•ญ +- **๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ €์žฅ**: ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” +- **๋‹จ์ˆœ ๊ฐ์ฒด ๋ณต์‚ฌ**: ๋ณต์žกํ•œ ๊ฐ์ฒด ๊ตฌ์กฐ๋Š” ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ œํ•œ +- **ํ•„๋“œ๋ช… ์˜์กด**: ์‘๋‹ต DTO์˜ ํ•„๋“œ๋ช…์ด ์ •ํ™•ํ•ด์•ผ ํ•จ + +### 2. ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ +- **์ต๋ช… ์‚ฌ์šฉ์ž**: ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ ์•ˆํ•จ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +- **์บ์‹œ ์ ์šฉ**: `@Cacheable`๋กœ ๋ฐ˜๋ณต ์กฐํšŒ ์ตœ์ ํ™” +- **๋กœ๊ทธ ๋ ˆ๋ฒจ**: ์šด์˜ ์‹œ DEBUG ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™” ๊ถŒ์žฅ + +### 3. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ +- **ํด๋ผ์ด์–ธํŠธ ๊ฒ€์ฆ**: ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š” +- **API ์ ‘๊ทผ ์ œ์–ด**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ง์ ‘ API ํ˜ธ์ถœ ์ฐจ๋‹จ ๊ณ ๋ ค + +--- + +## ๐Ÿ”„ ๋‹ค์Œ ๋‹จ๊ณ„ (์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ) + +### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™ +```sql +CREATE TABLE user_block ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + blocker_id BIGINT NOT NULL, + blocked_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_blocker_blocked (blocker_id, blocked_id) +); +``` + +### 2. Redis ์บ์‹œ ์ ์šฉ +```yaml +spring: + cache: + type: redis + redis: + host: localhost + port: 6379 +``` + +### 3. ์ฐจ๋‹จ ๊ด€๋ฆฌ API ๊ฐœ๋ฐœ +- ์ฐจ๋‹จํ•˜๊ธฐ/ํ•ด์ œํ•˜๊ธฐ API +- ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ API +- ์ฐจ๋‹จ ์‚ฌ์œ  ๊ด€๋ฆฌ + +### 4. ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ +- ๋ธ”๋Ÿฌ UI ์ปดํฌ๋„ŒํŠธ +- ์ฐจ๋‹จ ๋ฒ„ํŠผ ์ถ”๊ฐ€ +- ์ฐจ๋‹จ ๋ชฉ๋ก ๊ด€๋ฆฌ ํŽ˜์ด์ง€ + +--- + +## ๐Ÿ†˜ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### Q: AOP๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์Œ +**A:** `@EnableAspectJAutoProxy` ํ™•์ธ ๋˜๋Š” Spring Boot์˜ ์ž๋™ ์„ค์ • ํ™•์ธ + +### Q: ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค๋Š” ์˜ค๋ฅ˜ +**A:** `userField` ๊ฐ’์ด ์‘๋‹ต DTO์˜ ์‹ค์ œ ํ•„๋“œ๋ช…๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ + +### Q: ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š์Œ +**A:** ๊ฐ์ฒด์˜ setter๊ฐ€ ์žˆ๋Š”์ง€, ๋˜๋Š” ํ•„๋“œ๊ฐ€ final์ด ์•„๋‹Œ์ง€ ํ™•์ธ + +### Q: ์„ฑ๋Šฅ์ด ๋А๋ฆผ +**A:** ์ฐจ๋‹จ ๋ชฉ๋ก์ด ๋งŽ์€ ๊ฒฝ์šฐ ๋ฐฐ์น˜ ์กฐํšŒ๋‚˜ ์บ์‹œ TTL ์กฐ์ • ๊ณ ๋ ค + +--- + +## ๐Ÿ“ž ์—ฐ๋ฝ์ฒ˜ + +๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋ฌธ์˜์‚ฌํ•ญ์ด๋‚˜ ๊ฐœ์„  ์ œ์•ˆ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์—ฐ๋ฝ์ฃผ์„ธ์š”! + +- ๊ธฐ์ˆ  ๋ฌธ์˜: ๊ฐœ๋ฐœํŒ€ ์ฑ„๋„ +- ๊ธฐ๋Šฅ ์ œ์•ˆ: ๊ธฐํšํŒ€ ์ฑ„๋„ \ No newline at end of file diff --git a/USER_BLOCK_IMPLEMENTATION.md b/USER_BLOCK_IMPLEMENTATION.md new file mode 100644 index 000000000..6703b3756 --- /dev/null +++ b/USER_BLOCK_IMPLEMENTATION.md @@ -0,0 +1,687 @@ +# ๐Ÿšซ Bottle Note ์œ ์ € ์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +## ๐Ÿ“‹ ๋ชฉ์ฐจ +1. [ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„](#ํ˜„์žฌ-์‹œ์Šคํ…œ-๋ถ„์„) +2. [์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ์•ˆ ๋น„๊ต](#์ฐจ๋‹จ-๊ธฐ๋Šฅ-๊ตฌํ˜„-๋ฐฉ์•ˆ-๋น„๊ต) +3. [์ถ”์ฒœ ๊ตฌํ˜„ ๋ฐฉ์•ˆ: AOP ๊ธฐ๋ฐ˜](#์ถ”์ฒœ-๊ตฌํ˜„-๋ฐฉ์•ˆ-aop-๊ธฐ๋ฐ˜) +4. [์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต](#์„ฑ๋Šฅ-์ตœ์ ํ™”-์ „๋žต) +5. [ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ ๋ฐฉ์•ˆ](#ํ”„๋ก ํŠธ์—”๋“œ-์—ฐ๋™-๋ฐฉ์•ˆ) +6. [๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ](#๊ตฌํ˜„-๋‹จ๊ณ„๋ณ„-๊ฐ€์ด๋“œ) + +--- + +## ๐Ÿ” ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„ + +### ์ฐจ๋‹จ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์กฐํšŒ ์ง€์ ๋“ค +- **๋ฆฌ๋ทฐ ๋ชฉ๋ก/์ƒ์„ธ ์กฐํšŒ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +- **๋ฆฌ๋ทฐ ๋Œ“๊ธ€ ์กฐํšŒ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋Œ“๊ธ€ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +- **ํŒ”๋กœ์›Œ/ํŒ”๋กœ์ž‰ ๋ชฉ๋ก** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ ๋˜๋Š” ๋ธ”๋Ÿฌ ํ‘œ์‹œ +- **๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ ์ œํ•œ +- **์œ ์ € ๊ฒ€์ƒ‰/์ถ”์ฒœ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ + +### ํ˜„์žฌ ์ธ์ฆ ์‹œ์Šคํ…œ +```java +// ์ต๋ช… ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ +Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); + +// ๋กœ๊ทธ์ธ ํ•„์ˆ˜ ์ฒ˜๋ฆฌ +Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new UserException(REQUIRED_USER_ID)); +``` + +--- + +## โš–๏ธ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ์•ˆ ๋น„๊ต + +### 1๏ธโƒฃ ๋ฐฉ์•ˆ 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ˆ˜์ • + +#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ +```sql +-- ๊ธฐ์กด ์ฟผ๋ฆฌ +SELECT * FROM review WHERE alcohol_id = ?; + +-- ์ˆ˜์ •๋œ ์ฟผ๋ฆฌ +SELECT r.*, + CASE WHEN ub.blocker_id IS NOT NULL THEN 1 ELSE 0 END as is_blocked +FROM review r +LEFT JOIN user_block ub ON ub.blocker_id = ? AND ub.blocked_id = r.user_id +WHERE r.alcohol_id = ?; +``` + +#### โœ… ์žฅ์  +- **๊ฐ€์žฅ ์ง๊ด€์ ์ธ ๋ฐฉ๋ฒ•** +- **DB ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ**๋กœ ํ™•์‹คํ•œ ์ฐจ๋‹จ +- **์„ฑ๋Šฅ์ด ์ข‹์Œ** (ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ํ•ด๊ฒฐ) + +#### โŒ ๋‹จ์  +- **๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•จ** (50+ ๊ฐœ์˜ ์ฟผ๋ฆฌ) +- **๊ธฐ์กด ์ฝ”๋“œ ๋Œ€๋Ÿ‰ ์ˆ˜์ •** ํ•„์š” +- **์œ ์ง€๋ณด์ˆ˜ ๋ณต์žก๋„ ์ฆ๊ฐ€** +- **ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋Œ€๋Ÿ‰ ์ˆ˜์ •** ํ•„์š” + +### 2๏ธโƒฃ ๋ฐฉ์•ˆ 2: ์„œ๋น„์Šค ๋ ˆ์ด์–ด ํ›„์ฒ˜๋ฆฌ + +#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ +```java +@Service +public class ReviewService { + + public PageResponse getReviews(Long alcoholId, ...) { + // ๊ธฐ์กด ๋กœ์ง์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ + var reviews = reviewRepository.findByAlcoholId(alcoholId); + + // ์ฐจ๋‹จ ๋กœ์ง ํ›„์ฒ˜๋ฆฌ + var filteredReviews = blockFilterService.filterBlockedContent( + reviews, currentUserId, ReviewListResponse::getAuthorId + ); + + return PageResponse.of(filteredReviews); + } +} +``` + +#### โœ… ์žฅ์  +- **๊ธฐ์กด Repository ์ฝ”๋“œ ์œ ์ง€** +- **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฐจ๋‹จ ๋กœ์ง ๋ถ„๋ฆฌ** +- **ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›€** + +#### โŒ ๋‹จ์  +- **๋ชจ๋“  ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ˆ˜์ •** ํ•„์š” +- **์ค‘๋ณต ์ฝ”๋“œ ๋ฐœ์ƒ** ๊ฐ€๋Šฅ์„ฑ +- **๊นœ๋นกํ•˜๊ธฐ ์‰ฌ์›€** (์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ) + +### 3๏ธโƒฃ ๋ฐฉ์•ˆ 3: AOP ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ โญ **์ถ”์ฒœ** + +#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ +```java +// ์ปจํŠธ๋กค๋Ÿฌ์— ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ +@BlockFilter(userField = "authorId", type = BlockType.BLUR) +@GetMapping("/{alcoholId}") +public ResponseEntity getReviews(@PathVariable Long alcoholId, ...) { + // ๊ธฐ์กด ๋กœ์ง ๊ทธ๋Œ€๋กœ ์œ ์ง€ (์ˆ˜์ • ์—†์Œ!) + return GlobalResponse.ok(reviewService.getReviews(alcoholId, request, currentUserId)); +} +``` + +#### โœ… ์žฅ์  +- **๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์ตœ์†Œํ™”** (์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€) +- **์ค‘์•™ ์ง‘์ค‘์‹ ๊ด€๋ฆฌ** (์ฐจ๋‹จ ๋กœ์ง์ด ํ•œ ๊ณณ์—) +- **๊นœ๋นกํ•  ์—ผ๋ ค ์—†์Œ** (์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋ช…์‹œ์ ) +- **์„ฑ๋Šฅ ์˜ํ–ฅ ์ตœ์†Œ** (์‘๋‹ต ํ›„์ฒ˜๋ฆฌ) +- **ํ…Œ์ŠคํŠธ ์šฉ์ด** (AOP ๋กœ์ง ๋…๋ฆฝ ํ…Œ์ŠคํŠธ) +- **ํ™•์žฅ์„ฑ ์ข‹์Œ** (๋‹ค์–‘ํ•œ ์ฐจ๋‹จ ํƒ€์ž… ์ง€์›) + +#### โŒ ๋‹จ์  +- **AOP ๊ฐœ๋… ์ดํ•ด** ํ•„์š” +- **๋””๋ฒ„๊น…์ด ์•ฝ๊ฐ„ ๋ณต์žก**ํ•  ์ˆ˜ ์žˆ์Œ + +### 4๏ธโƒฃ ๋ฐฉ์•ˆ 4: ํ”„๋ก ํŠธ์—”๋“œ ์ฒ˜๋ฆฌ + +#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ +```javascript +// ๋ฐฑ์—”๋“œ์—์„œ ์ฐจ๋‹จ ์ •๋ณด๋งŒ ์ œ๊ณต +const reviews = await api.getReviews(alcoholId); +const blockedUsers = await api.getBlockedUsers(); + +// ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +const filteredReviews = reviews.map(review => + blockedUsers.includes(review.authorId) + ? { ...review, isBlocked: true } + : review +); +``` + +#### โœ… ์žฅ์  +- **๋ฐฑ์—”๋“œ ์ฝ”๋“œ ์ˆ˜์ • ์—†์Œ** +- **์‹ค์‹œ๊ฐ„ ์ฐจ๋‹จ/ํ•ด์ œ** ๊ฐ€๋Šฅ + +#### โŒ ๋‹จ์  +- **๋ณด์•ˆ ์ทจ์•ฝ** (ํด๋ผ์ด์–ธํŠธ์—์„œ ์šฐํšŒ ๊ฐ€๋Šฅ) +- **๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ฆ๊ฐ€** +- **์ฐจ๋‹จ๋œ ๋ฐ์ดํ„ฐ๋„ ์ „์†ก**๋จ + +--- + +## ๐Ÿ† ์ถ”์ฒœ ๊ตฌํ˜„ ๋ฐฉ์•ˆ: AOP ๊ธฐ๋ฐ˜ + +### ๐Ÿ“ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Controller โ”‚ โ”‚ AOP Aspect โ”‚ โ”‚ Response โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ @BlockFilter โ”œโ”€โ”€โ”€โ”€โ”ค BlockFilterAsp- โ”œโ”€โ”€โ”€โ”€โ”ค ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ โ”‚ +โ”‚ @GetMapping โ”‚ โ”‚ ect โ”‚ โ”‚ ์‘๋‹ต ๋ฐ์ดํ„ฐ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Service โ”‚ โ”‚ BlockCacheManagerโ”‚ โ”‚ Frontend โ”‚ +โ”‚ (๊ธฐ์กด ๋กœ์ง) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ ์ˆ˜์ • ์—†์Œ โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ ๋ธ”๋Ÿฌ UI ์ฒ˜๋ฆฌ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ๐Ÿ”ง ํ•ต์‹ฌ ๊ตฌํ˜„ ์ฝ”๋“œ + +#### 1. ์ฐจ๋‹จ ํ•„ํ„ฐ ์–ด๋…ธํ…Œ์ด์…˜ +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockFilter { + BlockType type() default BlockType.BLUR; + String userField() default "authorId"; // ์ฐจ๋‹จ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… +} + +public enum BlockType { + BLUR, // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ (๋‚ด์šฉ ๊ฐ€๋ฆผ) + EXCLUDE, // ์™„์ „ ์ œ์™ธ (๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ) + ACCESS_DENY // ์ ‘๊ทผ ์ฐจ๋‹จ (403 ์—๋Ÿฌ) +} +``` + +#### 2. AOP ์–ด์ŠคํŽ™ํŠธ +```java +@Component +@Aspect +public class BlockFilterAspect { + + private final BlockService blockService; + + @Around("@annotation(blockFilter)") + public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) { + // 1. ๊ธฐ์กด ๋กœ์ง ์‹คํ–‰ (์ˆ˜์ • ์—†์Œ) + Object result = joinPoint.proceed(); + + // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํ™•์ธ + Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); + if (currentUserId == -1L) { + return result; // ์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ ์•ˆํ•จ + } + + // 3. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ + return applyBlockLogic(result, currentUserId, blockFilter); + } + + private Object applyBlockLogic(Object result, Long currentUserId, BlockFilter filter) { + // ResponseEntity์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ + if (result instanceof ResponseEntity) { + ResponseEntity response = (ResponseEntity) result; + GlobalResponse globalResponse = (GlobalResponse) response.getBody(); + Object data = globalResponse.getData(); + + // ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + if (data instanceof List) { + List list = (List) data; + List filteredList = processListData(list, currentUserId, filter); + return ResponseEntity.ok(GlobalResponse.success(filteredList)); + } + + // ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + Object filteredData = processSingleData(data, currentUserId, filter); + return ResponseEntity.ok(GlobalResponse.success(filteredData)); + } + + return result; + } +} +``` + +#### 3. ์ฐจ๋‹จ ์„œ๋น„์Šค +```java +@Service +public class BlockService { + + private final BlockRepository blockRepository; + private final RedisTemplate> redisTemplate; + + @Cacheable(value = "blocked_users", key = "#userId") + public Set getBlockedUserIds(Long userId) { + return blockRepository.findBlockedUserIdsByBlockerId(userId); + } + + public T applyBlockFilter(T item, Long currentUserId, String userField, BlockType type) { + Set blockedUsers = getBlockedUserIds(currentUserId); + Long authorId = extractUserIdFromObject(item, userField); + + if (blockedUsers.contains(authorId)) { + switch (type) { + case BLUR: + return blurContent(item); + case EXCLUDE: + return null; // ๋ฆฌ์ŠคํŠธ์—์„œ ์ œ๊ฑฐ๋จ + case ACCESS_DENY: + throw new BlockedException("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ์ž…๋‹ˆ๋‹ค."); + } + } + + return item; + } +} +``` + +### ๐ŸŽจ ์‚ฌ์šฉ ์˜ˆ์‹œ + +```java +// ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ฆฌ๋ทฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ +@BlockFilter(userField = "authorId", type = BlockType.BLUR) +@GetMapping("/{alcoholId}") +public ResponseEntity getReviews(@PathVariable Long alcoholId, ...) { + // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ +} + +// ํŒ”๋กœ์›Œ ๋ชฉ๋ก ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์™„์ „ ์ œ์™ธ +@BlockFilter(userField = "userId", type = BlockType.EXCLUDE) +@GetMapping("/{targetUserId}/follower-list") +public ResponseEntity findFollowerList(@PathVariable Long targetUserId, ...) { + // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ +} + +// ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ ‘๊ทผ ์ฐจ๋‹จ +@BlockFilter(userField = "userId", type = BlockType.ACCESS_DENY) +@GetMapping("/{userId}") +public ResponseEntity getMyPage(@PathVariable Long userId) { + // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ +} +``` + +--- + +## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต + +### 1๏ธโƒฃ Redis ์บ์‹ฑ ์ „๋žต + +```java +@Component +public class BlockCacheManager { + + // ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ๋ชฉ๋ก ์บ์‹ฑ (TTL: 1์‹œ๊ฐ„) + @Cacheable(value = "blocked_users", key = "#userId", unless = "#userId == -1") + public Set getBlockedUserIds(Long userId) { + return blockRepository.findBlockedUserIdsByBlockerId(userId); + } + + // ์ฐจ๋‹จ ๊ด€๊ณ„ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” + @CacheEvict(value = "blocked_users", key = "#userId") + public void evictBlockCache(Long userId) { + log.info("์ฐจ๋‹จ ์บ์‹œ ๋ฌดํšจํ™”: userId={}", userId); + } + + // ๋ฐฐ์น˜๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ + public Map> getBulkBlockedUsers(Set userIds) { + return userIds.stream() + .filter(id -> id != -1L) // ์ต๋ช… ์‚ฌ์šฉ์ž ์ œ์™ธ + .collect(Collectors.toMap( + Function.identity(), + this::getBlockedUserIds + )); + } +} +``` + +### 2๏ธโƒฃ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” + +```sql +-- ์ฐจ๋‹จ ๊ด€๊ณ„ ํ…Œ์ด๋ธ” +CREATE TABLE user_block ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + blocker_id BIGINT NOT NULL COMMENT '์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž', + blocked_id BIGINT NOT NULL COMMENT '์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž', + block_type VARCHAR(20) DEFAULT 'FULL', + reason VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_blocker_blocked (blocker_id, blocked_id) +); + +-- ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ธ๋ฑ์Šค +CREATE INDEX idx_user_block_blocker ON user_block(blocker_id); +CREATE INDEX idx_user_block_blocked ON user_block(blocked_id); +``` + +### 3๏ธโƒฃ ์„ฑ๋Šฅ ๋น„๊ต ๋ถ„์„ + +| ๋ฐฉ์‹ | DB ์ฟผ๋ฆฌ ํšŸ์ˆ˜ | ์‘๋‹ต ์‹œ๊ฐ„ | ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ | ์œ ์ง€๋ณด์ˆ˜์„ฑ | +|------|-------------|-----------|--------------|------------| +| **์ฟผ๋ฆฌ ์ˆ˜์ •** | 1ํšŒ | 50ms | ๋‚ฎ์Œ | โญโญ | +| **์„œ๋น„์Šค ํ›„์ฒ˜๋ฆฌ** | 2ํšŒ | 80ms | ์ค‘๊ฐ„ | โญโญโญ | +| **AOP (์บ์‹œ ์ ์šฉ)** | 1ํšŒ + ์บ์‹œ | 60ms | ์ค‘๊ฐ„ | โญโญโญโญโญ | +| **ํ”„๋ก ํŠธ์—”๋“œ** | 2ํšŒ | 100ms | ๋†’์Œ | โญ | + +--- + +## ๐ŸŽญ ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ ๋ฐฉ์•ˆ + +### ๐Ÿ“ก API ์‘๋‹ต ํ˜•์‹ + +#### ๊ธฐ์กด ์‘๋‹ต +```json +{ + "success": true, + "data": [ + { + "reviewId": 123, + "content": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", + "authorProfileImage": "https://...", + "rating": 4.5, + "createdAt": "2024-01-15T10:30:00" + } + ] +} +``` + +#### ์ฐจ๋‹จ ์ ์šฉ ํ›„ ์‘๋‹ต +```json +{ + "success": true, + "data": [ + { + "reviewId": 123, + "content": "*** ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค ***", + "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", + "authorProfileImage": null, + "rating": 4.5, + "createdAt": "2024-01-15T10:30:00", + "isBlocked": true, + "blockReason": "BLOCKED_USER" + } + ] +} +``` + +### ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ UI ์ปดํฌ๋„ŒํŠธ + +#### React ์˜ˆ์‹œ +```tsx +interface ReviewCardProps { + review: ReviewResponse; +} + +const ReviewCard: React.FC = ({ review }) => { + const [showBlocked, setShowBlocked] = useState(false); + + if (review.isBlocked && !showBlocked) { + return ( +
+
+
+ + ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ +
+ +
+
+ ); + } + + return ( +
+
+ ํ”„๋กœํ•„ + {review.authorNickname} + +
+
+ {review.content} +
+ {review.isBlocked && ( +
+ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ  + +
+ )} +
+ ); +}; +``` + +#### CSS ์Šคํƒ€์ผ +```css +.review-card.blocked { + position: relative; + filter: blur(5px); + opacity: 0.6; +} + +.blur-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + backdrop-filter: blur(10px); +} + +.blocked-message { + color: white; + text-align: center; + margin-bottom: 16px; +} + +.show-content-btn { + background: #007bff; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; +} + +.blocked-indicator { + background: #fff3cd; + border: 1px solid #ffeaa7; + padding: 8px; + border-radius: 4px; + margin-top: 8px; + font-size: 12px; + color: #856404; +} +``` + +--- + +## ๐Ÿ“‹ ๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ + +### ๐Ÿ—๏ธ Phase 1: ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ ๊ตฌ์ถ• + +#### 1.1 ์ฐจ๋‹จ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ƒ์„ฑ +```java +// 1. Entity ์ƒ์„ฑ +@Entity +@Table(name = "user_block") +public class UserBlock extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "blocker_id", nullable = false) + private Long blockerId; + + @Column(name = "blocked_id", nullable = false) + private Long blockedId; + + @Enumerated(EnumType.STRING) + private BlockType blockType = BlockType.FULL; + + private String reason; +} + +// 2. Repository ์ƒ์„ฑ +public interface BlockRepository extends JpaRepository { + Set findBlockedIdsByBlockerId(Long blockerId); + boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); +} + +// 3. Service ์ƒ์„ฑ +@Service +public class BlockService { + public void createBlock(Long blockerId, Long blockedId) { ... } + public void removeBlock(Long blockerId, Long blockedId) { ... } + public Set getBlockedUserIds(Long userId) { ... } +} +``` + +#### 1.2 ์บ์‹œ ์„ค์ • +```yaml +# application.yml +spring: + cache: + type: caffeine + caffeine: + spec: maximumSize=10000,expireAfterWrite=1h +``` + +### ๐Ÿ”ง Phase 2: AOP ๊ตฌํ˜„ + +#### 2.1 ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐ AOP ๊ตฌํ˜„ +```java +// ์•ž์„œ ์ œ์‹œํ•œ ์ฝ”๋“œ ๊ตฌํ˜„ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockFilter { ... } + +@Component +@Aspect +public class BlockFilterAspect { ... } +``` + +#### 2.2 ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ +```java +@ExtendWith(MockitoExtension.class) +class BlockFilterAspectTest { + + @Mock + private BlockService blockService; + + @InjectMocks + private BlockFilterAspect blockFilterAspect; + + @Test + void ์ฐจ๋‹จ๋œ_์‚ฌ์šฉ์ž_๋ฆฌ๋ทฐ_๋ธ”๋Ÿฌ_์ฒ˜๋ฆฌ_ํ…Œ์ŠคํŠธ() { + // Given + Long currentUserId = 1L; + Long blockedUserId = 2L; + when(blockService.getBlockedUserIds(currentUserId)) + .thenReturn(Set.of(blockedUserId)); + + // When & Then + // AOP ๋กœ์ง ํ…Œ์ŠคํŠธ + } +} +``` + +### ๐Ÿ“ฑ Phase 3: ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ + +#### 3.1 API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • +```typescript +// API ํƒ€์ž… ์ •์˜ ํ™•์žฅ +interface BaseResponse { + isBlocked?: boolean; + blockReason?: string; +} + +interface ReviewResponse extends BaseResponse { + reviewId: number; + content: string; + authorNickname: string; + // ... ๊ธฐํƒ€ ํ•„๋“œ +} +``` + +#### 3.2 UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ +```tsx +// ์•ž์„œ ์ œ์‹œํ•œ React ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ +``` + +### ๐Ÿš€ Phase 4: ์ ์ง„์  ์ ์šฉ + +#### 4.1 ์šฐ์„ ์ˆœ์œ„๋ณ„ ์ ์šฉ +1. **High Priority**: ๋ฆฌ๋ทฐ ์กฐํšŒ, ๋Œ“๊ธ€ ์กฐํšŒ +2. **Medium Priority**: ํŒ”๋กœ์šฐ ๋ชฉ๋ก, ๋งˆ์ดํŽ˜์ด์ง€ +3. **Low Priority**: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ, ์ถ”์ฒœ ๋ชฉ๋ก + +#### 4.2 ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • +```java +@Component +public class BlockMetrics { + private final MeterRegistry meterRegistry; + + public void recordBlockFilter(String endpoint, BlockType type, int blockedCount) { + Counter.builder("block.filter.applied") + .tag("endpoint", endpoint) + .tag("type", type.name()) + .register(meterRegistry) + .increment(blockedCount); + } +} +``` + +### ๐Ÿ“Š Phase 5: ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง + +#### 5.1 ์„ฑ๋Šฅ ์ง€ํ‘œ ์ˆ˜์ง‘ +- ์ฐจ๋‹จ ์บ์‹œ ํžˆํŠธ์œจ +- ์‘๋‹ต ์‹œ๊ฐ„ ๋ณ€ํ™” +- ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ณ€ํ™” + +#### 5.2 ์•Œ๋žŒ ์„ค์ • +```yaml +# Prometheus ๊ทœ์น™ +groups: + - name: block_system + rules: + - alert: BlockCacheHitRateLow + expr: block_cache_hit_rate < 0.8 + for: 5m + annotations: + summary: "์ฐจ๋‹จ ์บ์‹œ ํžˆํŠธ์œจ์ด ๋‚ฎ์Šต๋‹ˆ๋‹ค" +``` + +--- + +## ๐ŸŽฏ ๊ฒฐ๋ก  + +### โœจ ์™œ AOP ๋ฐฉ์‹์„ ์ถ”์ฒœํ•˜๋Š”๊ฐ€? + +1. **์ตœ์†Œ ์นจ์Šต์ ** - ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๊ฑฐ์˜ ์ˆ˜์ •ํ•˜์ง€ ์•Š์Œ +2. **์ค‘์•™ ์ง‘์ค‘์‹** - ์ฐจ๋‹จ ๋กœ์ง์ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ๋จ +3. **ํ™•์žฅ ๊ฐ€๋Šฅ** - ๋‹ค์–‘ํ•œ ์ฐจ๋‹จ ํƒ€์ž…์„ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ +4. **์„ฑ๋Šฅ ํšจ์œจ์ ** - ์บ์‹œ๋ฅผ ํ†ตํ•œ ์ตœ์ ํ™” +5. **ํ…Œ์ŠคํŠธ ์šฉ์ด** - ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ + +### ๐Ÿš€ ๊ธฐ๋Œ€ ํšจ๊ณผ + +- **๊ฐœ๋ฐœ ์‹œ๊ฐ„ ๋‹จ์ถ•**: ๊ธฐ์กด ์ฟผ๋ฆฌ ์ˆ˜์ • ๋ถˆํ•„์š” +- **์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ**: ์ฐจ๋‹จ ๋กœ์ง ์ค‘์•™ํ™” +- **์„ฑ๋Šฅ ์•ˆ์ •์„ฑ**: ์บ์‹œ ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” +- **์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ **: ์„ธ๋ฐ€ํ•œ ์ฐจ๋‹จ ์˜ต์…˜ ์ œ๊ณต + +### ๐Ÿ“… ๊ตฌํ˜„ ์ผ์ • (์˜ˆ์ƒ) + +- **Week 1**: Phase 1-2 (๊ธฐ๋ฐ˜ ๊ตฌ์กฐ + AOP) +- **Week 2**: Phase 3 (ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™) +- **Week 3**: Phase 4-5 (์ ์ง„์  ์ ์šฉ + ๋ชจ๋‹ˆํ„ฐ๋ง) + +**์ด ์˜ˆ์ƒ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„: 3์ฃผ** + +--- + +## ๐Ÿ“ž ๋ฌธ์˜ ๋ฐ ์ง€์› + +์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋ฌธ์˜์‚ฌํ•ญ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์—ฐ๋ฝ์ฃผ์„ธ์š”! + +- ๊ธฐ์ˆ ์  ๋ฌธ์˜: ๊ฐœ๋ฐœํŒ€ ์ฑ„๋„ +- ๊ธฐํš ๊ด€๋ จ: ๊ธฐํšํŒ€ ์ฑ„๋„ +- ์„ฑ๋Šฅ ๊ด€๋ จ: DevOps ํŒ€ ์ฑ„๋„ \ No newline at end of file diff --git a/git.environment-variables b/git.environment-variables index 2bf643353..b146e17ba 160000 --- a/git.environment-variables +++ b/git.environment-variables @@ -1 +1 @@ -Subproject commit 2bf6433534696b910c69c8237689ce9753e52f9a +Subproject commit b146e17baab4747e4f7045340b8cb13636db09e8 diff --git "a/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" "b/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" new file mode 100644 index 000000000..29264d208 --- /dev/null +++ "b/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" @@ -0,0 +1,31 @@ +### ํ† ํฐ ๋ฐœ๊ธ‰ (์„ ํƒ์‚ฌํ•ญ - ํ† ํฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ) +POST {{host}}/api/v1/oauth/login +Content-Type: application/json + +{ + "email": "dev.bottle-note@gmail.com", + "socialType": "GOOGLE", + "gender": "MALE", + "age": 25 +} + +> {% + //ํ† ํฐ์„ ์ „์—ญ๋ณ€์ˆ˜๋กœ ์ €์žฅ + client.global.set("accessToken", response.body.data.accessToken); +%} + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ +POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=2&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (๊ธฐ๋ณธ ์‚ฌ์œ ) +POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=3 +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (์ปค์Šคํ…€ ์‚ฌ์œ ) +POST {{host}}/api/v1/test/block/add?blockerId=2&blockedId=1&reason=๋ถ€์ ์ ˆํ•œ ๋Œ“๊ธ€ +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ ์ผ€์ด์Šค (๋™์ผ ์‚ฌ์šฉ์ž) +POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=1&reason=์ž๊ธฐ ์ž์‹  ์ฐจ๋‹จ +Content-Type: application/json \ No newline at end of file diff --git "a/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" "b/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" new file mode 100644 index 000000000..1d67b2c9d --- /dev/null +++ "b/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" @@ -0,0 +1,15 @@ +### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ) +GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=2 +Content-Type: application/json + +### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์ฐจ๋‹จ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ) +GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=5 +Content-Type: application/json + +### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) +GET {{host}}/api/v1/test/block/check?blockerId=2&blockedId=1 +Content-Type: application/json + +### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/check?blockerId=999&blockedId=888 +Content-Type: application/json \ No newline at end of file diff --git "a/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" "b/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" new file mode 100644 index 000000000..fc73a52d8 --- /dev/null +++ "b/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" @@ -0,0 +1,15 @@ +### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์ด ์žˆ๋Š” ๊ฒฝ์šฐ) +GET {{host}}/api/v1/test/block/list/1 +Content-Type: application/json + +### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) +GET {{host}}/api/v1/test/block/list/10 +Content-Type: application/json + +### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) +GET {{host}}/api/v1/test/block/list/2 +Content-Type: application/json + +### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/list/999 +Content-Type: application/json \ No newline at end of file diff --git "a/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" "b/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" new file mode 100644 index 000000000..a477b571c --- /dev/null +++ "b/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" @@ -0,0 +1,15 @@ +### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์ฐจ๋‹จ ๋‚ด์—ญ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/statistics/1 +Content-Type: application/json + +### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์ฐจ๋‹จ ๋‚ด์—ญ์ด ์—†๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/statistics/10 +Content-Type: application/json + +### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (๋งŽ์€ ์ฐจ๋‹จ ๋‚ด์—ญ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/statistics/2 +Content-Type: application/json + +### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) +GET {{host}}/api/v1/test/block/statistics/999 +Content-Type: application/json \ No newline at end of file diff --git "a/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" "b/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" new file mode 100644 index 000000000..bed251f37 --- /dev/null +++ "b/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" @@ -0,0 +1,15 @@ +### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๊ธฐ์กด ์ฐจ๋‹จ ๊ด€๊ณ„) +DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=2 +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๋˜ ๋‹ค๋ฅธ ์ฐจ๋‹จ ๊ด€๊ณ„) +DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=3 +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฐจ๋‹จ ๊ด€๊ณ„) +DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=10 +Content-Type: application/json + +### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) +DELETE {{host}}/api/v1/test/block/remove?blockerId=2&blockedId=1 +Content-Type: application/json \ No newline at end of file diff --git "a/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" "b/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" new file mode 100644 index 000000000..ac175c16e --- /dev/null +++ "b/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" @@ -0,0 +1,47 @@ +### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์ค€๋น„ - ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ +# ๋จผ์ € ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ•˜๋„๋ก ์„ค์ • +POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=2&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ +Content-Type: application/json + +### +POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=3&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ +Content-Type: application/json + +### 1. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (์ฐจ๋‹จํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋กœ ์š”์ฒญ) +# ์‚ฌ์šฉ์ž 5๋Š” ์•„๋ฌด๋„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ๋ชจ๋“  ๋ฆฌ๋ทฐ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ณด์ž„ +GET {{host}}/api/v1/test/block/test-data/5 +Content-Type: application/json +X-User-Id: 5 + +### 2. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) +# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ–ˆ์œผ๋ฏ€๋กœ ํ•ด๋‹น ๋ฆฌ๋ทฐ๊ฐ€ ๋งˆ์Šคํ‚น๋˜์–ด์•ผ ํ•จ +GET {{host}}/api/v1/test/block/test-data/1 +Content-Type: application/json +X-User-Id: 1 + +### 3. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋กœ ์š”์ฒญ) +# ์‚ฌ์šฉ์ž 4๋Š” ์•„๋ฌด๋„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ๋ชจ๋“  ๋ฆฌ๋ทฐ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ณด์ž„ +GET {{host}}/api/v1/test/block/test-data/4 +Content-Type: application/json +X-User-Id: 4 + +### 4. ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์‚ฌ์ „ ํ™•์ธ) +# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จํ–ˆ๋Š”์ง€ ํ™•์ธ +GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=2 +Content-Type: application/json + +### 5. ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ +# ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ (2,3์ด ๋‚˜์™€์•ผ ํ•จ) +GET {{host}}/api/v1/test/block/list/1 +Content-Type: application/json + +### 6. ์ฐจ๋‹จ ํ•ด์ œ ํ›„ ์žฌํ…Œ์ŠคํŠธ +# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2์˜ ์ฐจ๋‹จ์„ ํ•ด์ œ +DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=2 +Content-Type: application/json + +### +# ์ฐจ๋‹จ ํ•ด์ œ ํ›„ ๋‹ค์‹œ ํ…Œ์ŠคํŠธ (์‚ฌ์šฉ์ž 2์˜ ๋ฆฌ๋ทฐ๊ฐ€ ๋‹ค์‹œ ๋ณด์—ฌ์•ผ ํ•จ) +GET {{host}}/api/v1/test/block/test-data/1 +Content-Type: application/json +X-User-Id: 1 \ No newline at end of file diff --git "a/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" "b/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" new file mode 100644 index 000000000..66614909a --- /dev/null +++ "b/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" @@ -0,0 +1,6 @@ +### ์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” (๋น„ํ™œ์„ฑํ™”๋œ ๊ธฐ๋Šฅ) +DELETE {{host}}/api/v1/test/block/clear +Content-Type: application/json + +### ์ฐธ๊ณ : ์ด ๊ธฐ๋Šฅ์€ DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +### ์‹ค์ œ๋กœ๋Š” ๊ฐœ๋ณ„ ์ฐจ๋‹จ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/http/block/README.md b/http/block/README.md new file mode 100644 index 000000000..66bc3a1b0 --- /dev/null +++ b/http/block/README.md @@ -0,0 +1,107 @@ +# Block API ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ + +์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” `BlockTestController`์˜ ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ HTTP ํŒŒ์ผ๋“ค์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +## ๐Ÿ”ง ์‚ฌ์ „ ์ค€๋น„ + +1. **ํ™˜๊ฒฝ ์„ค์ •**: ์ด ์ปจํŠธ๋กค๋Ÿฌ๋Š” `local`, `dev`, `test` ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค (`prod` ํ™˜๊ฒฝ์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”) +2. **ํ™˜๊ฒฝ ๋ณ€์ˆ˜**: IntelliJ HTTP Client์—์„œ `http-client.env.json` ํŒŒ์ผ์˜ ํ™˜๊ฒฝ์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + - `local`: http://localhost:30001 + - `dev`: http://localhost:30001 + - `prod`: https://api.bottle-note.com/ +3. **์ธ์ฆ**: Block API (`/api/v1/test/**`)๋Š” ์ธ์ฆ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (Spring Security์—์„œ `permitAll()` ์„ค์ •) +4. **ํ† ํฐ (์„ ํƒ์‚ฌํ•ญ)**: ํ•„์š”์‹œ 01๋ฒˆ ํŒŒ์ผ์˜ ํ† ํฐ ๋ฐœ๊ธ‰ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `accessToken` ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค + +## ๐Ÿ“ ํ…Œ์ŠคํŠธ ํŒŒ์ผ ๊ตฌ์„ฑ + +### 01-์ฐจ๋‹จ_๊ด€๊ณ„_์ƒ์„ฑ.http +- **๋ชฉ์ **: ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ์ƒ์„ฑํ•˜๋Š” API ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `POST /api/v1/test/block/add` +- **ํŒŒ๋ผ๋ฏธํ„ฐ**: + - `blockerId`: ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ•„์ˆ˜) + - `blockedId`: ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ•„์ˆ˜) + - `reason`: ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ, ๊ธฐ๋ณธ๊ฐ’: "ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ") + +### 02-์ฐจ๋‹จ_์—ฌ๋ถ€_ํ™•์ธ.http +- **๋ชฉ์ **: ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” API ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/check` +- **ํŒŒ๋ผ๋ฏธํ„ฐ**: + - `blockerId`: ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + - `blockedId`: ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + +### 03-์ฐจ๋‹จ_๋ชฉ๋ก_์กฐํšŒ.http +- **๋ชฉ์ **: ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” API ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/list/{userId}` +- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ์ฐจ๋‹จ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID + +### 04-์ฐจ๋‹จ_ํ†ต๊ณ„_์กฐํšŒ.http +- **๋ชฉ์ **: ํŠน์ • ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ํ†ต๊ณ„๋ฅผ ์กฐํšŒํ•˜๋Š” API ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/statistics/{userId}` +- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ํ†ต๊ณ„๋ฅผ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID + +### 05-์ฐจ๋‹จ_๊ด€๊ณ„_ํ•ด์ œ.http +- **๋ชฉ์ **: ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ํ•ด์ œํ•˜๋Š” API ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `DELETE /api/v1/test/block/remove` +- **ํŒŒ๋ผ๋ฏธํ„ฐ**: + - `blockerId`: ์ฐจ๋‹จ์„ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID + - `blockedId`: ์ฐจ๋‹จ ํ•ด์ œ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID + +### 06-์ฐจ๋‹จ_ํ•„ํ„ฐ_ํ…Œ์ŠคํŠธ.http +- **๋ชฉ์ **: `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋œ API์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ +- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/test-data/{userId}` +- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ๊ฒฝ๋กœ์˜ ์‚ฌ์šฉ์ž ID (์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ) +- **ํ—ค๋”**: `X-User-Id` - ์‹ค์ œ ์š”์ฒญํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ •) +- **ํŠน์ง•**: + - ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ œ๊ณต + - ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ๋ฐ์ดํ„ฐ๋Š” "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น๋จ + - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋‹‰๋„ค์ž„์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"๋กœ ๋ณ€๊ฒฝ๋จ + - ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋„ null๋กœ ์ฒ˜๋ฆฌ๋จ + +### 07-์ „์ฒด_์ดˆ๊ธฐํ™”_ํ…Œ์ŠคํŠธ.http +- **๋ชฉ์ **: ์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” API ํ…Œ์ŠคํŠธ (ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™”๋จ) +- **์—”๋“œํฌ์ธํŠธ**: `DELETE /api/v1/test/block/clear` +- **์ฐธ๊ณ **: DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์•ˆ์ „์ƒ์˜ ์ด์œ ๋กœ ๋น„ํ™œ์„ฑํ™”๋œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค + +## ๐Ÿš€ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์‹œ + +### ๊ธฐ๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค +1. **์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ**: ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จ +2. **์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ**: 1 โ†’ 2 ์ฐจ๋‹จ ํ™•์ธ (true), 2 โ†’ 1 ์ฐจ๋‹จ ํ™•์ธ (false) +3. **์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ**: ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก์— ์‚ฌ์šฉ์ž 2๊ฐ€ ํฌํ•จ๋˜๋Š”์ง€ ํ™•์ธ +4. **์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ**: ์‚ฌ์šฉ์ž 1๋กœ ์š”์ฒญ ์‹œ ์‚ฌ์šฉ์ž 2์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ™•์ธ +5. **์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ**: ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2์˜ ์ฐจ๋‹จ์„ ํ•ด์ œ +6. **์ฐจ๋‹จ ์—ฌ๋ถ€ ์žฌํ™•์ธ**: 1 โ†’ 2 ์ฐจ๋‹จ ํ™•์ธ (false) + +### ๊ณ ๊ธ‰ ์‹œ๋‚˜๋ฆฌ์˜ค +1. **์ƒํ˜ธ ์ฐจ๋‹จ**: ์‚ฌ์šฉ์ž 1๊ณผ ์‚ฌ์šฉ์ž 2๊ฐ€ ์„œ๋กœ ์ฐจ๋‹จ +2. **๋‹ค์ค‘ ์ฐจ๋‹จ**: ์‚ฌ์šฉ์ž 1์ด ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž(2, 3, 4)๋ฅผ ์ฐจ๋‹จ +3. **ํ†ต๊ณ„ ํ™•์ธ**: ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ +4. **ํ•„ํ„ฐ๋ง ํ™•์ธ**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋“ค์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ™•์ธ + +## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ + +1. **ํ™˜๊ฒฝ ์ œํ•œ**: `prod` ํ™˜๊ฒฝ์—์„œ๋Š” ์ด API๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +2. **๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ**: ์‹ค์ œ ์šด์˜ ๋ฐ์ดํ„ฐ์™€ ์—ฐ๋™๋˜๋ฏ€๋กœ ํ…Œ์ŠคํŠธ ์‹œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค +3. **์‚ฌ์šฉ์ž ID**: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ID๋กœ ํ…Œ์ŠคํŠธํ•  ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +4. **์ „์ฒด ์ดˆ๊ธฐํ™”**: ์•ˆ์ „์ƒ์˜ ์ด์œ ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๊ฐœ๋ณ„ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + +## ๐Ÿ” ์‘๋‹ต ํ˜•ํƒœ + +๋ชจ๋“  API๋Š” `GlobalResponse` ํ˜•ํƒœ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค: + +```json +{ + "success": true, + "message": "์„ฑ๊ณต ๋ฉ”์‹œ์ง€", + "data": "์‘๋‹ต ๋ฐ์ดํ„ฐ" +} +``` + +์‹คํŒจ ์‹œ: +```json +{ + "success": false, + "message": "์‹คํŒจ ๋ฉ”์‹œ์ง€", + "data": null +} +``` \ No newline at end of file diff --git a/src/main/java/app/bottlenote/global/annotation/BlockFilter.java b/src/main/java/app/bottlenote/global/annotation/BlockFilter.java new file mode 100644 index 000000000..c19ad46d5 --- /dev/null +++ b/src/main/java/app/bottlenote/global/annotation/BlockFilter.java @@ -0,0 +1,26 @@ +package app.bottlenote.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ + *

+ * ์‚ฌ์šฉ ์˜ˆ์‹œ: + * @BlockFilter(userField = "authorId") + * @GetMapping("/reviews") public ResponseEntity getReviews() { ... } + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockFilter { + + /** + * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… + * ์˜ˆ: "authorId", "userId", "writerId" + * + * @return ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… + */ + String userField() default "authorId"; +} diff --git a/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java b/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java new file mode 100644 index 000000000..ace2aaa1f --- /dev/null +++ b/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java @@ -0,0 +1,228 @@ +package app.bottlenote.global.aspect; + +import app.bottlenote.global.annotation.BlockFilter; +import app.bottlenote.global.data.response.GlobalResponse; +import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.global.service.BlockService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ์ฐจ๋‹จ ํ•„ํ„ฐ AOP ํด๋ž˜์Šค + * + * @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ์˜ ์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„์„œ + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class BlockFilterAspect { + + private final BlockService blockService; + private final ObjectMapper objectMapper; + + /** + * @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๋ฅผ Around ์–ด๋“œ๋ฐ”์ด์Šค๋กœ ์ฒ˜๋ฆฌ + */ + @Around("@annotation(blockFilter)") + public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) throws Throwable { + + // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ + Object result = joinPoint.proceed(); + + // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํ™•์ธ + Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); + + if (currentUserId == -1L) { + // ์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉํ•˜์ง€ ์•Š์Œ + return result; + } + + // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ + Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); + + if (blockedUserIds.isEmpty()) { + return result; + } + + // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ (ํ•ญ์ƒ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ) + return applyBlockFilter(result, blockedUserIds, blockFilter.userField()); + } + + /** + * ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง์„ ์ ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ (๋‹จ์ˆœ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ) + */ + private Object applyBlockFilter(Object result, Set blockedUserIds, String userField) { + + if (!(result instanceof ResponseEntity)) { + return result; + } + + ResponseEntity responseEntity = (ResponseEntity) result; + Object body = responseEntity.getBody(); + + if (!(body instanceof GlobalResponse)) { + return result; + } + + GlobalResponse globalResponse = (GlobalResponse) body; + Object data = globalResponse.getData(); + + if (data == null) { + return result; + } + + // ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + Object filteredData; + if (data instanceof List) { + filteredData = blurListData((List) data, blockedUserIds, userField); + } else { + filteredData = blurSingleData(data, blockedUserIds, userField); + } + + // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ์šด ์‘๋‹ต ์ƒ์„ฑ + GlobalResponse filteredResponse = GlobalResponse.success(filteredData); + return ResponseEntity.ok(filteredResponse); + } + + /** + * List ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + @SuppressWarnings("unchecked") + private List blurListData(List dataList, Set blockedUserIds, String userField) { + return dataList.stream() + .map(item -> blurItemIfBlocked(item, blockedUserIds, userField)) + .collect(Collectors.toList()); + } + + /** + * ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + private Object blurSingleData(Object data, Set blockedUserIds, String userField) { + return blurItemIfBlocked(data, blockedUserIds, userField); + } + + /** + * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + private Object blurItemIfBlocked(Object item, Set blockedUserIds, String userField) { + if (item == null) { + return item; + } + + // ์‚ฌ์šฉ์ž ID ์ถ”์ถœ + Long userId = extractUserIdFromObject(item, userField); + + if (userId == null || !blockedUserIds.contains(userId)) { + return item; // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์›๋ณธ ๋ฐ˜ํ™˜ + } + + // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฉด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + return blurContent(item); + } + + /** + * ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฉ”์„œ๋“œ + */ + private Long extractUserIdFromObject(Object obj, String fieldName) { + try { + Field field = findFieldInClass(obj.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + Object value = field.get(obj); + if (value instanceof Long) { + return (Long) value; + } + } + } catch (Exception e) { + log.warn("์‚ฌ์šฉ์ž ID ์ถ”์ถœ ์‹คํŒจ - ๊ฐ์ฒด: {}, ํ•„๋“œ: {}, ์—๋Ÿฌ: {}", + obj.getClass().getSimpleName(), fieldName, e.getMessage()); + } + return null; + } + + /** + * ํด๋ž˜์Šค ๊ณ„์ธต๊ตฌ์กฐ์—์„œ ํ•„๋“œ๋ฅผ ์ฐพ๋Š” ๋ฉ”์„œ๋“œ + */ + private Field findFieldInClass(Class clazz, String fieldName) { + Class currentClass = clazz; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + return null; + } + + /** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ + */ + private Object blurContent(Object originalItem) { + try { + // Jackson์„ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ๊นŠ์€ ๋ณต์‚ฌ + Object clonedItem = deepCloneObject(originalItem); + + // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ํ‘œ์‹œ๋กœ ๋งˆ์Šคํ‚น + maskField(clonedItem, "content", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); + maskField(clonedItem, "title", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); + maskField(clonedItem, "authorNickname", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"); + maskField(clonedItem, "nickname", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"); + maskField(clonedItem, "authorProfileImage", null); + maskField(clonedItem, "profileImage", null); + + return clonedItem; + + } catch (Exception e) { + log.error("์ฐจ๋‹จ ์ฒ˜๋ฆฌ ์‹คํŒจ: {}", e.getMessage()); + // ์‹คํŒจ ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ + return originalItem; + } + } + + /** + * ๊ฐ์ฒด์˜ ํŠน์ • ํ•„๋“œ๋ฅผ ๋งˆ์Šคํ‚นํ•˜๋Š” ๋ฉ”์„œ๋“œ + */ + private void maskField(Object obj, String fieldName, Object maskValue) { + try { + Field field = findFieldInClass(obj.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + field.set(obj, maskValue); + } + } catch (Exception e) { + // ํ•„๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์„ค์ • ์‹คํŒจ ์‹œ ๋ฌด์‹œ (์ •์ƒ์ ์ธ ์ƒํ™ฉ) + } + } + + /** + * Jackson ObjectMapper๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ๊นŠ์€ ๋ณต์‚ฌ + */ + private Object deepCloneObject(Object original) { + try { + if (original == null) { + return null; + } + + // Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ + String jsonString = objectMapper.writeValueAsString(original); + return objectMapper.readValue(jsonString, original.getClass()); + } catch (Exception e) { + log.warn("๊ฐ์ฒด ๋ณต์‚ฌ ์‹คํŒจ, ์›๋ณธ ๋ฐ˜ํ™˜: {}", e.getMessage()); + return original; + } + } +} diff --git a/src/main/java/app/bottlenote/global/cache/local/LocalCacheType.java b/src/main/java/app/bottlenote/global/cache/local/LocalCacheType.java index cf34f65c5..9ddf011a1 100644 --- a/src/main/java/app/bottlenote/global/cache/local/LocalCacheType.java +++ b/src/main/java/app/bottlenote/global/cache/local/LocalCacheType.java @@ -9,7 +9,8 @@ @AllArgsConstructor public enum LocalCacheType { LOCAL_REGION_CACHE("local_cache_alcohol_region_information", 60 * 60 * 24, 1), - LOCAL_ALCOHOL_CATEGORY_CACHE("local_cache_alcohol_category_information", 60 * 60 * 24, 1); + LOCAL_ALCOHOL_CATEGORY_CACHE("local_cache_alcohol_category_information", 60 * 60 * 24, 1), + BLOCKED_USERS_CACHE("blocked_users", 60 * 60 * 2, 1000); private final String cacheName; // ๋“ฑ๋กํ•  ์บ์‹œ ์ด๋ฆ„ private final int secsToExpireAfterWrite; // ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ( 60 * 60 * 24 = 1์ผ ) diff --git a/src/main/java/app/bottlenote/global/controller/BlockTestController.java b/src/main/java/app/bottlenote/global/controller/BlockTestController.java new file mode 100644 index 000000000..b859fe458 --- /dev/null +++ b/src/main/java/app/bottlenote/global/controller/BlockTestController.java @@ -0,0 +1,248 @@ +package app.bottlenote.global.controller; + +import app.bottlenote.global.data.response.GlobalResponse; +import app.bottlenote.global.service.BlockService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ์šฉ ์ปจํŠธ๋กค๋Ÿฌ + * + * ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. + * ์šด์˜ ํ™˜๊ฒฝ(prod)์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/test/block") +@RequiredArgsConstructor +@Profile({"local", "dev", "test", "!prod"}) +public class BlockTestController { + + private final BlockService blockService; + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ) + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋  ์‚ฌ์šฉ์ž ID + * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + */ + @PostMapping("/add") + public ResponseEntity createBlock( + @RequestParam Long blockerId, + @RequestParam Long blockedId, + @RequestParam(required = false, defaultValue = "ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ") String reason) { + + try { + var userBlock = blockService.createBlock(blockerId, blockedId, reason); + return GlobalResponse.ok("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์™„๋ฃŒ: " + blockerId + " -> " + blockedId + + " (ID: " + userBlock.getId() + ")"); + } catch (Exception e) { + log.error("ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ ์ƒ์„ฑ ์‹คํŒจ", e); + return ResponseEntity.badRequest() + .body(GlobalResponse.fail("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ: " + e.getMessage())); + } + } + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (ํ…Œ์ŠคํŠธ์šฉ) + */ + @DeleteMapping("/remove") + public ResponseEntity deleteBlock( + @RequestParam Long blockerId, + @RequestParam Long blockedId) { + + blockService.removeBlock(blockerId, blockedId); + + return GlobalResponse.ok("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์™„๋ฃŒ: " + blockerId + " -> " + blockedId); + } + + /** + * ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (ํ…Œ์ŠคํŠธ์šฉ) + */ + @GetMapping("/list/{userId}") + public ResponseEntity getBlockList(@PathVariable Long userId) { + + var blockedUsers = blockService.getBlockedUserIds(userId); + + return GlobalResponse.ok(blockedUsers); + } + + /** + * ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (ํ…Œ์ŠคํŠธ์šฉ) + */ + @GetMapping("/check") + public ResponseEntity checkBlock( + @RequestParam Long blockerId, + @RequestParam Long blockedId) { + + boolean isBlocked = blockService.isBlocked(blockerId, blockedId); + + return GlobalResponse.ok("์ฐจ๋‹จ ์—ฌ๋ถ€: " + isBlocked); + } + + /** + * ๋ชจ๋“  ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” (ํ…Œ์ŠคํŠธ์šฉ) - ํ˜„์žฌ DB ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด ์ด ๊ธฐ๋Šฅ์€ ๋น„ํ™œ์„ฑํ™” + */ + @DeleteMapping("/clear") + public ResponseEntity deleteAllBlocks() { + + // DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์ „์ฒด ์‚ญ์ œ๋Š” ์œ„ํ—˜ํ•˜๋ฏ€๋กœ ๋น„ํ™œ์„ฑํ™” + log.warn("์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™”๋Š” DB ๊ธฐ๋ฐ˜์—์„œ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + return GlobalResponse.ok("DB ๊ธฐ๋ฐ˜์—์„œ๋Š” ์ „์ฒด ์ดˆ๊ธฐํ™”๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”."); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (ํ…Œ์ŠคํŠธ์šฉ) + */ + @GetMapping("/statistics/{userId}") + public ResponseEntity getBlockStatistics(@PathVariable Long userId) { + + try { + var statistics = blockService.getBlockStatistics(userId); + return GlobalResponse.ok(statistics); + } catch (Exception e) { + log.error("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ", e); + return ResponseEntity.badRequest() + .body(GlobalResponse.fail("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ: " + e.getMessage())); + } + } + + /** + * ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ (ํ…Œ์ŠคํŠธ์šฉ) - ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ์‘๋‹ต + * + * ์ด API๋Š” ์‹ค์ œ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์‘๋‹ต๊ณผ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋กœ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ์ง์ ‘ ์ฐจ๋‹จ ํ•„ํ„ฐ๋ง์„ ์ ์šฉํ•˜์—ฌ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๊ฐ€ ๋งˆ์Šคํ‚น๋ฉ๋‹ˆ๋‹ค. + * + * ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค: + * 1. ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ•œ ์ƒํƒœ์—์„œ X-User-Id=1๋กœ ์š”์ฒญ + * 2. ์‚ฌ์šฉ์ž 2,3์˜ ๋ฆฌ๋ทฐ ๋‚ด์šฉ์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น๋จ + * 3. ๋‹‰๋„ค์ž„์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"๋กœ ๋ณ€๊ฒฝ๋จ + */ + @GetMapping("/test-data/{userId}") + public ResponseEntity getTestData(@PathVariable Long userId, + @RequestHeader(value = "X-User-Id", required = false) Long requestUserId) { + + log.info("์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์š”์ฒญ - pathUserId: {}, requestUserId: {}", userId, requestUserId); + + // ์š”์ฒญํ•œ ์‚ฌ์šฉ์ž ID ๊ฒฐ์ • (X-User-Id ํ—ค๋” ์šฐ์„ ) + Long currentUserId = requestUserId != null ? requestUserId : userId; + + // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ + java.util.Set blockedUserIds = java.util.Collections.emptySet(); + if (currentUserId != null && currentUserId != -1L) { + blockedUserIds = blockService.getBlockedUserIds(currentUserId); + log.info("์‚ฌ์šฉ์ž {}์˜ ์ฐจ๋‹จ ๋ชฉ๋ก: {}", currentUserId, blockedUserIds); + } + + // ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + var testReviews = java.util.List.of( + createTestReview(1L, "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค. ์Šค๋ชจํ‚คํ•œ ํ–ฅ์ด ์ผํ’ˆ์ด๋„ค์š”!", "์œ„์Šคํ‚ค_๋Ÿฌ๋ฒ„", "profile1.jpg", 4.5), + createTestReview(2L, "์ฒซ ์‹œ์Œ์ธ๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ๋ถ€๋“œ๋Ÿฝ๊ณ  ๋‹ฌ์ฝคํ•ฉ๋‹ˆ๋‹ค. ์ž…๋ฌธ์ž์—๊ฒŒ ์ถ”์ฒœ!", "์ดˆ๋ณด_์œ„์Šคํ‚ค", "profile2.jpg", 4.0), + createTestReview(3L, "๊ฐ€๊ฒฉ ๋Œ€๋น„ ํ›Œ๋ฅญํ•œ ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค. ์žฌ๊ตฌ๋งค ์˜ํ–ฅ 100%", "์œ„์Šคํ‚ค_๋งˆ๋‹ˆ์•„", "profile3.jpg", 4.8), + createTestReview(4L, "์˜คํฌํ–ฅ์ด ๊ฐ•ํ•ด์„œ ํ˜ธ๋ถˆํ˜ธ๊ฐ€ ๊ฐˆ๋ฆด ๊ฒƒ ๊ฐ™์•„์š”.", "๋ฆฌ๋ทฐ_๊ณ ์ˆ˜", "profile4.jpg", 3.5), + createTestReview(5L, "์นœ๊ตฌ๋“ค๊ณผ ํ•จ๊ป˜ ๋งˆ์‹œ๊ธฐ ์ข‹์€ ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค.", "์†Œ์…œ_๋“œ๋ง์ปค", "profile5.jpg", 4.2) + ); + + // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ์— ํ•„ํ„ฐ๋ง ์ ์šฉ + var filteredReviews = testReviews.stream() + .map(review -> { + Long authorId = (Long) review.get("authorId"); + if (blockedUserIds.contains(authorId)) { + return applyBlockMask(review); + } + return review; + }) + .collect(java.util.stream.Collectors.toList()); + + // ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์‘๋‹ต ๊ตฌ์กฐ๋กœ ๋ž˜ํ•‘ + var response = java.util.Map.of( + "totalCount", filteredReviews.size(), + "reviewList", filteredReviews, + "message", "์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ (์ง์ ‘ ํ•„ํ„ฐ๋ง ์ ์šฉ)", + "blockedUserIds", blockedUserIds, + "currentUserId", currentUserId + ); + + return GlobalResponse.ok(response); + } + + /** + * ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ—ฌํผ ๋ฉ”์„œ๋“œ + */ + private java.util.Map createTestReview( + Long authorId, String content, String nickname, String profileImage, Double rating) { + + java.util.Map review = new java.util.HashMap<>(); + + // ๋ฆฌ๋ทฐ ๊ธฐ๋ณธ ์ •๋ณด + review.put("reviewId", 1000L + authorId); + review.put("reviewContent", content); + review.put("reviewImageUrl", "review_image_" + authorId + ".jpg"); + review.put("createAt", java.time.LocalDateTime.now().minusDays(authorId)); + review.put("totalImageCount", 2L); + + // ์ž‘์„ฑ์ž ์ •๋ณด (์ฐจ๋‹จ ํ•„ํ„ฐ๋ง ๋Œ€์ƒ) + review.put("authorId", authorId); + review.put("authorNickname", nickname); + review.put("authorProfileImage", profileImage); + + // ์‚ฌ์šฉ์ž ์ •๋ณด (UserInfo ๊ตฌ์กฐ ๋ชจ๋ฐฉ) + review.put("userInfo", java.util.Map.of( + "userId", authorId, + "nickName", nickname, + "userProfileImage", profileImage + )); + + // ๋ฆฌ๋ทฐ ์ƒํƒœ ๋ฐ ํ‰์  + review.put("isMyReview", false); + review.put("status", "PUBLIC"); + review.put("isBestReview", rating >= 4.5); + review.put("rating", rating); + + // ์ƒํ˜ธ์ž‘์šฉ ์ •๋ณด + review.put("likeCount", (long) (Math.random() * 20)); + review.put("replyCount", (long) (Math.random() * 10)); + review.put("isLikedByMe", false); + review.put("hasReplyByMe", false); + review.put("viewCount", (long) (Math.random() * 100)); + + // ๊ธฐํƒ€ ์ •๋ณด + review.put("price", java.math.BigDecimal.valueOf(50000 + (authorId * 10000))); + review.put("sizeType", "BOTTLE_750"); + review.put("locationInfo", "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ"); + review.put("tastingTagList", "์Šค๋ชจํ‚ค, ์˜คํฌ, ๋ฐ”๋‹๋ผ"); + + return review; + } + + /** + * ํ…Œ์ŠคํŠธ์šฉ SecurityContext ๋ชจํ‚น ๋ฉ”์„œ๋“œ + * ์‹ค์ œ ์šด์˜์—์„œ๋Š” JWT ํ† ํฐ์œผ๋กœ SecurityContext๊ฐ€ ์„ค์ •๋˜์ง€๋งŒ, + * ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์ง์ ‘ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + private void mockSecurityContext(Long userId) { + try { + // SecurityContext์— ์ง์ ‘ ์‚ฌ์šฉ์ž ID ์„ค์ • + // ์ฃผ์˜: ์ด๋Š” ํ…Œ์ŠคํŠธ์šฉ์ด๋ฏ€๋กœ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ! + org.springframework.security.core.context.SecurityContextHolder.getContext() + .setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken( + userId.toString(), null, "ROLE_USER")); + + log.debug("ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ • ์™„๋ฃŒ: {}", userId); + } catch (Exception e) { + log.warn("ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ • ์‹คํŒจ: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/app/bottlenote/global/security/SecurityContextUtil.java b/src/main/java/app/bottlenote/global/security/SecurityContextUtil.java index 6ffec6cd1..c416a3d30 100644 --- a/src/main/java/app/bottlenote/global/security/SecurityContextUtil.java +++ b/src/main/java/app/bottlenote/global/security/SecurityContextUtil.java @@ -46,6 +46,18 @@ public static Optional getUserIdByContext() { } Object principal = authentication.getPrincipal(); + + // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ TestingAuthenticationToken ์ง€์› + if (authentication.getClass().getSimpleName().equals("TestingAuthenticationToken")) { + try { + Long userId = Long.parseLong(principal.toString()); + log.debug("ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ: {}", userId); + return Optional.of(userId); + } catch (NumberFormatException e) { + log.warn("ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ์ž ID ํŒŒ์‹ฑ ์‹คํŒจ: {}", principal); + return Optional.empty(); + } + } if (!(principal instanceof CustomUserContext customUserContext)) { log.debug("์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๊ฐ€ CustomUserContext์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋น„ํšŒ์› ์‚ฌ์šฉ์ž์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); return Optional.empty(); diff --git a/src/main/java/app/bottlenote/global/service/BlockService.java b/src/main/java/app/bottlenote/global/service/BlockService.java new file mode 100644 index 000000000..40982dd6e --- /dev/null +++ b/src/main/java/app/bottlenote/global/service/BlockService.java @@ -0,0 +1,262 @@ +package app.bottlenote.global.service; + +import app.bottlenote.user.domain.UserBlock; +import app.bottlenote.user.domain.UserBlockRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค + * DB ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, Redis ์บ์‹œ๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BlockService { + + private final UserBlockRepository userBlockRepository; + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ + * Redis ์บ์‹œ๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ ์ตœ์ ํ™” + * + * @param userId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + */ + @Cacheable( + value = "blocked_users", + key = "#userId", + unless = "#userId == null or #userId == -1", + condition = "#userId != null and #userId != -1" + ) + @Transactional(readOnly = true) + public Set getBlockedUserIds(Long userId) { + if (userId == null || userId == -1L) { + return Collections.emptySet(); + } + + try { + Set blockedUsers = userBlockRepository.findBlockedUserIdsByBlockerId(userId); + log.debug("์‚ฌ์šฉ์ž {}์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ: {} ๋ช…", userId, blockedUsers.size()); + return blockedUsers; + } catch (Exception e) { + log.error("์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ - userId: {}, error: {}", userId, e.getMessage()); + return Collections.emptySet(); + } + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ–ˆ๋Š”์ง€ ํ™•์ธ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + @Transactional(readOnly = true) + public boolean isBlocked(Long blockerId, Long blockedId) { + if (blockerId == null || blockedId == null || blockerId == -1L || blockerId.equals(blockedId)) { + return false; + } + + try { + return userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId); + } catch (Exception e) { + log.error("์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", + blockerId, blockedId, e.getMessage()); + return false; + } + } + + /** + * ์ƒํ˜ธ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์„œ๋กœ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) + * + * @param userId1 ์‚ฌ์šฉ์ž 1 + * @param userId2 ์‚ฌ์šฉ์ž 2 + * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + @Transactional(readOnly = true) + public boolean isMutuallyBlocked(Long userId1, Long userId2) { + if (userId1 == null || userId2 == null || userId1.equals(userId2)) { + return false; + } + + try { + return userBlockRepository.existsMutualBlock(userId1, userId2); + } catch (Exception e) { + log.error("์ƒํ˜ธ ์ฐจ๋‹จ ํ™•์ธ ์‹คํŒจ - userId1: {}, userId2: {}, error: {}", + userId1, userId2, e.getMessage()); + return false; + } + } + + /** + * ๋ฐฐ์น˜๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์„ฑ๋Šฅ ์ตœ์ ํ™”) + * + * @param userIds ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + * @return ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ๋ชฉ๋ก ๋งต + */ + @Transactional(readOnly = true) + public Map> getBulkBlockedUsers(Set userIds) { + if (userIds == null || userIds.isEmpty()) { + return Collections.emptyMap(); + } + + return userIds.stream() + .filter(id -> id != null && id != -1L) + .collect(Collectors.toMap( + Function.identity(), + this::getBlockedUserIds, + (existing, replacement) -> existing // ์ค‘๋ณต ํ‚ค ์ฒ˜๋ฆฌ + )); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‹ค์ œ ์šด์˜์šฉ ๋ฉ”์„œ๋“œ) + * + * @param currentUserId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param targetUserId ์ฐจ๋‹จํ•  ์‚ฌ์šฉ์ž ID + * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  + * @return ์ƒ์„ฑ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#currentUserId") + public UserBlock blockUser(Long currentUserId, Long targetUserId, String reason) { + validateBlockRequest(currentUserId, targetUserId); + return createBlock(currentUserId, targetUserId, reason); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ (์‹ค์ œ ์šด์˜์šฉ ๋ฉ”์„œ๋“œ) + * + * @param currentUserId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param targetUserId ์ฐจ๋‹จ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#currentUserId") + public void unblockUser(Long currentUserId, Long targetUserId) { + validateBlockRequest(currentUserId, targetUserId); + removeBlock(currentUserId, targetUserId); + } + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (๋‚ด๋ถ€์šฉ - ํ…Œ์ŠคํŠธ์—์„œ๋„ ์‚ฌ์šฉ) + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋  ์‚ฌ์šฉ์ž ID + * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  + * @return ์ƒ์„ฑ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public UserBlock createBlock(Long blockerId, Long blockedId, String reason) { + validateBlockRequest(blockerId, blockedId); + + // ์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ธ์ง€ ํ™•์ธ + if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { + log.warn("์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค - blockerId: {}, blockedId: {}", blockerId, blockedId); + return userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) + .orElseThrow(() -> new IllegalStateException("์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ")); + } + + try { + UserBlock userBlock = UserBlock.create(blockerId, blockedId, reason); + UserBlock savedBlock = userBlockRepository.save(userBlock); + + log.info("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์™„๋ฃŒ - blockerId: {}, blockedId: {}, reason: {}", + blockerId, blockedId, reason); + + return savedBlock; + } catch (Exception e) { + log.error("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", + blockerId, blockedId, e.getMessage()); + throw new RuntimeException("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public void removeBlock(Long blockerId, Long blockedId) { + validateBlockRequest(blockerId, blockedId); + + try { + UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) + .orElse(null); + + if (userBlock != null) { + userBlockRepository.delete(userBlock); + log.info("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์™„๋ฃŒ - blockerId: {}, blockedId: {}", blockerId, blockedId); + } else { + log.warn("ํ•ด์ œํ•  ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค - blockerId: {}, blockedId: {}", blockerId, blockedId); + } + } catch (Exception e) { + log.error("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", + blockerId, blockedId, e.getMessage()); + throw new RuntimeException("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + } + } + + /** + * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + private void validateBlockRequest(Long blockerId, Long blockedId) { + if (blockerId == null || blockedId == null) { + throw new IllegalArgumentException("์‚ฌ์šฉ์ž ID๋Š” null์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (blockerId.equals(blockedId)) { + throw new IllegalArgumentException("์ž๊ธฐ ์ž์‹ ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (blockerId == -1L) { + throw new IllegalArgumentException("์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ํ†ต๊ณ„ ์ •๋ณด + */ + @Transactional(readOnly = true) + public BlockStatistics getBlockStatistics(Long userId) { + if (userId == null || userId == -1L) { + return new BlockStatistics(0L, 0L); + } + + try { + long blockedCount = userBlockRepository.countByBlockerId(userId); + long blockedByCount = userBlockRepository.countByBlockedId(userId); + + return new BlockStatistics(blockedCount, blockedByCount); + } catch (Exception e) { + log.error("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ - userId: {}, error: {}", userId, e.getMessage()); + return new BlockStatistics(0L, 0L); + } + } + + /** + * ์ฐจ๋‹จ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” record + */ + public record BlockStatistics( + long blockedCount, // ๋‚ด๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + long blockedByCount // ๋‚˜๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + ) {} +} diff --git a/src/main/java/app/bottlenote/review/controller/ReviewController.java b/src/main/java/app/bottlenote/review/controller/ReviewController.java index 012d9476e..e5b965996 100644 --- a/src/main/java/app/bottlenote/review/controller/ReviewController.java +++ b/src/main/java/app/bottlenote/review/controller/ReviewController.java @@ -1,5 +1,6 @@ package app.bottlenote.review.controller; +import app.bottlenote.global.annotation.BlockFilter; import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.security.SecurityContextUtil; import app.bottlenote.global.service.cursor.PageResponse; @@ -42,6 +43,7 @@ public ResponseEntity createReview(@RequestBody @Valid ReviewCreateRequest re return GlobalResponse.ok(reviewService.createReview(reviewCreateRequest, currentUserId)); } + @BlockFilter(userField = "authorId") @GetMapping("/{alcoholId}") public ResponseEntity getReviews(@PathVariable Long alcoholId, @ModelAttribute ReviewPageableRequest reviewPageableRequest) { Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); @@ -54,6 +56,7 @@ public ResponseEntity getReviews(@PathVariable Long alcoholId, @ModelAttribut return GlobalResponse.ok(pageResponse.content(), MetaService.createMetaInfo().add("pageable", pageResponse.cursorPageable())); } + @BlockFilter(userField = "authorId") @GetMapping("/detail/{reviewId}") public ResponseEntity getDetailReview(@PathVariable Long reviewId) { diff --git a/src/main/java/app/bottlenote/user/domain/UserBlock.java b/src/main/java/app/bottlenote/user/domain/UserBlock.java new file mode 100644 index 000000000..e16958ea8 --- /dev/null +++ b/src/main/java/app/bottlenote/user/domain/UserBlock.java @@ -0,0 +1,70 @@ +package app.bottlenote.user.domain; + +import app.bottlenote.common.domain.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +/** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ + */ +@Getter +@Entity(name = "userBlock") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "user_block", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_blocker_blocked", + columnNames = {"blocker_id", "blocked_id"} + ) + }, + indexes = { + @Index(name = "idx_user_block_blocker", columnList = "blocker_id"), + @Index(name = "idx_user_block_blocked", columnList = "blocked_id") + } +) +public class UserBlock extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("์ฐจ๋‹จ ๊ด€๊ณ„ ID") + private Long id; + + @Column(name = "blocker_id", nullable = false) + @Comment("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID") + private Long blockerId; + + @Column(name = "blocked_id", nullable = false) + @Comment("์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID") + private Long blockedId; + + @Column(name = "block_reason", length = 500) + @Comment("์ฐจ๋‹จ ์‚ฌ์œ ") + private String blockReason; + + @Builder + public UserBlock(Long blockerId, Long blockedId, String blockReason) { + this.blockerId = blockerId; + this.blockedId = blockedId; + this.blockReason = blockReason; + } + + public static UserBlock create(Long blockerId, Long blockedId, String reason) { + return UserBlock.builder() + .blockerId(blockerId) + .blockedId(blockedId) + .blockReason(reason) + .build(); + } +} diff --git a/src/main/java/app/bottlenote/user/domain/UserBlockRepository.java b/src/main/java/app/bottlenote/user/domain/UserBlockRepository.java new file mode 100644 index 000000000..1fb9ee847 --- /dev/null +++ b/src/main/java/app/bottlenote/user/domain/UserBlockRepository.java @@ -0,0 +1,71 @@ +package app.bottlenote.user.domain; + +import java.util.Optional; +import java.util.Set; + +/** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๊ณ„ Repository + */ +public interface UserBlockRepository { + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ €์žฅ + */ + UserBlock save(UserBlock userBlock); + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์‚ญ์ œ + */ + void delete(UserBlock userBlock); + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก ์กฐํšŒ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + */ + Set findBlockedUserIdsByBlockerId(Long blockerId); + + /** + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ๊ด€๊ณ„ ์กด์žฌ ์—ฌ๋ถ€ + */ + boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + + /** + * ํŠน์ • ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ๊ด€๊ณ„ ์—”ํ‹ฐํ‹ฐ + */ + Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + long countByBlockedId(Long blockedId); + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + long countByBlockerId(Long blockerId); + + /** + * ์ƒํ˜ธ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์„œ๋กœ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) + * + * @param userId1 ์‚ฌ์šฉ์ž 1 + * @param userId2 ์‚ฌ์šฉ์ž 2 + * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + boolean existsMutualBlock(Long userId1, Long userId2); +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java b/src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java new file mode 100644 index 000000000..9325db9bb --- /dev/null +++ b/src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java @@ -0,0 +1,37 @@ +package app.bottlenote.user.repository; + +import app.bottlenote.user.domain.UserBlock; +import app.bottlenote.user.domain.UserBlockRepository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.Set; + +@Repository +public interface JpaUserBlockRepository extends UserBlockRepository, JpaRepository { + + @Override + @Query("SELECT ub.blockedId FROM userBlock ub WHERE ub.blockerId = :blockerId") + Set findBlockedUserIdsByBlockerId(@Param("blockerId") Long blockerId); + + @Override + boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + + @Override + Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + + @Override + long countByBlockedId(Long blockedId); + + @Override + long countByBlockerId(Long blockerId); + + @Override + @Query("SELECT COUNT(ub) = 2 FROM userBlock ub " + + "WHERE (ub.blockerId = :userId1 AND ub.blockedId = :userId2) " + + "OR (ub.blockerId = :userId2 AND ub.blockedId = :userId1)") + boolean existsMutualBlock(@Param("userId1") Long userId1, @Param("userId2") Long userId2); +} \ No newline at end of file From 9d7508d77f6a376d0a40d03ec2c433fe339ead1f Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 02:10:25 +0900 Subject: [PATCH 02/16] feat: implement block filtering functionality with @BlockFilter annotation and AOP support Signed-off-by: hgkim --- http/block/block-blur-test.http | 10 + .../block/annotation/BlockFilter.java.bak | 38 +++ .../common/block/annotation/BlockWord.java | 28 ++ .../block/aspect/BlockFilterAspect.java.bak | 238 ++++++++++++++++ .../common/block/config/BlockWordConfig.java | 29 ++ .../controller/BlockTestController.java.bak | 70 +++++ .../block/serializer/BlockWordSerializer.java | 150 ++++++++++ .../common/block/service/BlockService.java | 48 ++++ .../java/app/bottlenote/common/block/v1.0.md | 158 +++++++++++ .../java/app/bottlenote/common/block/v1.1.md | 189 +++++++++++++ .../java/app/bottlenote/common/block/v1.3.md | 181 ++++++++++++ .../java/app/bottlenote/common/block/v1.4.md | 154 ++++++++++ .../java/app/bottlenote/common/block/v1.5.md | 192 +++++++++++++ .../global/annotation/BlockFilter.java | 26 -- .../global/aspect/BlockFilterAspect.java | 228 --------------- .../controller/BlockTestController.java | 248 ----------------- .../global/service/BlockService.java | 262 ------------------ .../review/controller/ReviewController.java | 2 +- 18 files changed, 1486 insertions(+), 765 deletions(-) create mode 100644 http/block/block-blur-test.http create mode 100644 src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak create mode 100644 src/main/java/app/bottlenote/common/block/annotation/BlockWord.java create mode 100644 src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak create mode 100644 src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java create mode 100644 src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak create mode 100644 src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java create mode 100644 src/main/java/app/bottlenote/common/block/service/BlockService.java create mode 100644 src/main/java/app/bottlenote/common/block/v1.0.md create mode 100644 src/main/java/app/bottlenote/common/block/v1.1.md create mode 100644 src/main/java/app/bottlenote/common/block/v1.3.md create mode 100644 src/main/java/app/bottlenote/common/block/v1.4.md create mode 100644 src/main/java/app/bottlenote/common/block/v1.5.md delete mode 100644 src/main/java/app/bottlenote/global/annotation/BlockFilter.java delete mode 100644 src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java delete mode 100644 src/main/java/app/bottlenote/global/controller/BlockTestController.java delete mode 100644 src/main/java/app/bottlenote/global/service/BlockService.java diff --git a/http/block/block-blur-test.http b/http/block/block-blur-test.http new file mode 100644 index 000000000..b201ca1a9 --- /dev/null +++ b/http/block/block-blur-test.http @@ -0,0 +1,10 @@ +### ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ - ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ์‚ฌ์šฉ์ž 1์˜ ๊ฒฝ์šฐ +# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ–ˆ์œผ๋ฏ€๋กœ ํ•ด๋‹น ๋ฆฌ๋ทฐ๋“ค์ด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ +GET {{host}}/api/v1/test/block/reviews +Content-Type: application/json + +### ๊ฒฐ๊ณผ ํ™•์ธ: +# - ์‚ฌ์šฉ์ž 2,3์˜ reviewContent๊ฐ€ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋ณ€๊ฒฝ +# - ์‚ฌ์šฉ์ž 2,3์˜ authorNickname์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋ณ€๊ฒฝ +# - ์‚ฌ์šฉ์ž 2,3์˜ authorProfileImage๊ฐ€ null๋กœ ๋ณ€๊ฒฝ +# - ์‚ฌ์šฉ์ž 1,4,5์˜ ๋ฆฌ๋ทฐ๋Š” ์ •์ƒ ํ‘œ์‹œ \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak b/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak new file mode 100644 index 000000000..001bf0512 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak @@ -0,0 +1,38 @@ +package app.bottlenote.common.block.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ + * + * ์‚ฌ์šฉ ์˜ˆ์‹œ: + * @BlockFilter( + * userField = "authorId", + * blurFields = {"reviewContent", "authorNickname", "authorProfileImage"} + * ) + * @GetMapping("/reviews") + * public ResponseEntity getReviews() { ... } + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockFilter { + + /** + * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… + * ์˜ˆ: "authorId", "userId", "writerId" + */ + String userField() default "authorId"; + + /** + * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•  ํ•„๋“œ๋ช… ๋ฐฐ์—ด + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ ์ด ํ•„๋“œ๋“ค์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋Œ€์ฒด๋จ + */ + String[] blurFields() default { + "content", "reviewContent", "title", + "authorNickname", "nickname", + "authorProfileImage", "profileImage" + }; +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java b/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java new file mode 100644 index 000000000..f4ac91625 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java @@ -0,0 +1,28 @@ +package app.bottlenote.common.block.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ + * JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockWord { + + /** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" + */ + String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; + + /** + * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ + * ๊ธฐ๋ณธ๊ฐ’: "userId" + * ์ค‘์ฒฉ ๊ฐ์ฒด ์˜ˆ์‹œ: "userInfo.userId" + */ + String userIdPath() default "userId"; +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak b/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak new file mode 100644 index 000000000..bbe8632a7 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak @@ -0,0 +1,238 @@ +package app.bottlenote.common.block.aspect; + +import app.bottlenote.common.block.annotation.BlockFilter; +import app.bottlenote.common.block.service.BlockService; +import app.bottlenote.global.data.response.GlobalResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ํ•„ํ„ฐ AOP + * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class BlockFilterAspect { + + private final BlockService blockService; + private final ObjectMapper objectMapper; + + @Around("@annotation(blockFilter)") + public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) throws Throwable { + + // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ + Object result = joinPoint.proceed(); + + // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ (ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ—ค๋”์—์„œ ๊ฐ€์ ธ์˜ด) + Long currentUserId = extractCurrentUserId(); + + if (currentUserId == -1L) { + log.debug("์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž - ์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉํ•˜์ง€ ์•Š์Œ"); + return result; + } + + // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ + Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); + + if (blockedUserIds.isEmpty()) { + log.debug("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Œ - ํ•„ํ„ฐ๋ง ๊ฑด๋„ˆ๋œ€"); + return result; + } + + log.info("์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉ: ์‚ฌ์šฉ์ž {}, ์ฐจ๋‹จ ๋Œ€์ƒ {}", currentUserId, blockedUserIds); + + // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ + return applyBlurFilter(result, blockedUserIds, blockFilter); + } + + /** + * ํ…Œ์ŠคํŠธ์šฉ: ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ + * ์‹ค์ œ ์šด์˜์—์„œ๋Š” SecurityContext์—์„œ ๊ฐ€์ ธ์˜ด + */ + private Long extractCurrentUserId() { + // ํ˜„์žฌ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๊ณ ์ •๊ฐ’ ๋ฐ˜ํ™˜ + // ์‹ค์ œ๋กœ๋Š” SecurityContextUtil.getUserIdByContext() ์‚ฌ์šฉ + return 1L; // ํ…Œ์ŠคํŠธ์šฉ: ์‚ฌ์šฉ์ž 1๋กœ ๊ณ ์ • + } + + /** + * ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ + */ + private Object applyBlurFilter(Object result, Set blockedUserIds, BlockFilter blockFilter) { + + if (!(result instanceof ResponseEntity)) { + return result; + } + + ResponseEntity responseEntity = (ResponseEntity) result; + Object body = responseEntity.getBody(); + + if (!(body instanceof GlobalResponse)) { + return result; + } + + GlobalResponse globalResponse = (GlobalResponse) body; + Object data = globalResponse.getData(); + + if (data == null) { + return result; + } + + // ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + Object blurredData; + if (data instanceof List) { + blurredData = blurListData((List) data, blockedUserIds, blockFilter); + } else { + blurredData = blurSingleData(data, blockedUserIds, blockFilter); + } + + // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ์šด ์‘๋‹ต ์ƒ์„ฑ + GlobalResponse blurredResponse = GlobalResponse.success(blurredData); + return ResponseEntity.ok(blurredResponse); + } + + /** + * List ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + @SuppressWarnings("unchecked") + private List blurListData(List dataList, Set blockedUserIds, BlockFilter blockFilter) { + return dataList.stream() + .map(item -> blurItemIfBlocked(item, blockedUserIds, blockFilter)) + .collect(Collectors.toList()); + } + + /** + * ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + private Object blurSingleData(Object data, Set blockedUserIds, BlockFilter blockFilter) { + return blurItemIfBlocked(data, blockedUserIds, blockFilter); + } + + /** + * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + */ + private Object blurItemIfBlocked(Object item, Set blockedUserIds, BlockFilter blockFilter) { + if (item == null) { + return item; + } + + // ์‚ฌ์šฉ์ž ID ์ถ”์ถœ + Long userId = extractUserIdFromObject(item, blockFilter.userField()); + + if (userId == null || !blockedUserIds.contains(userId)) { + return item; // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์›๋ณธ ๋ฐ˜ํ™˜ + } + + log.debug("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ  ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ: userId={}", userId); + + // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฉด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ + return applyBlurToItem(item, blockFilter.blurFields()); + } + + /** + * ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœ + */ + private Long extractUserIdFromObject(Object obj, String fieldName) { + try { + Field field = findFieldInClass(obj.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + Object value = field.get(obj); + if (value instanceof Long) { + return (Long) value; + } + } + } catch (Exception e) { + log.warn("์‚ฌ์šฉ์ž ID ์ถ”์ถœ ์‹คํŒจ: ๊ฐ์ฒด={}, ํ•„๋“œ={}", obj.getClass().getSimpleName(), fieldName); + } + return null; + } + + /** + * ํด๋ž˜์Šค ๊ณ„์ธต๊ตฌ์กฐ์—์„œ ํ•„๋“œ ์ฐพ๊ธฐ + */ + private Field findFieldInClass(Class clazz, String fieldName) { + Class currentClass = clazz; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + return null; + } + + /** + * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ + */ + private Object applyBlurToItem(Object originalItem, String[] blurFields) { + try { + // Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ + Object clonedItem = deepCloneObject(originalItem); + + // ์ง€์ •๋œ ํ•„๋“œ๋“ค์„ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น + for (String fieldName : blurFields) { + maskField(clonedItem, fieldName, "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); + } + + return clonedItem; + + } catch (Exception e) { + log.error("๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์‹คํŒจ: {}", e.getMessage()); + return originalItem; // ์‹คํŒจ ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ + } + } + + /** + * ๊ฐ์ฒด์˜ ํŠน์ • ํ•„๋“œ๋ฅผ ๋งˆ์Šคํ‚น + */ + private void maskField(Object obj, String fieldName, String maskValue) { + try { + Field field = findFieldInClass(obj.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + + // ํ•„๋“œ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋งˆ์Šคํ‚น ๊ฐ’ ์„ค์ • + if (fieldName.toLowerCase().contains("image") || fieldName.toLowerCase().contains("url")) { + field.set(obj, null); // ์ด๋ฏธ์ง€/URL์€ null๋กœ ์ฒ˜๋ฆฌ + } else { + field.set(obj, maskValue); // ํ…์ŠคํŠธ๋Š” ๋งˆ์Šคํ‚น ๋ฉ”์‹œ์ง€๋กœ ์ฒ˜๋ฆฌ + } + } + } catch (Exception e) { + // ํ•„๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์„ค์ • ์‹คํŒจ ์‹œ ๋ฌด์‹œ (์ •์ƒ์ ์ธ ์ƒํ™ฉ) + log.debug("ํ•„๋“œ ๋งˆ์Šคํ‚น ๊ฑด๋„ˆ๋œ€: {}.{}", obj.getClass().getSimpleName(), fieldName); + } + } + + /** + * Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ + */ + private Object deepCloneObject(Object original) { + try { + if (original == null) { + return null; + } + + String jsonString = objectMapper.writeValueAsString(original); + return objectMapper.readValue(jsonString, original.getClass()); + } catch (Exception e) { + log.warn("๊ฐ์ฒด ๋ณต์‚ฌ ์‹คํŒจ, ์›๋ณธ ๋ฐ˜ํ™˜: {}", e.getMessage()); + return original; + } + } +} diff --git a/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java new file mode 100644 index 000000000..5056a6cff --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java @@ -0,0 +1,29 @@ +package app.bottlenote.common.block.config; + +import app.bottlenote.common.block.serializer.BlockWordSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; + +/** + * BlockWord ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์„ค์ • + */ +@Configuration +@RequiredArgsConstructor +public class BlockWordConfig { + + private final ObjectMapper objectMapper; + private final BlockWordSerializer blockWordSerializer; + + @PostConstruct + public void configureObjectMapper() { + SimpleModule blockWordModule = new SimpleModule("BlockWordModule"); + + // String ํƒ€์ž…์— ๋Œ€ํ•ด @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์„ ๋•Œ ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์‚ฌ์šฉ + blockWordModule.addSerializer(String.class, blockWordSerializer); + + objectMapper.registerModule(blockWordModule); + } +} diff --git a/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak b/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak new file mode 100644 index 000000000..61da4b552 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak @@ -0,0 +1,70 @@ +package app.bottlenote.common.block.controller; + +import app.bottlenote.common.block.annotation.BlockFilter; +import app.bottlenote.global.data.response.GlobalResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ + * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๋งŒ ์ง‘์ค‘ + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/test/block") +@Profile({"local", "dev", "test", "!prod"}) +public class BlockTestController { + + /** + * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ชฉ๋ก API + * + * ์ฐจ๋‹จ ๊ด€๊ณ„ (ํ•˜๋“œ์ฝ”๋”ฉ): + * - ์‚ฌ์šฉ์ž 1 โ†’ ์‚ฌ์šฉ์ž 2,3 ์ฐจ๋‹จ + * - ์‚ฌ์šฉ์ž 2 โ†’ ์‚ฌ์šฉ์ž 4 ์ฐจ๋‹จ + * - ์‚ฌ์šฉ์ž 3 โ†’ ์‚ฌ์šฉ์ž 1,5 ์ฐจ๋‹จ + */ + @BlockFilter( + userField = "authorId", + blurFields = {"reviewContent", "authorNickname", "authorProfileImage"} + ) + @GetMapping("/reviews") + public ResponseEntity getTestReviews() { + + log.info("๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ"); + + List> reviews = List.of( + createReview(1L, "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค!", "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", "profile1.jpg"), + createReview(2L, "์ฒซ ์‹œ์Œ์ธ๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ์ข‹๋„ค์š”", "์ดˆ๋ณด์œ„์Šคํ‚ค", "profile2.jpg"), + createReview(3L, "๊ฐ€๊ฒฉ ๋Œ€๋น„ ํ›Œ๋ฅญํ•ฉ๋‹ˆ๋‹ค", "์œ„์Šคํ‚ค๋งˆ๋‹ˆ์•„", "profile3.jpg"), + createReview(4L, "์˜คํฌํ–ฅ์ด ๊ฐ•ํ•ด์„œ ํ˜ธ๋ถˆํ˜ธ๊ฐ€ ๊ฐˆ๋ฆด๋“ฏ", "๋ฆฌ๋ทฐ๊ณ ์ˆ˜", "profile4.jpg"), + createReview(5L, "์นœ๊ตฌ๋“ค๊ณผ ๋งˆ์‹œ๊ธฐ ์ข‹์•˜์–ด์š”", "์†Œ์…œ๋“œ๋ง์ปค", "profile5.jpg") + ); + + return GlobalResponse.ok(Map.of( + "totalCount", reviews.size(), + "reviews", reviews + )); + } + + /** + * ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + */ + private Map createReview(Long authorId, String content, String nickname, String profileImage) { + return Map.of( + "reviewId", 1000L + authorId, + "authorId", authorId, + "reviewContent", content, + "authorNickname", nickname, + "authorProfileImage", profileImage, + "createAt", LocalDateTime.now().minusDays(authorId), + "likeCount", (int)(Math.random() * 20), + "rating", 4.0 + (Math.random() * 1.0) + ); + } +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java new file mode 100644 index 000000000..f2ef60231 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java @@ -0,0 +1,150 @@ +package app.bottlenote.common.block.serializer; + +import app.bottlenote.common.block.annotation.BlockWord; +import app.bottlenote.common.block.service.BlockService; +import app.bottlenote.global.security.SecurityContextUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.lang.reflect.Field; + +/** + * @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋œ ํ•„๋“œ์˜ Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer { + + private final BlockService blockService; + private BlockWord blockWordAnnotation; + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (blockWordAnnotation == null) { + gen.writeString(value); + return; + } + + try { + // ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ + Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(null); + if (currentUserId == null) { + // ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ํ•„ํ„ฐ๋ง ์Šคํ‚ต + gen.writeString(value); + return; + } + + // ํ˜„์žฌ ์ง๋ ฌํ™” ์ค‘์ธ ๊ฐ์ฒด ํš๋“ + Object currentObject = gen.getCurrentValue(); + if (currentObject == null) { + gen.writeString(value); + return; + } + + // userIdPath๋กœ ์ž‘์„ฑ์ž ID ์ถ”์ถœ + Long authorId = extractUserIdByPath(currentObject, blockWordAnnotation.userIdPath()); + if (authorId == null) { + gen.writeString(value); + return; + } + + // ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ + boolean isBlocked = blockService.isBlocked(currentUserId, authorId); + if (isBlocked) { + // ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ + gen.writeString(blockWordAnnotation.value()); + } else { + // ์ •์ƒ์ธ ๊ฒฝ์šฐ ์›๋ณธ ๊ฐ’ ์‚ฌ์šฉ + gen.writeString(value); + } + + } catch (Exception e) { + log.warn("BlockWordSerializer ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ. ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜: {}", e.getMessage()); + gen.writeString(value); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { + + if (property != null) { + BlockWord annotation = property.getAnnotation(BlockWord.class); + if (annotation != null) { + BlockWordSerializer serializer = new BlockWordSerializer(blockService); + serializer.blockWordAnnotation = annotation; + return serializer; + } + } + + return this; + } + + /** + * ์ง€์ •๋œ ๊ฒฝ๋กœ๋กœ ๊ฐ์ฒด์—์„œ userId ์ถ”์ถœ + * + * @param object ๋Œ€์ƒ ๊ฐ์ฒด + * @param userIdPath userId ๊ฒฝ๋กœ (์˜ˆ: "userId", "userInfo.userId") + * @return ์ถ”์ถœ๋œ userId, ์‹คํŒจ์‹œ null + */ + private Long extractUserIdByPath(Object object, String userIdPath) { + try { + String[] pathParts = userIdPath.split("\\."); + Object currentObject = object; + + // ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ๊ฐ์ฒด ํƒ์ƒ‰ + for (String part : pathParts) { + if (currentObject == null) { + return null; + } + + Field field = findField(currentObject.getClass(), part); + if (field == null) { + log.warn("ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {} in {}", part, currentObject.getClass().getSimpleName()); + return null; + } + + field.setAccessible(true); + currentObject = field.get(currentObject); + } + + // ์ตœ์ข… ๊ฐ’์ด Long ํƒ€์ž…์ธ์ง€ ํ™•์ธ + if (currentObject instanceof Long) { + return (Long) currentObject; + } else { + log.warn("userId ํ•„๋“œ๊ฐ€ Long ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค: {}", currentObject != null ? currentObject.getClass() : "null"); + return null; + } + + } catch (Exception e) { + log.warn("userId ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {}", e.getMessage()); + return null; + } + } + + /** + * ํด๋ž˜์Šค์—์„œ ํ•„๋“œ ์ฐพ๊ธฐ (์ƒ์† ๊ณ„์ธต ํฌํ•จ) + */ + private Field findField(Class clazz, String fieldName) { + Class currentClass = clazz; + + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/service/BlockService.java b/src/main/java/app/bottlenote/common/block/service/BlockService.java new file mode 100644 index 000000000..6e4072657 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/service/BlockService.java @@ -0,0 +1,48 @@ +package app.bottlenote.common.block.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ์„œ๋น„์Šค + * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ + */ +@Slf4j +@Service +public class BlockService { + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ + * ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + */ + public Set getBlockedUserIds(Long userId) { + if (userId == null || userId == -1L) { + return Set.of(); + } + + // ํ…Œ์ŠคํŠธ์šฉ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ + return switch (userId.intValue()) { + case 1 -> Set.of(2L, 3L); // ์‚ฌ์šฉ์ž 1์€ ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จ + case 2 -> Set.of(4L); // ์‚ฌ์šฉ์ž 2๋Š” ์‚ฌ์šฉ์ž 4๋ฅผ ์ฐจ๋‹จ + case 3 -> Set.of(1L, 5L); // ์‚ฌ์šฉ์ž 3์€ ์‚ฌ์šฉ์ž 1,5๋ฅผ ์ฐจ๋‹จ + default -> Set.of(); // ๋‚˜๋จธ์ง€๋Š” ์ฐจ๋‹จ ์—†์Œ + }; + } + + /** + * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ + */ + public boolean isBlocked(Long blockerId, Long blockedId) { + if (blockerId == null || blockedId == null) { + return false; + } + + Set blockedUsers = getBlockedUserIds(blockerId); + return blockedUsers.contains(blockedId); + } +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.0.md b/src/main/java/app/bottlenote/common/block/v1.0.md new file mode 100644 index 000000000..5cdc7c269 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/v1.0.md @@ -0,0 +1,158 @@ +# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.0 - AOP ๋ฐฉ์‹ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**AOP ๊ธฐ๋ฐ˜ ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ๋ฒจ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์ ์šฉํ•˜์—ฌ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ "๋ธ”๋Ÿฌ" ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹ + +## ๐ŸŽฏ @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜ + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockFilter { + /** + * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… + */ + String userField() default "authorId"; + + /** + * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•  ํ•„๋“œ๋ช… ๋ฐฐ์—ด + */ + String[] blurFields() default { + "content", "reviewContent", "title", + "authorNickname", "nickname", + "authorProfileImage", "profileImage" + }; +} +``` + +## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ + +### ์ปจํŠธ๋กค๋Ÿฌ ์ ์šฉ +```java +@RestController +public class ReviewController { + + @BlockFilter(userField = "authorId") + @GetMapping("/{alcoholId}") + public ResponseEntity getReviews(@PathVariable Long alcoholId) { + // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์‹คํ–‰ + return GlobalResponse.ok(reviewService.getReviews(alcoholId)); + } +} +``` + +## ๐Ÿ”ง ๋™์ž‘ ์›๋ฆฌ + +### 1. AOP Aspect ์ฒ˜๋ฆฌ ํ๋ฆ„ +```java +@Around("@annotation(blockFilter)") +public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) { + // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ + Object result = joinPoint.proceed(); + + // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ + Long currentUserId = extractCurrentUserId(); + + // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ + Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); + + // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ + return applyBlurFilter(result, blockedUserIds, blockFilter); +} +``` + +### 2. ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๊ณผ์ • +1. **ResponseEntity โ†’ GlobalResponse โ†’ data ์ˆœ์œผ๋กœ ํƒ์ƒ‰** +2. **๋ฐ์ดํ„ฐ ํƒ€์ž… ํ™•์ธ**: List ๋˜๋Š” ๋‹จ์ผ ๊ฐ์ฒด +3. **๊ฐ ํ•ญ๋ชฉ๋ณ„๋กœ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ** (`userField` ์‚ฌ์šฉ) +4. **์ฐจ๋‹จ ๋Œ€์ƒ์ธ์ง€ ํ™•์ธ** (blockedUserIds์— ํฌํ•จ ์—ฌ๋ถ€) +5. **์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๊ฐ์ฒด ๋ณต์ œ ํ›„ ํ•„๋“œ ๋งˆ์Šคํ‚น** + +### 3. ํ•„๋“œ ๋งˆ์Šคํ‚น ๊ทœ์น™ +```java +// ํ…์ŠคํŠธ ํ•„๋“œ: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" +reviewContent โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" +authorNickname โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" + +// ์ด๋ฏธ์ง€/URL ํ•„๋“œ: null ์ฒ˜๋ฆฌ +authorProfileImage โ†’ null +imageUrl โ†’ null +``` + +## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + +### ์ •์ƒ ์‚ฌ์šฉ์ž ์‘๋‹ต +```json +{ + "success": true, + "data": { + "reviewList": [ + { + "authorId": 123, + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", + "authorProfileImage": "profile.jpg" + } + ] + } +} +``` + +### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์‘๋‹ต (๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ›„) +```json +{ + "success": true, + "data": { + "reviewList": [ + { + "authorId": 456, + "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "authorProfileImage": null + } + ] + } +} +``` + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๊ตฌ์กฐ + +### ํ•ต์‹ฌ ํŒŒ์ผ๋“ค +- `annotation/BlockFilter.java` - ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ +- `aspect/BlockFilterAspect.java` - AOP ์ฒ˜๋ฆฌ ๋กœ์ง +- `service/BlockService.java` - ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ +- `controller/BlockTestController.java` - ํ…Œ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ + +### ์ฃผ์š” ๋ฉ”์„œ๋“œ +1. **extractCurrentUserId()**: ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ (ํ…Œ์ŠคํŠธ์šฉ ๊ณ ์ •๊ฐ’) +2. **applyBlurFilter()**: ์‘๋‹ต ๋ฐ์ดํ„ฐ ํƒ€์ž…๋ณ„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ„๊ธฐ +3. **blurItemIfBlocked()**: ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ +4. **applyBlurToItem()**: Jackson ๊นŠ์€ ๋ณต์‚ฌ ํ›„ ํ•„๋“œ ๋งˆ์Šคํ‚น +5. **maskField()**: ํ•„๋“œ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋งˆ์Šคํ‚น ๊ฐ’ ์„ค์ • + +## ๐Ÿšง ํ•œ๊ณ„์  + +### 1. ๋ณต์žกํ•œ ์ฒ˜๋ฆฌ ๊ณผ์ • +- **ResponseEntity โ†’ GlobalResponse โ†’ data ๊ตฌ์กฐ ์˜์กด์„ฑ** +- **๋ฆฌํ”Œ๋ ‰์…˜ ๊ธฐ๋ฐ˜ ํ•„๋“œ ์ ‘๊ทผ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ด์Šˆ** +- **Jackson ๊นŠ์€ ๋ณต์‚ฌ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€** + +### 2. ์ปจํŠธ๋กค๋Ÿฌ ๊ฒฐํ•ฉ๋„ +- **๊ฐ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋งˆ๋‹ค ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ ํ•„์š”** +- **userField ์„ค์ •์ด ์‘๋‹ต ๊ตฌ์กฐ์— ์˜์กด์ ** +- **์ƒˆ๋กœ์šด ์‘๋‹ต ๊ตฌ์กฐ ์ถ”๊ฐ€ ์‹œ blurFields ์žฌ์„ค์ • ํ•„์š”** + +### 3. ์œ ์ง€๋ณด์ˆ˜ ์–ด๋ ค์›€ +- **AOP ์ฒ˜๋ฆฌ ๊ณผ์ •์ด ๋ณต์žกํ•˜์—ฌ ๋””๋ฒ„๊น… ์–ด๋ ค์›€** +- **์‘๋‹ต ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ํ•„ํ„ฐ๋ง ๋กœ์ง๋„ ํ•จ๊ป˜ ์ˆ˜์ • ํ•„์š”** +- **ํ…Œ์ŠคํŠธ์šฉ ๊ณ ์ • ์‚ฌ์šฉ์ž ID ๋“ฑ ์ž„์‹œ ๊ตฌํ˜„ ์กด์žฌ** + +### 4. ํ™•์žฅ์„ฑ ์ œํ•œ +- **ํŠน์ • ์‘๋‹ต ๊ตฌ์กฐ(GlobalResponse)์—๋งŒ ๋™์ž‘** +- **์ค‘์ฒฉ ๊ฐ์ฒด ๋‚ด๋ถ€ ํ•„๋“œ ์ฒ˜๋ฆฌ ๋ณต์žก** +- **๋‹ค์–‘ํ•œ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ ์šฉ ์–ด๋ ค์›€** + +## ๐Ÿ“ ๊ฒฐ๋ก  + +AOP ๋ฐฉ์‹์€ ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ๋ฒจ์—์„œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์„ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ๋ณต์žกํ•œ ๊ตฌ์กฐ์™€ ๋†’์€ ๊ฒฐํ•ฉ๋„๋กœ ์ธํ•ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์šด ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ์‘๋‹ต ๊ตฌ์กฐ์— ๋Œ€ํ•œ ์˜์กด์„ฑ์ด ๋†’๊ณ , ์„ฑ๋Šฅ์ƒ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.1.md b/src/main/java/app/bottlenote/common/block/v1.1.md new file mode 100644 index 000000000..f3376315c --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/v1.1.md @@ -0,0 +1,189 @@ +# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.1 - ๋ณต์žกํ•œ Jackson ์ธํ„ฐํŽ˜์ด์Šค ๋ฐฉ์‹ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**์‹ฌํ”Œํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜์„ ํ•„๋“œ์— ์ง์ ‘ ์ ์šฉํ•˜์—ฌ Jackson ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ์‹ + +## ๐ŸŽฏ ํ•ต์‹ฌ ์•„์ด๋””์–ด + +### ๊ธฐ๋ณธ ์›์น™ +- **ํ•„๋“œ ๋ ˆ๋ฒจ ์ œ์–ด**: ์‘๋‹ต ๊ฐ์ฒด ๊ตฌ์กฐ๋Š” ๊ทธ๋Œ€๋กœ, ํ•„๋“œ๋ณ„๋กœ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ +- **๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ๋ฐฉ์‹**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜์ง€ ์•Š๊ณ  ์ง€์ •๋œ ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด +- **Jackson ์ง๋ ฌํ™” ํ™œ์šฉ**: JSON ๋ณ€ํ™˜ ์‹œ์ ์—์„œ ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋ง ์ฒ˜๋ฆฌ +- **์ตœ์†Œํ•œ์˜ ๋ณ€๊ฒฝ**: ๊ธฐ์กด ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ตœ๋Œ€ํ•œ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ธ”๋ก ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +## ๐Ÿ—๏ธ ์„ค๊ณ„ ์ƒ์„ธ + +### 1. ์‘๋‹ต ๊ตฌ์กฐ ์œ ์ง€ +```java +// ReviewListResponse - ๊ธฐ์กด ๊ตฌ์กฐ ๊ทธ๋Œ€๋กœ ์œ ์ง€ +public record ReviewListResponse( + Long totalCount, + List reviewList // ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ +) { + public static ReviewListResponse of(Long totalCount, List reviewList) { + return new ReviewListResponse(totalCount, reviewList); + } +} +``` + +### 2. ํ•„๋“œ ๋ ˆ๋ฒจ ๋ธ”๋ก ํ•„ํ„ฐ๋ง +```java +// ReviewInfo - ํŠน์ • ํ•„๋“œ์—๋งŒ @BlockWord ์ ์šฉ +@Builder +public record ReviewInfo( + Long reviewId, + + @BlockWord // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ ๋‚ด์šฉ ํ•„ํ„ฐ๋ง + String reviewContent, + + String reviewImageUrl, + // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค (๋ธ”๋ก ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š”) + + UserInfo userInfo, // ๋‚ด๋ถ€์—์„œ ๋‹‰๋„ค์ž„ ํ•„ํ„ฐ๋ง + // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ๋“ค +) { +} +``` + +### 3. ์‚ฌ์šฉ์ž ์ •๋ณด ๋ธ”๋ก ํ•„ํ„ฐ๋ง +```java +// UserInfo - ๋‹‰๋„ค์ž„์— ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์ ์šฉ +public record UserInfo( + Long userId, // userId๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ (์ฐจ๋‹จ ๊ด€๊ณ„ ํŒ๋‹จ์šฉ) + + @BlockWord(๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž") // ์ปค์Šคํ…€ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + String nickName, + + String userProfileImage // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ์œ ์ง€ +) { + public static UserInfo of(Long userId, String nickName, String userProfileImage) { + return new UserInfo(userId, nickName, userProfileImage); + } +} +``` + +## ๐Ÿ”ง @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์„ค๊ณ„ + +### ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockWord { + /** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + * ๊ธฐ๋ณธ๊ฐ’: ๋นˆ ๋ฌธ์ž์—ด (์ปจํ…์ธ  ์™„์ „ ์ˆจ๊น€) + */ + String ๋Œ€์ฒด๋ฉ”์‹œ์ง€() default ""; + + /** + * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… + * ๊ธฐ๋ณธ๊ฐ’: ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ ์‚ฌ์šฉ + */ + String userIdField() default "userId"; +} +``` + +### ์‚ฌ์šฉ ์˜ˆ์‹œ +```java +// ์ผ€์ด์Šค 1: ์ปจํ…์ธ  ์™„์ „ ์ˆจ๊น€ +@BlockWord +String reviewContent; // โ†’ "" + +// ์ผ€์ด์Šค 2: ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด +@BlockWord(๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž") +String nickName; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" + +// ์ผ€์ด์Šค 3: ๋‹ค๋ฅธ ํ•„๋“œ์˜ userId ์ฐธ์กฐ +@BlockWord(userIdField = "authorId", ๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ๋Œ“๊ธ€") +String commentContent; +``` + +## ๐ŸŽจ ์ฒ˜๋ฆฌ ํ๋ฆ„ + +### 1. ์ •์ƒ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  +```json +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "userId": 123, + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", + "userProfileImage": "profile.jpg" + } +} +``` + +### 2. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  (ํ•„ํ„ฐ๋ง ํ›„) +```json +{ + "reviewContent": "", // @BlockWord๋กœ ๋นˆ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ + "userInfo": { + "userId": 456, // userId๋Š” ์œ ์ง€ (์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ) + "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", // ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด + "userProfileImage": "profile.jpg" // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ์œ ์ง€ + } +} +``` + +## ๐Ÿ” ๋™์ž‘ ์›๋ฆฌ + +### Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ฒ˜๋ฆฌ ๊ณผ์ • + +1. **ํ•„๋“œ ์Šค์บ”**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํ•„๋“œ ๊ฐ์ง€ +2. **์‚ฌ์šฉ์ž ID ํš๋“**: ํ˜„์žฌ ์š”์ฒญ ์‚ฌ์šฉ์ž์™€ ์ปจํ…์ธ  ์ž‘์„ฑ์ž ID ํ™•์ธ +3. **์ฐจ๋‹จ ๊ด€๊ณ„ ๊ฒ€์‚ฌ**: `BlockService`๋ฅผ ํ†ตํ•ด ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์บ์‹œ ํ™œ์šฉ) +4. **์กฐ๊ฑด๋ถ€ ๋Œ€์ฒด**: ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ง€์ •๋œ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ +5. **JSON ์ถœ๋ ฅ**: ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ JSON์œผ๋กœ ์ง๋ ฌํ™” + +## ๐Ÿš€ ์žฅ์  + +### 1. ์‹ฌํ”Œํ•จ +- **์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ๋ณ€๊ฒฝ**: ๊ธฐ์กด DTO์— ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ +- **์ง๊ด€์ **: ์–ด๋–ค ํ•„๋“œ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ•œ๋ˆˆ์— ํŒŒ์•… ๊ฐ€๋Šฅ +- **์„ค์ • ๋ถˆํ•„์š”**: ๋ณต์žกํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋‚˜ ์„ค์ • ํด๋ž˜์Šค ๋ถˆํ•„์š” + +### 2. ์œ ์—ฐ์„ฑ +- **ํ•„๋“œ๋ณ„ ์ œ์–ด**: ๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๋‹ค๋ฅธ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์„ค์ • ๊ฐ€๋Šฅ +- **์„ ํƒ์  ์ ์šฉ**: ํ•„์š”ํ•œ ํ•„๋“œ์—๋งŒ ์„ ํƒ์ ์œผ๋กœ ์ ์šฉ +- **์ปค์Šคํ„ฐ๋งˆ์ด์ง•**: userIdField๋กœ ๋‹ค์–‘ํ•œ ๊ตฌ์กฐ์— ๋Œ€์‘ + +### 3. ์„ฑ๋Šฅ +- **Jackson ๋ ˆ๋ฒจ ์ฒ˜๋ฆฌ**: ์ง๋ ฌํ™” ์‹œ์ ์—๋งŒ ์‹คํ–‰์œผ๋กœ ํšจ์œจ์  +- **์บ์‹œ ํ™œ์šฉ**: ๊ธฐ์กด BlockService์˜ ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ทธ๋Œ€๋กœ ํ™œ์šฉ +- **์กฐ๊ฑด๋ถ€ ์‹คํ–‰**: ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ฒ˜๋ฆฌ + +### 4. ์œ ์ง€๋ณด์ˆ˜์„ฑ +- **๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ**: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์™„์ „ ๋ถ„๋ฆฌ +- **๊ธฐ์กด ๊ตฌ์กฐ ์œ ์ง€**: ์‘๋‹ต ๊ฐ์ฒด๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ +- **ํ™•์žฅ ์šฉ์ด**: ์ƒˆ๋กœ์šด DTO์— ์‰ฝ๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅ + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๋ฒ”์œ„ + +### Phase 1: ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ +- `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ +- Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๊ตฌํ˜„ +- ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ํš๋“ ๋กœ์ง + +### Phase 2: DTO ์ ์šฉ +- `ReviewInfo`์— `reviewContent` ํ•„๋“œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ +- `UserInfo`์— `nickName` ํ•„๋“œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ + +### Phase 3: ๊ธฐ์กด ์ฝ”๋“œ ์ •๋ฆฌ +- ์ปจํŠธ๋กค๋Ÿฌ์˜ `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ +- AOP ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ฝ”๋“œ ์ œ๊ฑฐ + +## ๐ŸŽฏ ์˜ˆ์ƒ ๊ฒฐ๊ณผ + +### ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜ +- **๊ฐ„๋‹จํ•œ ์ ์šฉ**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ํ•˜๋‚˜๋กœ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์™„์„ฑ +- **๋ช…ํ™•ํ•œ ์˜๋„**: ์ฝ”๋“œ๋งŒ ๋ด๋„ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ๋ธ”๋ก ๋Œ€์ƒ์ธ์ง€ ์ฆ‰์‹œ ํŒŒ์•… +- **๋””๋ฒ„๊น… ์šฉ์ด**: JSON ์‘๋‹ต์—์„œ ๋ฐ”๋กœ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ ํ™•์ธ ๊ฐ€๋Šฅ + +### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ +- **์ž์—ฐ์Šค๋Ÿฌ์šด ์ฐจ๋‹จ**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋Œ€์ฒด๋จ +- **์ •๋ณด ์œ ์ง€**: ์‚ฌ์šฉ์ž ID ๋“ฑ ํ•„์š”ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ +- **์ผ๊ด€์„ฑ**: ๋ชจ๋“  API์—์„œ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ + +## ๐Ÿ“ ๊ฒฐ๋ก  + +`@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์€ ๋ณต์žกํ•œ ์„ค์ • ์—†์ด๋„ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ๋ธ”๋ก ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ตœ๋Œ€ํ•œ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•œ ์šฐ์•„ํ•œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.3.md b/src/main/java/app/bottlenote/common/block/v1.3.md new file mode 100644 index 000000000..1ea582fc0 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/v1.3.md @@ -0,0 +1,181 @@ +# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.3 - ์ค‘์ฒฉ ๊ฐ์ฒด ์ง€์› @BlockWord ๋ฐฉ์‹ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**@BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ + +## ๐ŸŽฏ ํ•ต์‹ฌ ์„ค๊ณ„ + +### @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockWord { + /** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" + */ + String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; + + /** + * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ + * ๊ธฐ๋ณธ๊ฐ’: ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ + * ์ค‘์ฒฉ ๊ฐ์ฒด: "userInfo.userId" ํ˜•ํƒœ ์ง€์› + */ + String userIdPath() default "userId"; +} +``` + +### ์‚ฌ์šฉ ์˜ˆ์‹œ + +#### 1. ๊ธฐ๋ณธ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ +```java +@BlockWord(userIdPath = "userInfo.userId") +String reviewContent; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" +``` + +#### 2. ์ปค์Šคํ…€ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ +```java +@BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userInfo.userId") +String nickName; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" +``` + +#### 3. ์ค‘์ฒฉ ๊ฐ์ฒด์—์„œ userId ์ฐธ์กฐ +```java +// ReviewInfo ๊ตฌ์กฐ +public record ReviewInfo( + @BlockWord(userIdPath = "userInfo.userId") // ๋‚ด๋ถ€ ๊ฐ์ฒด์˜ userId ์ฐธ์กฐ + String reviewContent, + + UserInfo userInfo // userId๋ฅผ ํฌํ•จํ•œ ์ค‘์ฒฉ ๊ฐ์ฒด +) {} + +public record UserInfo( + Long userId, // ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€ + + @BlockWord(userIdPath = "userId") // ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ์ฐธ์กฐ + String nickName +) {} +``` + +## ๐Ÿ”ง ๋™์ž‘ ์›๋ฆฌ + +### 1. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ํš๋“ +- **๋‹จ์ˆœ ๊ฒฝ๋กœ**: `"userId"` โ†’ ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ํ•„๋“œ +- **์ค‘์ฒฉ ๊ฒฝ๋กœ**: `"userInfo.userId"` โ†’ userInfo ๊ฐ์ฒด ๋‚ด๋ถ€์˜ userId ํ•„๋“œ +- **๋ฆฌํ”Œ๋ ‰์…˜ ํ™œ์šฉ**: ๋Ÿฐํƒ€์ž„์— ์ง€์ •๋œ ๊ฒฝ๋กœ๋กœ userId ๊ฐ’ ์ถ”์ถœ + +### 2. ์ฐจ๋‹จ ๊ด€๊ณ„ ๊ฒ€์‚ฌ ํ๋ฆ„ +``` +1. @BlockWord ํ•„๋“œ ๊ฐ์ง€ +2. userIdPath๋กœ ์ž‘์„ฑ์ž userId ์ถ”์ถœ +3. ํ˜„์žฌ ์š”์ฒญ ์‚ฌ์šฉ์ž ID ํš๋“ (SecurityContext) +4. BlockService.isBlocked(currentUserId, authorId) ํ˜ธ์ถœ +5. ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ โ†’ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ +6. ์ •์ƒ์ธ ๊ฒฝ์šฐ โ†’ ์›๋ณธ ๊ฐ’ ์œ ์ง€ +``` + +## ๐Ÿ“ ์ ์šฉ ์˜ˆ์‹œ + +### ReviewInfo ์ ์šฉ +```java +@Builder +public record ReviewInfo( + Long reviewId, + + // ๋ฆฌ๋ทฐ ๋‚ด์šฉ - userInfo ๋‚ด๋ถ€์˜ userId๋กœ ์ฐจ๋‹จ ํŒ๋‹จ + @BlockWord(userIdPath = "userInfo.userId") + String reviewContent, + + String reviewImageUrl, + LocalDateTime createAt, + + // ์‚ฌ์šฉ์ž ์ •๋ณด - ๋‚ด๋ถ€์—์„œ nickName ํ•„ํ„ฐ๋ง + UserInfo userInfo, + + Boolean isMyReview, + // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค +) {} +``` + +### UserInfo ์ ์šฉ +```java +public record UserInfo( + Long userId, // ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€ + + // ๋‹‰๋„ค์ž„ - ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId๋กœ ์ฐจ๋‹จ ํŒ๋‹จ + @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") + String nickName, + + String userProfileImage +) { + public static UserInfo of(Long userId, String nickName, String userProfileImage) { + return new UserInfo(userId, nickName, userProfileImage); + } +} +``` + +## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + +### ์ •์ƒ ์‚ฌ์šฉ์ž ์‘๋‹ต +```json +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "userId": 123, + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", + "userProfileImage": "profile.jpg" + } +} +``` + +### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์‘๋‹ต (ํ•„ํ„ฐ๋ง ํ›„) +```json +{ + "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "userInfo": { + "userId": 456, // userId๋Š” ์œ ์ง€ (์‹œ์Šคํ…œ ๋™์ž‘์šฉ) + "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", + "userProfileImage": "profile.jpg" + } +} +``` + +## ๐Ÿš€ ์ฃผ์š” ํŠน์ง• + +### 1. ์œ ์—ฐํ•œ ๊ฒฝ๋กœ ์ง€์ • +- **๋‹จ์ˆœ ๊ตฌ์กฐ**: `userIdPath = "userId"` +- **์ค‘์ฒฉ ๊ตฌ์กฐ**: `userIdPath = "userInfo.userId"` +- **๊นŠ์€ ์ค‘์ฒฉ**: `userIdPath = "author.profile.userId"` (ํ•„์š”์‹œ ํ™•์žฅ) + +### 2. ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต +- **๋Œ€์ฒด ๋ฉ”์‹œ์ง€**: ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" +- **๊ฒฝ๋กœ**: ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ ์‚ฌ์šฉ + +### 3. ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **์บ์‹œ ํ™œ์šฉ**: BlockService์˜ ๊ธฐ์กด ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ํ™œ์šฉ +- **์กฐ๊ฑด๋ถ€ ์‹คํ–‰**: ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๋Œ€์ฒด ์ฒ˜๋ฆฌ +- **์ง๋ ฌํ™” ์‹œ์ **: JSON ๋ณ€ํ™˜ํ•  ๋•Œ๋งŒ ์‹คํ–‰ + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. ๊ฒฝ๋กœ ํŒŒ์‹ฑ +- dot notation ํŒŒ์‹ฑ ๋กœ์ง ๊ตฌํ˜„ +- ์ค‘์ฒฉ ๊ฐ์ฒด ์ ‘๊ทผ์„ ์œ„ํ•œ ๋ฆฌํ”Œ๋ ‰์…˜ ํ™œ์šฉ +- ๊ฒฝ๋กœ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + +### 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ +- SecurityContextUtil์„ ํ†ตํ•œ ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ +- ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ (ํ•„ํ„ฐ๋ง ์Šคํ‚ต) + +### 3. Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € +- @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์Šค์บ” +- ํ•„๋“œ๋ณ„ ์กฐ๊ฑด๋ถ€ ๊ฐ’ ๋ณ€๊ฒฝ +- ์›๋ณธ ๊ฐ์ฒด ์ˆ˜์ • ์—†์ด ์ง๋ ฌํ™” ์‹œ์—๋งŒ ๋ณ€๊ฒฝ + +## ๐Ÿ“‹ ์žฅ์  + +- **์ง๊ด€์ **: ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ๋ด๋„ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ๋ช…ํ™• +- **์œ ์—ฐํ•จ**: ๋‹ค์–‘ํ•œ ๊ฐ์ฒด ๊ตฌ์กฐ์— ๋Œ€์‘ ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ง€์ • ๋ฐฉ์‹ +- **์ตœ์†Œ ๋ณ€๊ฒฝ**: ๊ธฐ์กด DTO ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์ด ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ +- **์„ฑ๋Šฅ**: ํ•„์š”ํ•  ๋•Œ๋งŒ ์‹คํ–‰๋˜๋Š” ํšจ์œจ์ ์ธ ๊ตฌ์กฐ \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.4.md b/src/main/java/app/bottlenote/common/block/v1.4.md new file mode 100644 index 000000000..9972a656b --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/v1.4.md @@ -0,0 +1,154 @@ +# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.4 - ์ตœ์ข… ๋‹จ์ˆœํ™”๋œ @BlockWord ๋ฐฉ์‹ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**@BlockWord ์–ด๋…ธํ…Œ์ด์…˜**: JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ์ž๋™ ๋ณ€๊ฒฝ + +## ๐ŸŽฏ @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ + +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockWord { + /** + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" + */ + String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; + + /** + * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ + * ๊ธฐ๋ณธ๊ฐ’: "userId" + * ์ค‘์ฒฉ ๊ฐ์ฒด: "userInfo.userId" ํ˜•ํƒœ ์ง€์› + */ + String userIdPath() default "userId"; +} +``` + +## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ + +### ReviewInfo +```java +@Builder +public record ReviewInfo( + Long reviewId, + + @BlockWord(userIdPath = "userInfo.userId") + String reviewContent, + + UserInfo userInfo, + // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค +) {} +``` + +### UserInfo +```java +public record UserInfo( + Long userId, + + @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") + String nickName, + + String userProfileImage +) {} +``` + +## ๐Ÿ”ง ๋™์ž‘ ํ๋ฆ„ + +### 1. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ +```java +// SecurityContextUtils ํ™œ์šฉ +Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(null); + +// ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ +if (currentUserId == null) { + // ํ•„ํ„ฐ๋ง ์Šคํ‚ต, ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + return originalValue; +} +``` + +### 2. ์ž‘์„ฑ์ž userId ์ถ”์ถœ +```java +// userIdPath๋กœ ๋ฆฌํ”Œ๋ ‰์…˜์„ ํ†ตํ•ด userId ์ถ”์ถœ +// "userId" โ†’ ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ํ•„๋“œ +// "userInfo.userId" โ†’ userInfo ๊ฐ์ฒด ๋‚ด๋ถ€์˜ userId ํ•„๋“œ +Long authorId = extractUserIdByPath(object, userIdPath); +``` + +### 3. ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ +```java +// BlockService๋ฅผ ํ†ตํ•œ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์บ์‹œ ์ œ์™ธ) +boolean isBlocked = blockService.isBlocked(currentUserId, authorId); + +if (isBlocked) { + return ๋Œ€์ฒด๋ฉ”์‹œ์ง€; // "@BlockWord" ์–ด๋…ธํ…Œ์ด์…˜์˜ value ๊ฐ’ +} else { + return ์›๋ณธ๊ฐ’; +} +``` + +## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + +### ์ •์ƒ ์‚ฌ์šฉ์ž +```json +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "userId": 123, + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" + } +} +``` + +### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž +```json +{ + "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "userInfo": { + "userId": 456, + "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" + } +} +``` + +### ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž +```json +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "userId": 123, + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" + } +} +``` + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ํ•ต์‹ฌ ์š”์†Œ + +### 1. Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € +- `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐ์ง€ +- ํ•„๋“œ๋ณ„ ์กฐ๊ฑด๋ถ€ ๊ฐ’ ๋ณ€๊ฒฝ +- ์ง๋ ฌํ™” ์‹œ์ ์—์„œ๋งŒ ์ฒ˜๋ฆฌ + +### 2. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ์ถ”์ถœ +- dot notation ํŒŒ์‹ฑ: `"userInfo.userId"` +- ๋ฆฌํ”Œ๋ ‰์…˜์„ ํ†ตํ•œ ์ค‘์ฒฉ ๊ฐ์ฒด ์ ‘๊ทผ +- ๊ฒฝ๋กœ ์˜ค๋ฅ˜ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + +### 3. ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ์ฒ˜๋ฆฌ +- `SecurityContextUtil.getUserIdByContext()` ํ™œ์šฉ +- `Optional` ๋ฐ˜ํ™˜๊ฐ’ ์ฒ˜๋ฆฌ +- ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ํ•„ํ„ฐ๋ง ์Šคํ‚ต + +### 4. ๋‹จ์ˆœํ•œ ๋ธ”๋ก ๊ฒ€์‚ฌ +- `BlockService.isBlocked(currentUserId, authorId)` ํ˜ธ์ถœ +- ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์ œ์™ธ +- ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ์—๋งŒ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์ ์šฉ + +## ๐Ÿ“‹ ์ฒ˜๋ฆฌ ์กฐ๊ฑด + +| ์ƒํ™ฉ | ๋™์ž‘ | +|------|------| +| ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ (ํ•„ํ„ฐ๋ง ์•ˆ ํ•จ) | +| ๋กœ๊ทธ์ธ + ์ฐจ๋‹จ ์•ˆ ๋จ | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ | +| ๋กœ๊ทธ์ธ + ์ฐจ๋‹จ๋จ | ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ | +| userIdPath ์˜ค๋ฅ˜ | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + ๋กœ๊ทธ ๊ธฐ๋ก | \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.5.md b/src/main/java/app/bottlenote/common/block/v1.5.md new file mode 100644 index 000000000..9c9b92861 --- /dev/null +++ b/src/main/java/app/bottlenote/common/block/v1.5.md @@ -0,0 +1,192 @@ +# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.5 - Jackson ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๊ตฌํ˜„ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**์‹ค์ œ ๊ตฌํ˜„๋œ @BlockWord ๋ฐฉ์‹**: Jackson ContextualSerializer๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ตฌํ˜„ + +## ๐ŸŽฏ ๊ตฌํ˜„ ๊ตฌ์„ฑ์š”์†Œ + +### 1. @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BlockWord { + String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; + String userIdPath() default "userId"; +} +``` + +### 2. BlockWordSerializer +```java +@Component +public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer { + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) { + // 1. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ (SecurityContextUtil) + // 2. ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ํ•„ํ„ฐ๋ง ์Šคํ‚ต + // 3. userIdPath๋กœ ์ž‘์„ฑ์ž ID ์ถ”์ถœ (๋ฆฌํ”Œ๋ ‰์…˜) + // 4. BlockService.isBlocked() ํ˜ธ์ถœ + // 5. ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { + // @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐ์ง€ํ•˜์—ฌ ์ปจํ…์ŠคํŠธ๋ณ„ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ƒ์„ฑ + } +} +``` + +### 3. BlockWordConfig +```java +@Configuration +public class BlockWordConfig { + + @PostConstruct + public void configureObjectMapper() { + SimpleModule blockWordModule = new SimpleModule("BlockWordModule"); + blockWordModule.addSerializer(String.class, blockWordSerializer); + objectMapper.registerModule(blockWordModule); + } +} +``` + +## ๐Ÿ”ง ๋™์ž‘ ๊ณผ์ • + +### 1. ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๋“ฑ๋ก +- ObjectMapper์— BlockWordSerializer๋ฅผ String ํƒ€์ž…์— ๋Œ€ํ•ด ๋“ฑ๋ก +- @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š” ํ•„๋“œ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋„๋ก ContextualSerializer ํ™œ์šฉ + +### 2. ์ง๋ ฌํ™” ์‹œ์  ์ฒ˜๋ฆฌ +```java +// JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ž๋™ ํ˜ธ์ถœ +serialize(String value, JsonGenerator gen, SerializerProvider serializers) { + + // ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ + Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(null); + if (currentUserId == null) { + gen.writeString(value); // ๋น„๋กœ๊ทธ์ธ โ†’ ์›๋ณธ ๋ฐ˜ํ™˜ + return; + } + + // ์ž‘์„ฑ์ž ID ์ถ”์ถœ (๋ฆฌํ”Œ๋ ‰์…˜) + Object currentObject = gen.getCurrentValue(); + Long authorId = extractUserIdByPath(currentObject, userIdPath); + + // ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ + boolean isBlocked = blockService.isBlocked(currentUserId, authorId); + if (isBlocked) { + gen.writeString(๋Œ€์ฒด๋ฉ”์‹œ์ง€); // ์ฐจ๋‹จ๋จ โ†’ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ + } else { + gen.writeString(value); // ์ •์ƒ โ†’ ์›๋ณธ ๊ฐ’ + } +} +``` + +### 3. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ์ถ”์ถœ +```java +private Long extractUserIdByPath(Object object, String userIdPath) { + String[] pathParts = userIdPath.split("\\."); + Object currentObject = object; + + // dot notation ๊ฒฝ๋กœ ํƒ์ƒ‰ + for (String part : pathParts) { + Field field = findField(currentObject.getClass(), part); + field.setAccessible(true); + currentObject = field.get(currentObject); + } + + return (Long) currentObject; +} +``` + +## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ + +### ReviewInfo ์ ์šฉ +```java +@Builder +public record ReviewInfo( + Long reviewId, + + @BlockWord(userIdPath = "userInfo.userId") + String reviewContent, + + UserInfo userInfo +) {} +``` + +### UserInfo ์ ์šฉ +```java +public record UserInfo( + Long userId, + + @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") + String nickName, + + String userProfileImage +) {} +``` + +## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + +### ์ •์ƒ ์‚ฌ์šฉ์ž +```json +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "userId": 123, + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" + } +} +``` + +### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž +```json +{ + "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "userInfo": { + "userId": 456, + "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" + } +} +``` + +## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ํŠน์ง• + +### 1. ContextualSerializer ํ™œ์šฉ +- BeanProperty์—์„œ @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”์ถœ +- ์–ด๋…ธํ…Œ์ด์…˜๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ +- ํ•„๋“œ๋ณ„ ๊ฐœ๋ณ„ ์„ค์ • ์ ์šฉ + +### 2. ๋ฆฌํ”Œ๋ ‰์…˜ ๊ธฐ๋ฐ˜ ๊ฒฝ๋กœ ํƒ์ƒ‰ +- dot notation ์ง€์›: "userInfo.userId" +- ํด๋ž˜์Šค ์ƒ์† ๊ณ„์ธต ๊ณ ๋ คํ•œ ํ•„๋“œ ๊ฒ€์ƒ‰ +- ์‹คํŒจ ์‹œ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜์œผ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด + +### 3. ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ +- SecurityContextUtil.getUserIdByContext() ํ™œ์šฉ +- Optional ๋ฐ˜ํ™˜๊ฐ’ ์ฒ˜๋ฆฌ๋กœ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ๊ณ ๋ ค +- ์˜ˆ์™ธ ์ƒํ™ฉ์—์„œ ์›๋ณธ ๊ฐ’ ๋ณด์กด + +### 4. ์—๋Ÿฌ ํ•ธ๋“ค๋ง +- ๊ฐ ๋‹จ๊ณ„๋ณ„ try-catch๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด +- ์‹คํŒจ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก ํ›„ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ +- ์šด์˜ ์ค‘ ์žฅ์•  ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ + +## ๐Ÿ“‹ ๊ตฌํ˜„ ํŒŒ์ผ ๊ตฌ์กฐ + +``` +common/block/ +โ”œโ”€โ”€ annotation/ +โ”‚ โ””โ”€โ”€ BlockWord.java # ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ +โ”œโ”€โ”€ serializer/ +โ”‚ โ””โ”€โ”€ BlockWordSerializer.java # Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € +โ”œโ”€โ”€ config/ +โ”‚ โ””โ”€โ”€ BlockWordConfig.java # ObjectMapper ์„ค์ • +โ””โ”€โ”€ service/ + โ””โ”€โ”€ BlockService.java # ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ (๊ธฐ์กด) +``` + +## ๐Ÿ“ ๊ฒฐ๋ก  + +Jackson์˜ ContextualSerializer๋ฅผ ํ™œ์šฉํ•œ ๋ฐฉ์‹์œผ๋กœ ํ•„๋“œ ๋ ˆ๋ฒจ์—์„œ ์„ธ๋ฐ€ํ•œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. AOP ๋ฐฉ์‹์˜ ๋ณต์žก์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/global/annotation/BlockFilter.java b/src/main/java/app/bottlenote/global/annotation/BlockFilter.java deleted file mode 100644 index c19ad46d5..000000000 --- a/src/main/java/app/bottlenote/global/annotation/BlockFilter.java +++ /dev/null @@ -1,26 +0,0 @@ -package app.bottlenote.global.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ - *

- * ์‚ฌ์šฉ ์˜ˆ์‹œ: - * @BlockFilter(userField = "authorId") - * @GetMapping("/reviews") public ResponseEntity getReviews() { ... } - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockFilter { - - /** - * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… - * ์˜ˆ: "authorId", "userId", "writerId" - * - * @return ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… - */ - String userField() default "authorId"; -} diff --git a/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java b/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java deleted file mode 100644 index ace2aaa1f..000000000 --- a/src/main/java/app/bottlenote/global/aspect/BlockFilterAspect.java +++ /dev/null @@ -1,228 +0,0 @@ -package app.bottlenote.global.aspect; - -import app.bottlenote.global.annotation.BlockFilter; -import app.bottlenote.global.data.response.GlobalResponse; -import app.bottlenote.global.security.SecurityContextUtil; -import app.bottlenote.global.service.BlockService; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * ์ฐจ๋‹จ ํ•„ํ„ฐ AOP ํด๋ž˜์Šค - * - * @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ์˜ ์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„์„œ - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. - */ -@Slf4j -@Aspect -@Component -@RequiredArgsConstructor -public class BlockFilterAspect { - - private final BlockService blockService; - private final ObjectMapper objectMapper; - - /** - * @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๋ฅผ Around ์–ด๋“œ๋ฐ”์ด์Šค๋กœ ์ฒ˜๋ฆฌ - */ - @Around("@annotation(blockFilter)") - public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) throws Throwable { - - // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ - Object result = joinPoint.proceed(); - - // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํ™•์ธ - Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); - - if (currentUserId == -1L) { - // ์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉํ•˜์ง€ ์•Š์Œ - return result; - } - - // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ - Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); - - if (blockedUserIds.isEmpty()) { - return result; - } - - // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ (ํ•ญ์ƒ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ) - return applyBlockFilter(result, blockedUserIds, blockFilter.userField()); - } - - /** - * ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง์„ ์ ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ (๋‹จ์ˆœ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ) - */ - private Object applyBlockFilter(Object result, Set blockedUserIds, String userField) { - - if (!(result instanceof ResponseEntity)) { - return result; - } - - ResponseEntity responseEntity = (ResponseEntity) result; - Object body = responseEntity.getBody(); - - if (!(body instanceof GlobalResponse)) { - return result; - } - - GlobalResponse globalResponse = (GlobalResponse) body; - Object data = globalResponse.getData(); - - if (data == null) { - return result; - } - - // ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - Object filteredData; - if (data instanceof List) { - filteredData = blurListData((List) data, blockedUserIds, userField); - } else { - filteredData = blurSingleData(data, blockedUserIds, userField); - } - - // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ์šด ์‘๋‹ต ์ƒ์„ฑ - GlobalResponse filteredResponse = GlobalResponse.success(filteredData); - return ResponseEntity.ok(filteredResponse); - } - - /** - * List ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - @SuppressWarnings("unchecked") - private List blurListData(List dataList, Set blockedUserIds, String userField) { - return dataList.stream() - .map(item -> blurItemIfBlocked(item, blockedUserIds, userField)) - .collect(Collectors.toList()); - } - - /** - * ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - private Object blurSingleData(Object data, Set blockedUserIds, String userField) { - return blurItemIfBlocked(data, blockedUserIds, userField); - } - - /** - * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - private Object blurItemIfBlocked(Object item, Set blockedUserIds, String userField) { - if (item == null) { - return item; - } - - // ์‚ฌ์šฉ์ž ID ์ถ”์ถœ - Long userId = extractUserIdFromObject(item, userField); - - if (userId == null || !blockedUserIds.contains(userId)) { - return item; // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์›๋ณธ ๋ฐ˜ํ™˜ - } - - // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฉด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - return blurContent(item); - } - - /** - * ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•˜๋Š” ๋ฉ”์„œ๋“œ - */ - private Long extractUserIdFromObject(Object obj, String fieldName) { - try { - Field field = findFieldInClass(obj.getClass(), fieldName); - if (field != null) { - field.setAccessible(true); - Object value = field.get(obj); - if (value instanceof Long) { - return (Long) value; - } - } - } catch (Exception e) { - log.warn("์‚ฌ์šฉ์ž ID ์ถ”์ถœ ์‹คํŒจ - ๊ฐ์ฒด: {}, ํ•„๋“œ: {}, ์—๋Ÿฌ: {}", - obj.getClass().getSimpleName(), fieldName, e.getMessage()); - } - return null; - } - - /** - * ํด๋ž˜์Šค ๊ณ„์ธต๊ตฌ์กฐ์—์„œ ํ•„๋“œ๋ฅผ ์ฐพ๋Š” ๋ฉ”์„œ๋“œ - */ - private Field findFieldInClass(Class clazz, String fieldName) { - Class currentClass = clazz; - while (currentClass != null) { - try { - return currentClass.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - currentClass = currentClass.getSuperclass(); - } - } - return null; - } - - /** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ - */ - private Object blurContent(Object originalItem) { - try { - // Jackson์„ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ๊นŠ์€ ๋ณต์‚ฌ - Object clonedItem = deepCloneObject(originalItem); - - // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ํ‘œ์‹œ๋กœ ๋งˆ์Šคํ‚น - maskField(clonedItem, "content", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); - maskField(clonedItem, "title", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); - maskField(clonedItem, "authorNickname", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"); - maskField(clonedItem, "nickname", "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"); - maskField(clonedItem, "authorProfileImage", null); - maskField(clonedItem, "profileImage", null); - - return clonedItem; - - } catch (Exception e) { - log.error("์ฐจ๋‹จ ์ฒ˜๋ฆฌ ์‹คํŒจ: {}", e.getMessage()); - // ์‹คํŒจ ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ - return originalItem; - } - } - - /** - * ๊ฐ์ฒด์˜ ํŠน์ • ํ•„๋“œ๋ฅผ ๋งˆ์Šคํ‚นํ•˜๋Š” ๋ฉ”์„œ๋“œ - */ - private void maskField(Object obj, String fieldName, Object maskValue) { - try { - Field field = findFieldInClass(obj.getClass(), fieldName); - if (field != null) { - field.setAccessible(true); - field.set(obj, maskValue); - } - } catch (Exception e) { - // ํ•„๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์„ค์ • ์‹คํŒจ ์‹œ ๋ฌด์‹œ (์ •์ƒ์ ์ธ ์ƒํ™ฉ) - } - } - - /** - * Jackson ObjectMapper๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ ๊นŠ์€ ๋ณต์‚ฌ - */ - private Object deepCloneObject(Object original) { - try { - if (original == null) { - return null; - } - - // Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ - String jsonString = objectMapper.writeValueAsString(original); - return objectMapper.readValue(jsonString, original.getClass()); - } catch (Exception e) { - log.warn("๊ฐ์ฒด ๋ณต์‚ฌ ์‹คํŒจ, ์›๋ณธ ๋ฐ˜ํ™˜: {}", e.getMessage()); - return original; - } - } -} diff --git a/src/main/java/app/bottlenote/global/controller/BlockTestController.java b/src/main/java/app/bottlenote/global/controller/BlockTestController.java deleted file mode 100644 index b859fe458..000000000 --- a/src/main/java/app/bottlenote/global/controller/BlockTestController.java +++ /dev/null @@ -1,248 +0,0 @@ -package app.bottlenote.global.controller; - -import app.bottlenote.global.data.response.GlobalResponse; -import app.bottlenote.global.service.BlockService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Profile; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -/** - * ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ์šฉ ์ปจํŠธ๋กค๋Ÿฌ - * - * ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. - * ์šด์˜ ํ™˜๊ฒฝ(prod)์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/test/block") -@RequiredArgsConstructor -@Profile({"local", "dev", "test", "!prod"}) -public class BlockTestController { - - private final BlockService blockService; - - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (ํ…Œ์ŠคํŠธ์šฉ) - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋  ์‚ฌ์šฉ์ž ID - * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) - */ - @PostMapping("/add") - public ResponseEntity createBlock( - @RequestParam Long blockerId, - @RequestParam Long blockedId, - @RequestParam(required = false, defaultValue = "ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ") String reason) { - - try { - var userBlock = blockService.createBlock(blockerId, blockedId, reason); - return GlobalResponse.ok("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์™„๋ฃŒ: " + blockerId + " -> " + blockedId + - " (ID: " + userBlock.getId() + ")"); - } catch (Exception e) { - log.error("ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ ์ƒ์„ฑ ์‹คํŒจ", e); - return ResponseEntity.badRequest() - .body(GlobalResponse.fail("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ: " + e.getMessage())); - } - } - - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (ํ…Œ์ŠคํŠธ์šฉ) - */ - @DeleteMapping("/remove") - public ResponseEntity deleteBlock( - @RequestParam Long blockerId, - @RequestParam Long blockedId) { - - blockService.removeBlock(blockerId, blockedId); - - return GlobalResponse.ok("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์™„๋ฃŒ: " + blockerId + " -> " + blockedId); - } - - /** - * ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (ํ…Œ์ŠคํŠธ์šฉ) - */ - @GetMapping("/list/{userId}") - public ResponseEntity getBlockList(@PathVariable Long userId) { - - var blockedUsers = blockService.getBlockedUserIds(userId); - - return GlobalResponse.ok(blockedUsers); - } - - /** - * ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (ํ…Œ์ŠคํŠธ์šฉ) - */ - @GetMapping("/check") - public ResponseEntity checkBlock( - @RequestParam Long blockerId, - @RequestParam Long blockedId) { - - boolean isBlocked = blockService.isBlocked(blockerId, blockedId); - - return GlobalResponse.ok("์ฐจ๋‹จ ์—ฌ๋ถ€: " + isBlocked); - } - - /** - * ๋ชจ๋“  ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” (ํ…Œ์ŠคํŠธ์šฉ) - ํ˜„์žฌ DB ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด ์ด ๊ธฐ๋Šฅ์€ ๋น„ํ™œ์„ฑํ™” - */ - @DeleteMapping("/clear") - public ResponseEntity deleteAllBlocks() { - - // DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์ „์ฒด ์‚ญ์ œ๋Š” ์œ„ํ—˜ํ•˜๋ฏ€๋กœ ๋น„ํ™œ์„ฑํ™” - log.warn("์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™”๋Š” DB ๊ธฐ๋ฐ˜์—์„œ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); - - return GlobalResponse.ok("DB ๊ธฐ๋ฐ˜์—์„œ๋Š” ์ „์ฒด ์ดˆ๊ธฐํ™”๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”."); - } - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (ํ…Œ์ŠคํŠธ์šฉ) - */ - @GetMapping("/statistics/{userId}") - public ResponseEntity getBlockStatistics(@PathVariable Long userId) { - - try { - var statistics = blockService.getBlockStatistics(userId); - return GlobalResponse.ok(statistics); - } catch (Exception e) { - log.error("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ", e); - return ResponseEntity.badRequest() - .body(GlobalResponse.fail("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ: " + e.getMessage())); - } - } - - /** - * ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ (ํ…Œ์ŠคํŠธ์šฉ) - ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ์‘๋‹ต - * - * ์ด API๋Š” ์‹ค์ œ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์‘๋‹ต๊ณผ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋กœ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. - * ์ง์ ‘ ์ฐจ๋‹จ ํ•„ํ„ฐ๋ง์„ ์ ์šฉํ•˜์—ฌ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๊ฐ€ ๋งˆ์Šคํ‚น๋ฉ๋‹ˆ๋‹ค. - * - * ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค: - * 1. ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ•œ ์ƒํƒœ์—์„œ X-User-Id=1๋กœ ์š”์ฒญ - * 2. ์‚ฌ์šฉ์ž 2,3์˜ ๋ฆฌ๋ทฐ ๋‚ด์šฉ์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น๋จ - * 3. ๋‹‰๋„ค์ž„์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"๋กœ ๋ณ€๊ฒฝ๋จ - */ - @GetMapping("/test-data/{userId}") - public ResponseEntity getTestData(@PathVariable Long userId, - @RequestHeader(value = "X-User-Id", required = false) Long requestUserId) { - - log.info("์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์š”์ฒญ - pathUserId: {}, requestUserId: {}", userId, requestUserId); - - // ์š”์ฒญํ•œ ์‚ฌ์šฉ์ž ID ๊ฒฐ์ • (X-User-Id ํ—ค๋” ์šฐ์„ ) - Long currentUserId = requestUserId != null ? requestUserId : userId; - - // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ - java.util.Set blockedUserIds = java.util.Collections.emptySet(); - if (currentUserId != null && currentUserId != -1L) { - blockedUserIds = blockService.getBlockedUserIds(currentUserId); - log.info("์‚ฌ์šฉ์ž {}์˜ ์ฐจ๋‹จ ๋ชฉ๋ก: {}", currentUserId, blockedUserIds); - } - - // ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - var testReviews = java.util.List.of( - createTestReview(1L, "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค. ์Šค๋ชจํ‚คํ•œ ํ–ฅ์ด ์ผํ’ˆ์ด๋„ค์š”!", "์œ„์Šคํ‚ค_๋Ÿฌ๋ฒ„", "profile1.jpg", 4.5), - createTestReview(2L, "์ฒซ ์‹œ์Œ์ธ๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ๋ถ€๋“œ๋Ÿฝ๊ณ  ๋‹ฌ์ฝคํ•ฉ๋‹ˆ๋‹ค. ์ž…๋ฌธ์ž์—๊ฒŒ ์ถ”์ฒœ!", "์ดˆ๋ณด_์œ„์Šคํ‚ค", "profile2.jpg", 4.0), - createTestReview(3L, "๊ฐ€๊ฒฉ ๋Œ€๋น„ ํ›Œ๋ฅญํ•œ ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค. ์žฌ๊ตฌ๋งค ์˜ํ–ฅ 100%", "์œ„์Šคํ‚ค_๋งˆ๋‹ˆ์•„", "profile3.jpg", 4.8), - createTestReview(4L, "์˜คํฌํ–ฅ์ด ๊ฐ•ํ•ด์„œ ํ˜ธ๋ถˆํ˜ธ๊ฐ€ ๊ฐˆ๋ฆด ๊ฒƒ ๊ฐ™์•„์š”.", "๋ฆฌ๋ทฐ_๊ณ ์ˆ˜", "profile4.jpg", 3.5), - createTestReview(5L, "์นœ๊ตฌ๋“ค๊ณผ ํ•จ๊ป˜ ๋งˆ์‹œ๊ธฐ ์ข‹์€ ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค.", "์†Œ์…œ_๋“œ๋ง์ปค", "profile5.jpg", 4.2) - ); - - // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ์— ํ•„ํ„ฐ๋ง ์ ์šฉ - var filteredReviews = testReviews.stream() - .map(review -> { - Long authorId = (Long) review.get("authorId"); - if (blockedUserIds.contains(authorId)) { - return applyBlockMask(review); - } - return review; - }) - .collect(java.util.stream.Collectors.toList()); - - // ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์‘๋‹ต ๊ตฌ์กฐ๋กœ ๋ž˜ํ•‘ - var response = java.util.Map.of( - "totalCount", filteredReviews.size(), - "reviewList", filteredReviews, - "message", "์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ (์ง์ ‘ ํ•„ํ„ฐ๋ง ์ ์šฉ)", - "blockedUserIds", blockedUserIds, - "currentUserId", currentUserId - ); - - return GlobalResponse.ok(response); - } - - /** - * ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ—ฌํผ ๋ฉ”์„œ๋“œ - */ - private java.util.Map createTestReview( - Long authorId, String content, String nickname, String profileImage, Double rating) { - - java.util.Map review = new java.util.HashMap<>(); - - // ๋ฆฌ๋ทฐ ๊ธฐ๋ณธ ์ •๋ณด - review.put("reviewId", 1000L + authorId); - review.put("reviewContent", content); - review.put("reviewImageUrl", "review_image_" + authorId + ".jpg"); - review.put("createAt", java.time.LocalDateTime.now().minusDays(authorId)); - review.put("totalImageCount", 2L); - - // ์ž‘์„ฑ์ž ์ •๋ณด (์ฐจ๋‹จ ํ•„ํ„ฐ๋ง ๋Œ€์ƒ) - review.put("authorId", authorId); - review.put("authorNickname", nickname); - review.put("authorProfileImage", profileImage); - - // ์‚ฌ์šฉ์ž ์ •๋ณด (UserInfo ๊ตฌ์กฐ ๋ชจ๋ฐฉ) - review.put("userInfo", java.util.Map.of( - "userId", authorId, - "nickName", nickname, - "userProfileImage", profileImage - )); - - // ๋ฆฌ๋ทฐ ์ƒํƒœ ๋ฐ ํ‰์  - review.put("isMyReview", false); - review.put("status", "PUBLIC"); - review.put("isBestReview", rating >= 4.5); - review.put("rating", rating); - - // ์ƒํ˜ธ์ž‘์šฉ ์ •๋ณด - review.put("likeCount", (long) (Math.random() * 20)); - review.put("replyCount", (long) (Math.random() * 10)); - review.put("isLikedByMe", false); - review.put("hasReplyByMe", false); - review.put("viewCount", (long) (Math.random() * 100)); - - // ๊ธฐํƒ€ ์ •๋ณด - review.put("price", java.math.BigDecimal.valueOf(50000 + (authorId * 10000))); - review.put("sizeType", "BOTTLE_750"); - review.put("locationInfo", "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ"); - review.put("tastingTagList", "์Šค๋ชจํ‚ค, ์˜คํฌ, ๋ฐ”๋‹๋ผ"); - - return review; - } - - /** - * ํ…Œ์ŠคํŠธ์šฉ SecurityContext ๋ชจํ‚น ๋ฉ”์„œ๋“œ - * ์‹ค์ œ ์šด์˜์—์„œ๋Š” JWT ํ† ํฐ์œผ๋กœ SecurityContext๊ฐ€ ์„ค์ •๋˜์ง€๋งŒ, - * ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์ง์ ‘ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. - */ - private void mockSecurityContext(Long userId) { - try { - // SecurityContext์— ์ง์ ‘ ์‚ฌ์šฉ์ž ID ์„ค์ • - // ์ฃผ์˜: ์ด๋Š” ํ…Œ์ŠคํŠธ์šฉ์ด๋ฏ€๋กœ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ! - org.springframework.security.core.context.SecurityContextHolder.getContext() - .setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken( - userId.toString(), null, "ROLE_USER")); - - log.debug("ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ • ์™„๋ฃŒ: {}", userId); - } catch (Exception e) { - log.warn("ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ • ์‹คํŒจ: {}", e.getMessage()); - } - } -} diff --git a/src/main/java/app/bottlenote/global/service/BlockService.java b/src/main/java/app/bottlenote/global/service/BlockService.java deleted file mode 100644 index 40982dd6e..000000000 --- a/src/main/java/app/bottlenote/global/service/BlockService.java +++ /dev/null @@ -1,262 +0,0 @@ -package app.bottlenote.global.service; - -import app.bottlenote.user.domain.UserBlock; -import app.bottlenote.user.domain.UserBlockRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค - * DB ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, Redis ์บ์‹œ๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. - */ -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class BlockService { - - private final UserBlockRepository userBlockRepository; - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ - * Redis ์บ์‹œ๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ ์ตœ์ ํ™” - * - * @param userId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - */ - @Cacheable( - value = "blocked_users", - key = "#userId", - unless = "#userId == null or #userId == -1", - condition = "#userId != null and #userId != -1" - ) - @Transactional(readOnly = true) - public Set getBlockedUserIds(Long userId) { - if (userId == null || userId == -1L) { - return Collections.emptySet(); - } - - try { - Set blockedUsers = userBlockRepository.findBlockedUserIdsByBlockerId(userId); - log.debug("์‚ฌ์šฉ์ž {}์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ: {} ๋ช…", userId, blockedUsers.size()); - return blockedUsers; - } catch (Exception e) { - log.error("์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ - userId: {}, error: {}", userId, e.getMessage()); - return Collections.emptySet(); - } - } - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ–ˆ๋Š”์ง€ ํ™•์ธ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ - @Transactional(readOnly = true) - public boolean isBlocked(Long blockerId, Long blockedId) { - if (blockerId == null || blockedId == null || blockerId == -1L || blockerId.equals(blockedId)) { - return false; - } - - try { - return userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId); - } catch (Exception e) { - log.error("์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", - blockerId, blockedId, e.getMessage()); - return false; - } - } - - /** - * ์ƒํ˜ธ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์„œ๋กœ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) - * - * @param userId1 ์‚ฌ์šฉ์ž 1 - * @param userId2 ์‚ฌ์šฉ์ž 2 - * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ - @Transactional(readOnly = true) - public boolean isMutuallyBlocked(Long userId1, Long userId2) { - if (userId1 == null || userId2 == null || userId1.equals(userId2)) { - return false; - } - - try { - return userBlockRepository.existsMutualBlock(userId1, userId2); - } catch (Exception e) { - log.error("์ƒํ˜ธ ์ฐจ๋‹จ ํ™•์ธ ์‹คํŒจ - userId1: {}, userId2: {}, error: {}", - userId1, userId2, e.getMessage()); - return false; - } - } - - /** - * ๋ฐฐ์น˜๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์„ฑ๋Šฅ ์ตœ์ ํ™”) - * - * @param userIds ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - * @return ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ๋ชฉ๋ก ๋งต - */ - @Transactional(readOnly = true) - public Map> getBulkBlockedUsers(Set userIds) { - if (userIds == null || userIds.isEmpty()) { - return Collections.emptyMap(); - } - - return userIds.stream() - .filter(id -> id != null && id != -1L) - .collect(Collectors.toMap( - Function.identity(), - this::getBlockedUserIds, - (existing, replacement) -> existing // ์ค‘๋ณต ํ‚ค ์ฒ˜๋ฆฌ - )); - } - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‹ค์ œ ์šด์˜์šฉ ๋ฉ”์„œ๋“œ) - * - * @param currentUserId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param targetUserId ์ฐจ๋‹จํ•  ์‚ฌ์šฉ์ž ID - * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  - * @return ์ƒ์„ฑ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#currentUserId") - public UserBlock blockUser(Long currentUserId, Long targetUserId, String reason) { - validateBlockRequest(currentUserId, targetUserId); - return createBlock(currentUserId, targetUserId, reason); - } - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ (์‹ค์ œ ์šด์˜์šฉ ๋ฉ”์„œ๋“œ) - * - * @param currentUserId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param targetUserId ์ฐจ๋‹จ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#currentUserId") - public void unblockUser(Long currentUserId, Long targetUserId) { - validateBlockRequest(currentUserId, targetUserId); - removeBlock(currentUserId, targetUserId); - } - - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (๋‚ด๋ถ€์šฉ - ํ…Œ์ŠคํŠธ์—์„œ๋„ ์‚ฌ์šฉ) - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋  ์‚ฌ์šฉ์ž ID - * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  - * @return ์ƒ์„ฑ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#blockerId") - public UserBlock createBlock(Long blockerId, Long blockedId, String reason) { - validateBlockRequest(blockerId, blockedId); - - // ์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ธ์ง€ ํ™•์ธ - if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { - log.warn("์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค - blockerId: {}, blockedId: {}", blockerId, blockedId); - return userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) - .orElseThrow(() -> new IllegalStateException("์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ")); - } - - try { - UserBlock userBlock = UserBlock.create(blockerId, blockedId, reason); - UserBlock savedBlock = userBlockRepository.save(userBlock); - - log.info("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์™„๋ฃŒ - blockerId: {}, blockedId: {}, reason: {}", - blockerId, blockedId, reason); - - return savedBlock; - } catch (Exception e) { - log.error("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", - blockerId, blockedId, e.getMessage()); - throw new RuntimeException("์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#blockerId") - public void removeBlock(Long blockerId, Long blockedId) { - validateBlockRequest(blockerId, blockedId); - - try { - UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) - .orElse(null); - - if (userBlock != null) { - userBlockRepository.delete(userBlock); - log.info("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์™„๋ฃŒ - blockerId: {}, blockedId: {}", blockerId, blockedId); - } else { - log.warn("ํ•ด์ œํ•  ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค - blockerId: {}, blockedId: {}", blockerId, blockedId); - } - } catch (Exception e) { - log.error("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ ์‹คํŒจ - blockerId: {}, blockedId: {}, error: {}", - blockerId, blockedId, e.getMessage()); - throw new RuntimeException("์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", e); - } - } - - /** - * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ - */ - private void validateBlockRequest(Long blockerId, Long blockedId) { - if (blockerId == null || blockedId == null) { - throw new IllegalArgumentException("์‚ฌ์šฉ์ž ID๋Š” null์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if (blockerId.equals(blockedId)) { - throw new IllegalArgumentException("์ž๊ธฐ ์ž์‹ ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - - if (blockerId == -1L) { - throw new IllegalArgumentException("์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - } - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ํ†ต๊ณ„ ์ •๋ณด - */ - @Transactional(readOnly = true) - public BlockStatistics getBlockStatistics(Long userId) { - if (userId == null || userId == -1L) { - return new BlockStatistics(0L, 0L); - } - - try { - long blockedCount = userBlockRepository.countByBlockerId(userId); - long blockedByCount = userBlockRepository.countByBlockedId(userId); - - return new BlockStatistics(blockedCount, blockedByCount); - } catch (Exception e) { - log.error("์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ์‹คํŒจ - userId: {}, error: {}", userId, e.getMessage()); - return new BlockStatistics(0L, 0L); - } - } - - /** - * ์ฐจ๋‹จ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” record - */ - public record BlockStatistics( - long blockedCount, // ๋‚ด๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - long blockedByCount // ๋‚˜๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - ) {} -} diff --git a/src/main/java/app/bottlenote/review/controller/ReviewController.java b/src/main/java/app/bottlenote/review/controller/ReviewController.java index e5b965996..4e154a604 100644 --- a/src/main/java/app/bottlenote/review/controller/ReviewController.java +++ b/src/main/java/app/bottlenote/review/controller/ReviewController.java @@ -1,6 +1,6 @@ package app.bottlenote.review.controller; -import app.bottlenote.global.annotation.BlockFilter; +import app.bottlenote.common.block.annotation.BlockFilter; import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.security.SecurityContextUtil; import app.bottlenote.global.service.cursor.PageResponse; From c0a73f9cd50036ab7afeb691667b9759009479e7 Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 12:56:47 +0900 Subject: [PATCH 03/16] feat: implement user blocking functionality with BlockWord annotation and related services Signed-off-by: hgkim --- ...352\263\204_\354\203\235\354\204\261.http" | 31 --- ...353\266\200_\355\231\225\354\235\270.http" | 15 -- ...353\241\235_\354\241\260\355\232\214.http" | 15 -- ...352\263\204_\354\241\260\355\232\214.http" | 15 -- ...352\263\204_\355\225\264\354\240\234.http" | 15 -- ...\355\205\214\354\212\244\355\212\270.http" | 47 ---- ...\355\205\214\354\212\244\355\212\270.http" | 6 - http/block/README.md | 107 -------- http/block/block-blur-test.http | 10 - ...\355\205\214\354\212\244\355\212\270.http" | 38 +++ .../block/annotation/BlockFilter.java.bak | 38 --- .../common/block/annotation/BlockWord.java | 11 +- .../block/aspect/BlockFilterAspect.java.bak | 238 ------------------ .../common/block/config/BlockWordConfig.java | 24 +- .../controller/BlockTestController.java.bak | 70 ------ .../block/serializer/BlockWordSerializer.java | 37 +-- .../common/block/service/BlockService.java | 48 ---- .../java/app/bottlenote/common/block/v1.0.md | 158 ------------ .../java/app/bottlenote/common/block/v1.1.md | 189 -------------- .../java/app/bottlenote/common/block/v1.3.md | 181 ------------- .../java/app/bottlenote/common/block/v1.4.md | 154 ------------ .../java/app/bottlenote/common/block/v1.5.md | 192 -------------- .../review/controller/ReviewController.java | 3 - .../review/facade/payload/ReviewInfo.java | 66 ++--- .../review/facade/payload/UserInfo.java | 9 +- .../block/controller/BlockController.java | 73 ++++++ .../block}/domain/UserBlock.java | 7 +- .../block/exception/BlockException.java | 10 + .../block/exception/BlockExceptionCode.java | 29 +++ .../repository/JpaUserBlockRepository.java | 5 +- .../repository}/UserBlockRepository.java | 4 +- .../support/block/service/BlockService.java | 174 +++++++++++++ src/main/resources/application-logging.yml | 3 + 33 files changed, 407 insertions(+), 1615 deletions(-) delete mode 100644 "http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" delete mode 100644 "http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" delete mode 100644 "http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" delete mode 100644 "http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" delete mode 100644 "http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" delete mode 100644 "http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" delete mode 100644 "http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" delete mode 100644 http/block/README.md delete mode 100644 http/block/block-blur-test.http create mode 100644 "http/block/\354\260\250\353\213\250_\355\225\204\355\204\260\353\247\201_\355\205\214\354\212\244\355\212\270.http" delete mode 100644 src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak delete mode 100644 src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak delete mode 100644 src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak delete mode 100644 src/main/java/app/bottlenote/common/block/service/BlockService.java delete mode 100644 src/main/java/app/bottlenote/common/block/v1.0.md delete mode 100644 src/main/java/app/bottlenote/common/block/v1.1.md delete mode 100644 src/main/java/app/bottlenote/common/block/v1.3.md delete mode 100644 src/main/java/app/bottlenote/common/block/v1.4.md delete mode 100644 src/main/java/app/bottlenote/common/block/v1.5.md create mode 100644 src/main/java/app/bottlenote/support/block/controller/BlockController.java rename src/main/java/app/bottlenote/{user => support/block}/domain/UserBlock.java (87%) create mode 100644 src/main/java/app/bottlenote/support/block/exception/BlockException.java create mode 100644 src/main/java/app/bottlenote/support/block/exception/BlockExceptionCode.java rename src/main/java/app/bottlenote/{user => support/block}/repository/JpaUserBlockRepository.java (89%) rename src/main/java/app/bottlenote/{user/domain => support/block/repository}/UserBlockRepository.java (94%) create mode 100644 src/main/java/app/bottlenote/support/block/service/BlockService.java diff --git "a/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" "b/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" deleted file mode 100644 index 29264d208..000000000 --- "a/http/block/01-\354\260\250\353\213\250_\352\264\200\352\263\204_\354\203\235\354\204\261.http" +++ /dev/null @@ -1,31 +0,0 @@ -### ํ† ํฐ ๋ฐœ๊ธ‰ (์„ ํƒ์‚ฌํ•ญ - ํ† ํฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //ํ† ํฐ์„ ์ „์—ญ๋ณ€์ˆ˜๋กœ ์ €์žฅ - client.global.set("accessToken", response.body.data.accessToken); -%} - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ -POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=2&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (๊ธฐ๋ณธ ์‚ฌ์œ ) -POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=3 -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ (์ปค์Šคํ…€ ์‚ฌ์œ ) -POST {{host}}/api/v1/test/block/add?blockerId=2&blockedId=1&reason=๋ถ€์ ์ ˆํ•œ ๋Œ“๊ธ€ -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ ์‹คํŒจ ์ผ€์ด์Šค (๋™์ผ ์‚ฌ์šฉ์ž) -POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=1&reason=์ž๊ธฐ ์ž์‹  ์ฐจ๋‹จ -Content-Type: application/json \ No newline at end of file diff --git "a/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" "b/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" deleted file mode 100644 index 1d67b2c9d..000000000 --- "a/http/block/02-\354\260\250\353\213\250_\354\227\254\353\266\200_\355\231\225\354\235\270.http" +++ /dev/null @@ -1,15 +0,0 @@ -### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ) -GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=2 -Content-Type: application/json - -### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์ฐจ๋‹จ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ) -GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=5 -Content-Type: application/json - -### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) -GET {{host}}/api/v1/test/block/check?blockerId=2&blockedId=1 -Content-Type: application/json - -### ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/check?blockerId=999&blockedId=888 -Content-Type: application/json \ No newline at end of file diff --git "a/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" "b/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" deleted file mode 100644 index fc73a52d8..000000000 --- "a/http/block/03-\354\260\250\353\213\250_\353\252\251\353\241\235_\354\241\260\355\232\214.http" +++ /dev/null @@ -1,15 +0,0 @@ -### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์ด ์žˆ๋Š” ๊ฒฝ์šฐ) -GET {{host}}/api/v1/test/block/list/1 -Content-Type: application/json - -### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) -GET {{host}}/api/v1/test/block/list/10 -Content-Type: application/json - -### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) -GET {{host}}/api/v1/test/block/list/2 -Content-Type: application/json - -### ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/list/999 -Content-Type: application/json \ No newline at end of file diff --git "a/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" "b/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" deleted file mode 100644 index a477b571c..000000000 --- "a/http/block/04-\354\260\250\353\213\250_\355\206\265\352\263\204_\354\241\260\355\232\214.http" +++ /dev/null @@ -1,15 +0,0 @@ -### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์ฐจ๋‹จ ๋‚ด์—ญ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/statistics/1 -Content-Type: application/json - -### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์ฐจ๋‹จ ๋‚ด์—ญ์ด ์—†๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/statistics/10 -Content-Type: application/json - -### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (๋งŽ์€ ์ฐจ๋‹จ ๋‚ด์—ญ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/statistics/2 -Content-Type: application/json - -### ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž) -GET {{host}}/api/v1/test/block/statistics/999 -Content-Type: application/json \ No newline at end of file diff --git "a/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" "b/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" deleted file mode 100644 index bed251f37..000000000 --- "a/http/block/05-\354\260\250\353\213\250_\352\264\200\352\263\204_\355\225\264\354\240\234.http" +++ /dev/null @@ -1,15 +0,0 @@ -### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๊ธฐ์กด ์ฐจ๋‹จ ๊ด€๊ณ„) -DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=2 -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๋˜ ๋‹ค๋ฅธ ์ฐจ๋‹จ ๊ด€๊ณ„) -DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=3 -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฐจ๋‹จ ๊ด€๊ณ„) -DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=10 -Content-Type: application/json - -### ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) -DELETE {{host}}/api/v1/test/block/remove?blockerId=2&blockedId=1 -Content-Type: application/json \ No newline at end of file diff --git "a/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" "b/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" deleted file mode 100644 index ac175c16e..000000000 --- "a/http/block/06-\354\260\250\353\213\250_\355\225\204\355\204\260_\355\205\214\354\212\244\355\212\270.http" +++ /dev/null @@ -1,47 +0,0 @@ -### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์ค€๋น„ - ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ -# ๋จผ์ € ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ•˜๋„๋ก ์„ค์ • -POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=2&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ -Content-Type: application/json - -### -POST {{host}}/api/v1/test/block/add?blockerId=1&blockedId=3&reason=ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ -Content-Type: application/json - -### 1. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (์ฐจ๋‹จํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋กœ ์š”์ฒญ) -# ์‚ฌ์šฉ์ž 5๋Š” ์•„๋ฌด๋„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ๋ชจ๋“  ๋ฆฌ๋ทฐ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ณด์ž„ -GET {{host}}/api/v1/test/block/test-data/5 -Content-Type: application/json -X-User-Id: 5 - -### 2. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) -# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ–ˆ์œผ๋ฏ€๋กœ ํ•ด๋‹น ๋ฆฌ๋ทฐ๊ฐ€ ๋งˆ์Šคํ‚น๋˜์–ด์•ผ ํ•จ -GET {{host}}/api/v1/test/block/test-data/1 -Content-Type: application/json -X-User-Id: 1 - -### 3. ์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ (๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋กœ ์š”์ฒญ) -# ์‚ฌ์šฉ์ž 4๋Š” ์•„๋ฌด๋„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ๋ชจ๋“  ๋ฆฌ๋ทฐ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ณด์ž„ -GET {{host}}/api/v1/test/block/test-data/4 -Content-Type: application/json -X-User-Id: 4 - -### 4. ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ (์‚ฌ์ „ ํ™•์ธ) -# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จํ–ˆ๋Š”์ง€ ํ™•์ธ -GET {{host}}/api/v1/test/block/check?blockerId=1&blockedId=2 -Content-Type: application/json - -### 5. ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ -# ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ (2,3์ด ๋‚˜์™€์•ผ ํ•จ) -GET {{host}}/api/v1/test/block/list/1 -Content-Type: application/json - -### 6. ์ฐจ๋‹จ ํ•ด์ œ ํ›„ ์žฌํ…Œ์ŠคํŠธ -# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2์˜ ์ฐจ๋‹จ์„ ํ•ด์ œ -DELETE {{host}}/api/v1/test/block/remove?blockerId=1&blockedId=2 -Content-Type: application/json - -### -# ์ฐจ๋‹จ ํ•ด์ œ ํ›„ ๋‹ค์‹œ ํ…Œ์ŠคํŠธ (์‚ฌ์šฉ์ž 2์˜ ๋ฆฌ๋ทฐ๊ฐ€ ๋‹ค์‹œ ๋ณด์—ฌ์•ผ ํ•จ) -GET {{host}}/api/v1/test/block/test-data/1 -Content-Type: application/json -X-User-Id: 1 \ No newline at end of file diff --git "a/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" "b/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" deleted file mode 100644 index 66614909a..000000000 --- "a/http/block/07-\354\240\204\354\262\264_\354\264\210\352\270\260\355\231\224_\355\205\214\354\212\244\355\212\270.http" +++ /dev/null @@ -1,6 +0,0 @@ -### ์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” (๋น„ํ™œ์„ฑํ™”๋œ ๊ธฐ๋Šฅ) -DELETE {{host}}/api/v1/test/block/clear -Content-Type: application/json - -### ์ฐธ๊ณ : ์ด ๊ธฐ๋Šฅ์€ DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. -### ์‹ค์ œ๋กœ๋Š” ๊ฐœ๋ณ„ ์ฐจ๋‹จ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/http/block/README.md b/http/block/README.md deleted file mode 100644 index 66bc3a1b0..000000000 --- a/http/block/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Block API ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ - -์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” `BlockTestController`์˜ ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ HTTP ํŒŒ์ผ๋“ค์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. - -## ๐Ÿ”ง ์‚ฌ์ „ ์ค€๋น„ - -1. **ํ™˜๊ฒฝ ์„ค์ •**: ์ด ์ปจํŠธ๋กค๋Ÿฌ๋Š” `local`, `dev`, `test` ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค (`prod` ํ™˜๊ฒฝ์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”) -2. **ํ™˜๊ฒฝ ๋ณ€์ˆ˜**: IntelliJ HTTP Client์—์„œ `http-client.env.json` ํŒŒ์ผ์˜ ํ™˜๊ฒฝ์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค - - `local`: http://localhost:30001 - - `dev`: http://localhost:30001 - - `prod`: https://api.bottle-note.com/ -3. **์ธ์ฆ**: Block API (`/api/v1/test/**`)๋Š” ์ธ์ฆ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (Spring Security์—์„œ `permitAll()` ์„ค์ •) -4. **ํ† ํฐ (์„ ํƒ์‚ฌํ•ญ)**: ํ•„์š”์‹œ 01๋ฒˆ ํŒŒ์ผ์˜ ํ† ํฐ ๋ฐœ๊ธ‰ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `accessToken` ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค - -## ๐Ÿ“ ํ…Œ์ŠคํŠธ ํŒŒ์ผ ๊ตฌ์„ฑ - -### 01-์ฐจ๋‹จ_๊ด€๊ณ„_์ƒ์„ฑ.http -- **๋ชฉ์ **: ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ์ƒ์„ฑํ•˜๋Š” API ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `POST /api/v1/test/block/add` -- **ํŒŒ๋ผ๋ฏธํ„ฐ**: - - `blockerId`: ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ•„์ˆ˜) - - `blockedId`: ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ•„์ˆ˜) - - `reason`: ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ, ๊ธฐ๋ณธ๊ฐ’: "ํ…Œ์ŠคํŠธ ์ฐจ๋‹จ") - -### 02-์ฐจ๋‹จ_์—ฌ๋ถ€_ํ™•์ธ.http -- **๋ชฉ์ **: ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” API ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/check` -- **ํŒŒ๋ผ๋ฏธํ„ฐ**: - - `blockerId`: ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - - `blockedId`: ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - -### 03-์ฐจ๋‹จ_๋ชฉ๋ก_์กฐํšŒ.http -- **๋ชฉ์ **: ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” API ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/list/{userId}` -- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ์ฐจ๋‹จ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID - -### 04-์ฐจ๋‹จ_ํ†ต๊ณ„_์กฐํšŒ.http -- **๋ชฉ์ **: ํŠน์ • ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ํ†ต๊ณ„๋ฅผ ์กฐํšŒํ•˜๋Š” API ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/statistics/{userId}` -- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ํ†ต๊ณ„๋ฅผ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ID - -### 05-์ฐจ๋‹จ_๊ด€๊ณ„_ํ•ด์ œ.http -- **๋ชฉ์ **: ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ํ•ด์ œํ•˜๋Š” API ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `DELETE /api/v1/test/block/remove` -- **ํŒŒ๋ผ๋ฏธํ„ฐ**: - - `blockerId`: ์ฐจ๋‹จ์„ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID - - `blockedId`: ์ฐจ๋‹จ ํ•ด์ œ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID - -### 06-์ฐจ๋‹จ_ํ•„ํ„ฐ_ํ…Œ์ŠคํŠธ.http -- **๋ชฉ์ **: `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋œ API์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ -- **์—”๋“œํฌ์ธํŠธ**: `GET /api/v1/test/block/test-data/{userId}` -- **๊ฒฝ๋กœ ๋ณ€์ˆ˜**: `userId` - ๊ฒฝ๋กœ์˜ ์‚ฌ์šฉ์ž ID (์‹ค์ œ๋กœ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ) -- **ํ—ค๋”**: `X-User-Id` - ์‹ค์ œ ์š”์ฒญํ•˜๋Š” ์‚ฌ์šฉ์ž ID (ํ…Œ์ŠคํŠธ์šฉ SecurityContext ์„ค์ •) -- **ํŠน์ง•**: - - ์‹ค์ œ ๋ฆฌ๋ทฐ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•œ ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ œ๊ณต - - ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ๋ฐ์ดํ„ฐ๋Š” "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น๋จ - - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋‹‰๋„ค์ž„์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž"๋กœ ๋ณ€๊ฒฝ๋จ - - ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋„ null๋กœ ์ฒ˜๋ฆฌ๋จ - -### 07-์ „์ฒด_์ดˆ๊ธฐํ™”_ํ…Œ์ŠคํŠธ.http -- **๋ชฉ์ **: ์ „์ฒด ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” API ํ…Œ์ŠคํŠธ (ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™”๋จ) -- **์—”๋“œํฌ์ธํŠธ**: `DELETE /api/v1/test/block/clear` -- **์ฐธ๊ณ **: DB ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ์•ˆ์ „์ƒ์˜ ์ด์œ ๋กœ ๋น„ํ™œ์„ฑํ™”๋œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค - -## ๐Ÿš€ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์‹œ - -### ๊ธฐ๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค -1. **์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ**: ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จ -2. **์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ**: 1 โ†’ 2 ์ฐจ๋‹จ ํ™•์ธ (true), 2 โ†’ 1 ์ฐจ๋‹จ ํ™•์ธ (false) -3. **์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ**: ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก์— ์‚ฌ์šฉ์ž 2๊ฐ€ ํฌํ•จ๋˜๋Š”์ง€ ํ™•์ธ -4. **์ฐจ๋‹จ ํ•„ํ„ฐ ํ…Œ์ŠคํŠธ**: ์‚ฌ์šฉ์ž 1๋กœ ์š”์ฒญ ์‹œ ์‚ฌ์šฉ์ž 2์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ™•์ธ -5. **์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ**: ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2์˜ ์ฐจ๋‹จ์„ ํ•ด์ œ -6. **์ฐจ๋‹จ ์—ฌ๋ถ€ ์žฌํ™•์ธ**: 1 โ†’ 2 ์ฐจ๋‹จ ํ™•์ธ (false) - -### ๊ณ ๊ธ‰ ์‹œ๋‚˜๋ฆฌ์˜ค -1. **์ƒํ˜ธ ์ฐจ๋‹จ**: ์‚ฌ์šฉ์ž 1๊ณผ ์‚ฌ์šฉ์ž 2๊ฐ€ ์„œ๋กœ ์ฐจ๋‹จ -2. **๋‹ค์ค‘ ์ฐจ๋‹จ**: ์‚ฌ์šฉ์ž 1์ด ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž(2, 3, 4)๋ฅผ ์ฐจ๋‹จ -3. **ํ†ต๊ณ„ ํ™•์ธ**: ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ -4. **ํ•„ํ„ฐ๋ง ํ™•์ธ**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋“ค์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ™•์ธ - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -1. **ํ™˜๊ฒฝ ์ œํ•œ**: `prod` ํ™˜๊ฒฝ์—์„œ๋Š” ์ด API๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค -2. **๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ**: ์‹ค์ œ ์šด์˜ ๋ฐ์ดํ„ฐ์™€ ์—ฐ๋™๋˜๋ฏ€๋กœ ํ…Œ์ŠคํŠธ ์‹œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค -3. **์‚ฌ์šฉ์ž ID**: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ID๋กœ ํ…Œ์ŠคํŠธํ•  ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค -4. **์ „์ฒด ์ดˆ๊ธฐํ™”**: ์•ˆ์ „์ƒ์˜ ์ด์œ ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๊ฐœ๋ณ„ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค - -## ๐Ÿ” ์‘๋‹ต ํ˜•ํƒœ - -๋ชจ๋“  API๋Š” `GlobalResponse` ํ˜•ํƒœ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค: - -```json -{ - "success": true, - "message": "์„ฑ๊ณต ๋ฉ”์‹œ์ง€", - "data": "์‘๋‹ต ๋ฐ์ดํ„ฐ" -} -``` - -์‹คํŒจ ์‹œ: -```json -{ - "success": false, - "message": "์‹คํŒจ ๋ฉ”์‹œ์ง€", - "data": null -} -``` \ No newline at end of file diff --git a/http/block/block-blur-test.http b/http/block/block-blur-test.http deleted file mode 100644 index b201ca1a9..000000000 --- a/http/block/block-blur-test.http +++ /dev/null @@ -1,10 +0,0 @@ -### ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ - ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ์‚ฌ์šฉ์ž 1์˜ ๊ฒฝ์šฐ -# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จํ–ˆ์œผ๋ฏ€๋กœ ํ•ด๋‹น ๋ฆฌ๋ทฐ๋“ค์ด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ -GET {{host}}/api/v1/test/block/reviews -Content-Type: application/json - -### ๊ฒฐ๊ณผ ํ™•์ธ: -# - ์‚ฌ์šฉ์ž 2,3์˜ reviewContent๊ฐ€ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋ณ€๊ฒฝ -# - ์‚ฌ์šฉ์ž 2,3์˜ authorNickname์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋ณ€๊ฒฝ -# - ์‚ฌ์šฉ์ž 2,3์˜ authorProfileImage๊ฐ€ null๋กœ ๋ณ€๊ฒฝ -# - ์‚ฌ์šฉ์ž 1,4,5์˜ ๋ฆฌ๋ทฐ๋Š” ์ •์ƒ ํ‘œ์‹œ \ No newline at end of file diff --git "a/http/block/\354\260\250\353\213\250_\355\225\204\355\204\260\353\247\201_\355\205\214\354\212\244\355\212\270.http" "b/http/block/\354\260\250\353\213\250_\355\225\204\355\204\260\353\247\201_\355\205\214\354\212\244\355\212\270.http" new file mode 100644 index 000000000..575fc8107 --- /dev/null +++ "b/http/block/\354\260\250\353\213\250_\355\225\204\355\204\260\353\247\201_\355\205\214\354\212\244\355\212\270.http" @@ -0,0 +1,38 @@ +### @BlockWord ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ํ…Œ์ŠคํŠธ + +### ํ† ํฐ ๋ฐœ๊ธ‰ (์ผ๋ฐ˜) +# @no-cookie-jar +POST {{host}}/api/v1/oauth/login +Content-Type: application/json + +{ + "email": "rkdtkfma@naver.com", + "socialType": "KAKAO" +} + +> {% + //ํ† ํฐ์„ ์ „์—ญ๋ณ€์ˆ˜๋กœ ์ €์žฅ + client.global.set("accessToken", response.body.data.accessToken); +%} + + +### 1. ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ +POST {{host}}/api/v1/blocks/create +Content-Type: application/json +Authorization: Bearer {{accessToken}} + +{ + "blockedUserId": 3 +} + +### 1-1. ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จ์ด ์ œ๋Œ€๋กœ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ) +GET {{host}}/api/v1/blocks +Authorization: Bearer {{accessToken}} + +### 2. ๋ฆฌ๋ทฐ ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง ํ™•์ธ +GET {{host}}/api/v1/reviews/274 +Authorization: Bearer {{accessToken}} + +### 3. ๋ฆฌ๋ทฐ ์ƒ์„ธ ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง ํ™•์ธ +GET {{host}}/api/v1/reviews/detail/1 +Authorization: Bearer {{accessToken}} diff --git a/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak b/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak deleted file mode 100644 index 001bf0512..000000000 --- a/src/main/java/app/bottlenote/common/block/annotation/BlockFilter.java.bak +++ /dev/null @@ -1,38 +0,0 @@ -package app.bottlenote.common.block.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜ - * - * ์‚ฌ์šฉ ์˜ˆ์‹œ: - * @BlockFilter( - * userField = "authorId", - * blurFields = {"reviewContent", "authorNickname", "authorProfileImage"} - * ) - * @GetMapping("/reviews") - * public ResponseEntity getReviews() { ... } - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockFilter { - - /** - * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… - * ์˜ˆ: "authorId", "userId", "writerId" - */ - String userField() default "authorId"; - - /** - * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•  ํ•„๋“œ๋ช… ๋ฐฐ์—ด - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ ์ด ํ•„๋“œ๋“ค์ด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋Œ€์ฒด๋จ - */ - String[] blurFields() default { - "content", "reviewContent", "title", - "authorNickname", "nickname", - "authorProfileImage", "profileImage" - }; -} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java b/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java index f4ac91625..682027774 100644 --- a/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java +++ b/src/main/java/app/bottlenote/common/block/annotation/BlockWord.java @@ -1,5 +1,8 @@ package app.bottlenote.common.block.annotation; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -11,18 +14,20 @@ */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@JsonSerialize(using = app.bottlenote.common.block.serializer.BlockWordSerializer.class) public @interface BlockWord { - + /** * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" */ String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; - + /** * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ * ๊ธฐ๋ณธ๊ฐ’: "userId" * ์ค‘์ฒฉ ๊ฐ์ฒด ์˜ˆ์‹œ: "userInfo.userId" */ String userIdPath() default "userId"; -} \ No newline at end of file +} diff --git a/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak b/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak deleted file mode 100644 index bbe8632a7..000000000 --- a/src/main/java/app/bottlenote/common/block/aspect/BlockFilterAspect.java.bak +++ /dev/null @@ -1,238 +0,0 @@ -package app.bottlenote.common.block.aspect; - -import app.bottlenote.common.block.annotation.BlockFilter; -import app.bottlenote.common.block.service.BlockService; -import app.bottlenote.global.data.response.GlobalResponse; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ํ•„ํ„ฐ AOP - * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ -@Slf4j -@Aspect -@Component -@RequiredArgsConstructor -public class BlockFilterAspect { - - private final BlockService blockService; - private final ObjectMapper objectMapper; - - @Around("@annotation(blockFilter)") - public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) throws Throwable { - - // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ - Object result = joinPoint.proceed(); - - // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ (ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ—ค๋”์—์„œ ๊ฐ€์ ธ์˜ด) - Long currentUserId = extractCurrentUserId(); - - if (currentUserId == -1L) { - log.debug("์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž - ์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉํ•˜์ง€ ์•Š์Œ"); - return result; - } - - // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ - Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); - - if (blockedUserIds.isEmpty()) { - log.debug("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Œ - ํ•„ํ„ฐ๋ง ๊ฑด๋„ˆ๋œ€"); - return result; - } - - log.info("์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉ: ์‚ฌ์šฉ์ž {}, ์ฐจ๋‹จ ๋Œ€์ƒ {}", currentUserId, blockedUserIds); - - // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ - return applyBlurFilter(result, blockedUserIds, blockFilter); - } - - /** - * ํ…Œ์ŠคํŠธ์šฉ: ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ - * ์‹ค์ œ ์šด์˜์—์„œ๋Š” SecurityContext์—์„œ ๊ฐ€์ ธ์˜ด - */ - private Long extractCurrentUserId() { - // ํ˜„์žฌ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๊ณ ์ •๊ฐ’ ๋ฐ˜ํ™˜ - // ์‹ค์ œ๋กœ๋Š” SecurityContextUtil.getUserIdByContext() ์‚ฌ์šฉ - return 1L; // ํ…Œ์ŠคํŠธ์šฉ: ์‚ฌ์šฉ์ž 1๋กœ ๊ณ ์ • - } - - /** - * ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ - */ - private Object applyBlurFilter(Object result, Set blockedUserIds, BlockFilter blockFilter) { - - if (!(result instanceof ResponseEntity)) { - return result; - } - - ResponseEntity responseEntity = (ResponseEntity) result; - Object body = responseEntity.getBody(); - - if (!(body instanceof GlobalResponse)) { - return result; - } - - GlobalResponse globalResponse = (GlobalResponse) body; - Object data = globalResponse.getData(); - - if (data == null) { - return result; - } - - // ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - Object blurredData; - if (data instanceof List) { - blurredData = blurListData((List) data, blockedUserIds, blockFilter); - } else { - blurredData = blurSingleData(data, blockedUserIds, blockFilter); - } - - // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ์šด ์‘๋‹ต ์ƒ์„ฑ - GlobalResponse blurredResponse = GlobalResponse.success(blurredData); - return ResponseEntity.ok(blurredResponse); - } - - /** - * List ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - @SuppressWarnings("unchecked") - private List blurListData(List dataList, Set blockedUserIds, BlockFilter blockFilter) { - return dataList.stream() - .map(item -> blurItemIfBlocked(item, blockedUserIds, blockFilter)) - .collect(Collectors.toList()); - } - - /** - * ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - private Object blurSingleData(Object data, Set blockedUserIds, BlockFilter blockFilter) { - return blurItemIfBlocked(data, blockedUserIds, blockFilter); - } - - /** - * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - */ - private Object blurItemIfBlocked(Object item, Set blockedUserIds, BlockFilter blockFilter) { - if (item == null) { - return item; - } - - // ์‚ฌ์šฉ์ž ID ์ถ”์ถœ - Long userId = extractUserIdFromObject(item, blockFilter.userField()); - - if (userId == null || !blockedUserIds.contains(userId)) { - return item; // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ์›๋ณธ ๋ฐ˜ํ™˜ - } - - log.debug("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ  ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ: userId={}", userId); - - // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฉด ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ - return applyBlurToItem(item, blockFilter.blurFields()); - } - - /** - * ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœ - */ - private Long extractUserIdFromObject(Object obj, String fieldName) { - try { - Field field = findFieldInClass(obj.getClass(), fieldName); - if (field != null) { - field.setAccessible(true); - Object value = field.get(obj); - if (value instanceof Long) { - return (Long) value; - } - } - } catch (Exception e) { - log.warn("์‚ฌ์šฉ์ž ID ์ถ”์ถœ ์‹คํŒจ: ๊ฐ์ฒด={}, ํ•„๋“œ={}", obj.getClass().getSimpleName(), fieldName); - } - return null; - } - - /** - * ํด๋ž˜์Šค ๊ณ„์ธต๊ตฌ์กฐ์—์„œ ํ•„๋“œ ์ฐพ๊ธฐ - */ - private Field findFieldInClass(Class clazz, String fieldName) { - Class currentClass = clazz; - while (currentClass != null) { - try { - return currentClass.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - currentClass = currentClass.getSuperclass(); - } - } - return null; - } - - /** - * ๊ฐœ๋ณ„ ํ•ญ๋ชฉ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ - */ - private Object applyBlurToItem(Object originalItem, String[] blurFields) { - try { - // Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ - Object clonedItem = deepCloneObject(originalItem); - - // ์ง€์ •๋œ ํ•„๋“œ๋“ค์„ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"๋กœ ๋งˆ์Šคํ‚น - for (String fieldName : blurFields) { - maskField(clonedItem, fieldName, "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"); - } - - return clonedItem; - - } catch (Exception e) { - log.error("๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์‹คํŒจ: {}", e.getMessage()); - return originalItem; // ์‹คํŒจ ์‹œ ์›๋ณธ ๋ฐ˜ํ™˜ - } - } - - /** - * ๊ฐ์ฒด์˜ ํŠน์ • ํ•„๋“œ๋ฅผ ๋งˆ์Šคํ‚น - */ - private void maskField(Object obj, String fieldName, String maskValue) { - try { - Field field = findFieldInClass(obj.getClass(), fieldName); - if (field != null) { - field.setAccessible(true); - - // ํ•„๋“œ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋งˆ์Šคํ‚น ๊ฐ’ ์„ค์ • - if (fieldName.toLowerCase().contains("image") || fieldName.toLowerCase().contains("url")) { - field.set(obj, null); // ์ด๋ฏธ์ง€/URL์€ null๋กœ ์ฒ˜๋ฆฌ - } else { - field.set(obj, maskValue); // ํ…์ŠคํŠธ๋Š” ๋งˆ์Šคํ‚น ๋ฉ”์‹œ์ง€๋กœ ์ฒ˜๋ฆฌ - } - } - } catch (Exception e) { - // ํ•„๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์„ค์ • ์‹คํŒจ ์‹œ ๋ฌด์‹œ (์ •์ƒ์ ์ธ ์ƒํ™ฉ) - log.debug("ํ•„๋“œ ๋งˆ์Šคํ‚น ๊ฑด๋„ˆ๋œ€: {}.{}", obj.getClass().getSimpleName(), fieldName); - } - } - - /** - * Jackson์„ ์‚ฌ์šฉํ•œ ๊นŠ์€ ๋ณต์‚ฌ - */ - private Object deepCloneObject(Object original) { - try { - if (original == null) { - return null; - } - - String jsonString = objectMapper.writeValueAsString(original); - return objectMapper.readValue(jsonString, original.getClass()); - } catch (Exception e) { - log.warn("๊ฐ์ฒด ๋ณต์‚ฌ ์‹คํŒจ, ์›๋ณธ ๋ฐ˜ํ™˜: {}", e.getMessage()); - return original; - } - } -} diff --git a/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java index 5056a6cff..a1df56fc5 100644 --- a/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java +++ b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java @@ -1,29 +1,19 @@ package app.bottlenote.common.block.config; -import app.bottlenote.common.block.serializer.BlockWordSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; /** * BlockWord ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์„ค์ • + * + * @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์— @JsonSerialize๊ฐ€ ์ง์ ‘ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ + * ๊ธ€๋กœ๋ฒŒ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๋“ฑ๋ก์€ ๋ถˆํ•„์š”ํ•จ */ +@Slf4j @Configuration -@RequiredArgsConstructor public class BlockWordConfig { - private final ObjectMapper objectMapper; - private final BlockWordSerializer blockWordSerializer; - - @PostConstruct - public void configureObjectMapper() { - SimpleModule blockWordModule = new SimpleModule("BlockWordModule"); - - // String ํƒ€์ž…์— ๋Œ€ํ•ด @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์„ ๋•Œ ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์‚ฌ์šฉ - blockWordModule.addSerializer(String.class, blockWordSerializer); - - objectMapper.registerModule(blockWordModule); + public BlockWordConfig() { + log.debug("BlockWordConfig ์ดˆ๊ธฐํ™” ์™„๋ฃŒ - @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ž์ฒด์— @JsonSerialize ํฌํ•จ"); } } diff --git a/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak b/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak deleted file mode 100644 index 61da4b552..000000000 --- a/src/main/java/app/bottlenote/common/block/controller/BlockTestController.java.bak +++ /dev/null @@ -1,70 +0,0 @@ -package app.bottlenote.common.block.controller; - -import app.bottlenote.common.block.annotation.BlockFilter; -import app.bottlenote.global.data.response.GlobalResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Profile; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ - * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๋งŒ ์ง‘์ค‘ - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/test/block") -@Profile({"local", "dev", "test", "!prod"}) -public class BlockTestController { - - /** - * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ชฉ๋ก API - * - * ์ฐจ๋‹จ ๊ด€๊ณ„ (ํ•˜๋“œ์ฝ”๋”ฉ): - * - ์‚ฌ์šฉ์ž 1 โ†’ ์‚ฌ์šฉ์ž 2,3 ์ฐจ๋‹จ - * - ์‚ฌ์šฉ์ž 2 โ†’ ์‚ฌ์šฉ์ž 4 ์ฐจ๋‹จ - * - ์‚ฌ์šฉ์ž 3 โ†’ ์‚ฌ์šฉ์ž 1,5 ์ฐจ๋‹จ - */ - @BlockFilter( - userField = "authorId", - blurFields = {"reviewContent", "authorNickname", "authorProfileImage"} - ) - @GetMapping("/reviews") - public ResponseEntity getTestReviews() { - - log.info("๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ"); - - List> reviews = List.of( - createReview(1L, "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์˜€์Šต๋‹ˆ๋‹ค!", "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", "profile1.jpg"), - createReview(2L, "์ฒซ ์‹œ์Œ์ธ๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ์ข‹๋„ค์š”", "์ดˆ๋ณด์œ„์Šคํ‚ค", "profile2.jpg"), - createReview(3L, "๊ฐ€๊ฒฉ ๋Œ€๋น„ ํ›Œ๋ฅญํ•ฉ๋‹ˆ๋‹ค", "์œ„์Šคํ‚ค๋งˆ๋‹ˆ์•„", "profile3.jpg"), - createReview(4L, "์˜คํฌํ–ฅ์ด ๊ฐ•ํ•ด์„œ ํ˜ธ๋ถˆํ˜ธ๊ฐ€ ๊ฐˆ๋ฆด๋“ฏ", "๋ฆฌ๋ทฐ๊ณ ์ˆ˜", "profile4.jpg"), - createReview(5L, "์นœ๊ตฌ๋“ค๊ณผ ๋งˆ์‹œ๊ธฐ ์ข‹์•˜์–ด์š”", "์†Œ์…œ๋“œ๋ง์ปค", "profile5.jpg") - ); - - return GlobalResponse.ok(Map.of( - "totalCount", reviews.size(), - "reviews", reviews - )); - } - - /** - * ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - */ - private Map createReview(Long authorId, String content, String nickname, String profileImage) { - return Map.of( - "reviewId", 1000L + authorId, - "authorId", authorId, - "reviewContent", content, - "authorNickname", nickname, - "authorProfileImage", profileImage, - "createAt", LocalDateTime.now().minusDays(authorId), - "likeCount", (int)(Math.random() * 20), - "rating", 4.0 + (Math.random() * 1.0) - ); - } -} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java index f2ef60231..f3adf9aba 100644 --- a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java +++ b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java @@ -1,15 +1,14 @@ package app.bottlenote.common.block.serializer; import app.bottlenote.common.block.annotation.BlockWord; -import app.bottlenote.common.block.service.BlockService; import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.support.block.service.BlockService; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -18,15 +17,19 @@ /** * @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋œ ํ•„๋“œ์˜ Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € + * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. */ @Slf4j @Component -@RequiredArgsConstructor public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer { private final BlockService blockService; private BlockWord blockWordAnnotation; + public BlockWordSerializer(BlockService blockService) { + this.blockService = blockService; + } + @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (blockWordAnnotation == null) { @@ -74,24 +77,29 @@ public void serialize(String value, JsonGenerator gen, SerializerProvider serial } @Override - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - + if (property != null) { + // @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ฐพ๊ธฐ BlockWord annotation = property.getAnnotation(BlockWord.class); + if (annotation == null && property.getMember() != null) { + annotation = property.getMember().getAnnotation(BlockWord.class); + } + if (annotation != null) { BlockWordSerializer serializer = new BlockWordSerializer(blockService); serializer.blockWordAnnotation = annotation; return serializer; } } - + return this; } /** * ์ง€์ •๋œ ๊ฒฝ๋กœ๋กœ ๊ฐ์ฒด์—์„œ userId ์ถ”์ถœ - * + * * @param object ๋Œ€์ƒ ๊ฐ์ฒด * @param userIdPath userId ๊ฒฝ๋กœ (์˜ˆ: "userId", "userInfo.userId") * @return ์ถ”์ถœ๋œ userId, ์‹คํŒจ์‹œ null @@ -109,7 +117,6 @@ private Long extractUserIdByPath(Object object, String userIdPath) { Field field = findField(currentObject.getClass(), part); if (field == null) { - log.warn("ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {} in {}", part, currentObject.getClass().getSimpleName()); return null; } @@ -117,13 +124,7 @@ private Long extractUserIdByPath(Object object, String userIdPath) { currentObject = field.get(currentObject); } - // ์ตœ์ข… ๊ฐ’์ด Long ํƒ€์ž…์ธ์ง€ ํ™•์ธ - if (currentObject instanceof Long) { - return (Long) currentObject; - } else { - log.warn("userId ํ•„๋“œ๊ฐ€ Long ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค: {}", currentObject != null ? currentObject.getClass() : "null"); - return null; - } + return currentObject instanceof Long ? (Long) currentObject : null; } catch (Exception e) { log.warn("userId ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {}", e.getMessage()); @@ -136,7 +137,7 @@ private Long extractUserIdByPath(Object object, String userIdPath) { */ private Field findField(Class clazz, String fieldName) { Class currentClass = clazz; - + while (currentClass != null) { try { return currentClass.getDeclaredField(fieldName); @@ -144,7 +145,7 @@ private Field findField(Class clazz, String fieldName) { currentClass = currentClass.getSuperclass(); } } - + return null; } -} \ No newline at end of file +} diff --git a/src/main/java/app/bottlenote/common/block/service/BlockService.java b/src/main/java/app/bottlenote/common/block/service/BlockService.java deleted file mode 100644 index 6e4072657..000000000 --- a/src/main/java/app/bottlenote/common/block/service/BlockService.java +++ /dev/null @@ -1,48 +0,0 @@ -package app.bottlenote.common.block.service; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Set; - -/** - * ๊ฐ„์†Œํ™”๋œ ์ฐจ๋‹จ ์„œ๋น„์Šค - * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ - */ -@Slf4j -@Service -public class BlockService { - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ - * ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - */ - public Set getBlockedUserIds(Long userId) { - if (userId == null || userId == -1L) { - return Set.of(); - } - - // ํ…Œ์ŠคํŠธ์šฉ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ฐจ๋‹จ ๊ด€๊ณ„ - return switch (userId.intValue()) { - case 1 -> Set.of(2L, 3L); // ์‚ฌ์šฉ์ž 1์€ ์‚ฌ์šฉ์ž 2,3์„ ์ฐจ๋‹จ - case 2 -> Set.of(4L); // ์‚ฌ์šฉ์ž 2๋Š” ์‚ฌ์šฉ์ž 4๋ฅผ ์ฐจ๋‹จ - case 3 -> Set.of(1L, 5L); // ์‚ฌ์šฉ์ž 3์€ ์‚ฌ์šฉ์ž 1,5๋ฅผ ์ฐจ๋‹จ - default -> Set.of(); // ๋‚˜๋จธ์ง€๋Š” ์ฐจ๋‹จ ์—†์Œ - }; - } - - /** - * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ - */ - public boolean isBlocked(Long blockerId, Long blockedId) { - if (blockerId == null || blockedId == null) { - return false; - } - - Set blockedUsers = getBlockedUserIds(blockerId); - return blockedUsers.contains(blockedId); - } -} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.0.md b/src/main/java/app/bottlenote/common/block/v1.0.md deleted file mode 100644 index 5cdc7c269..000000000 --- a/src/main/java/app/bottlenote/common/block/v1.0.md +++ /dev/null @@ -1,158 +0,0 @@ -# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.0 - AOP ๋ฐฉ์‹ - -## ๐Ÿ“‹ ๊ฐœ์š” - -**AOP ๊ธฐ๋ฐ˜ ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ๋ฒจ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜์„ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์ ์šฉํ•˜์—ฌ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ "๋ธ”๋Ÿฌ" ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹ - -## ๐ŸŽฏ @BlockFilter ์–ด๋…ธํ…Œ์ด์…˜ - -```java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockFilter { - /** - * ์‘๋‹ต ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉ์ž ID๋ฅผ ์ถ”์ถœํ•  ํ•„๋“œ๋ช… - */ - String userField() default "authorId"; - - /** - * ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌํ•  ํ•„๋“œ๋ช… ๋ฐฐ์—ด - */ - String[] blurFields() default { - "content", "reviewContent", "title", - "authorNickname", "nickname", - "authorProfileImage", "profileImage" - }; -} -``` - -## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ - -### ์ปจํŠธ๋กค๋Ÿฌ ์ ์šฉ -```java -@RestController -public class ReviewController { - - @BlockFilter(userField = "authorId") - @GetMapping("/{alcoholId}") - public ResponseEntity getReviews(@PathVariable Long alcoholId) { - // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์‹คํ–‰ - return GlobalResponse.ok(reviewService.getReviews(alcoholId)); - } -} -``` - -## ๐Ÿ”ง ๋™์ž‘ ์›๋ฆฌ - -### 1. AOP Aspect ์ฒ˜๋ฆฌ ํ๋ฆ„ -```java -@Around("@annotation(blockFilter)") -public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) { - // 1. ์›๋ณธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ - Object result = joinPoint.proceed(); - - // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ - Long currentUserId = extractCurrentUserId(); - - // 3. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ - Set blockedUserIds = blockService.getBlockedUserIds(currentUserId); - - // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์šฉ - return applyBlurFilter(result, blockedUserIds, blockFilter); -} -``` - -### 2. ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๊ณผ์ • -1. **ResponseEntity โ†’ GlobalResponse โ†’ data ์ˆœ์œผ๋กœ ํƒ์ƒ‰** -2. **๋ฐ์ดํ„ฐ ํƒ€์ž… ํ™•์ธ**: List ๋˜๋Š” ๋‹จ์ผ ๊ฐ์ฒด -3. **๊ฐ ํ•ญ๋ชฉ๋ณ„๋กœ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ** (`userField` ์‚ฌ์šฉ) -4. **์ฐจ๋‹จ ๋Œ€์ƒ์ธ์ง€ ํ™•์ธ** (blockedUserIds์— ํฌํ•จ ์—ฌ๋ถ€) -5. **์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๊ฐ์ฒด ๋ณต์ œ ํ›„ ํ•„๋“œ ๋งˆ์Šคํ‚น** - -### 3. ํ•„๋“œ ๋งˆ์Šคํ‚น ๊ทœ์น™ -```java -// ํ…์ŠคํŠธ ํ•„๋“œ: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" -reviewContent โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" -authorNickname โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" - -// ์ด๋ฏธ์ง€/URL ํ•„๋“œ: null ์ฒ˜๋ฆฌ -authorProfileImage โ†’ null -imageUrl โ†’ null -``` - -## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ - -### ์ •์ƒ ์‚ฌ์šฉ์ž ์‘๋‹ต -```json -{ - "success": true, - "data": { - "reviewList": [ - { - "authorId": 123, - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", - "authorProfileImage": "profile.jpg" - } - ] - } -} -``` - -### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์‘๋‹ต (๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ํ›„) -```json -{ - "success": true, - "data": { - "reviewList": [ - { - "authorId": 456, - "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", - "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", - "authorProfileImage": null - } - ] - } -} -``` - -## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๊ตฌ์กฐ - -### ํ•ต์‹ฌ ํŒŒ์ผ๋“ค -- `annotation/BlockFilter.java` - ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ -- `aspect/BlockFilterAspect.java` - AOP ์ฒ˜๋ฆฌ ๋กœ์ง -- `service/BlockService.java` - ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ -- `controller/BlockTestController.java` - ํ…Œ์ŠคํŠธ ์ปจํŠธ๋กค๋Ÿฌ - -### ์ฃผ์š” ๋ฉ”์„œ๋“œ -1. **extractCurrentUserId()**: ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ์ถ”์ถœ (ํ…Œ์ŠคํŠธ์šฉ ๊ณ ์ •๊ฐ’) -2. **applyBlurFilter()**: ์‘๋‹ต ๋ฐ์ดํ„ฐ ํƒ€์ž…๋ณ„ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ„๊ธฐ -3. **blurItemIfBlocked()**: ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ -4. **applyBlurToItem()**: Jackson ๊นŠ์€ ๋ณต์‚ฌ ํ›„ ํ•„๋“œ ๋งˆ์Šคํ‚น -5. **maskField()**: ํ•„๋“œ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋งˆ์Šคํ‚น ๊ฐ’ ์„ค์ • - -## ๐Ÿšง ํ•œ๊ณ„์  - -### 1. ๋ณต์žกํ•œ ์ฒ˜๋ฆฌ ๊ณผ์ • -- **ResponseEntity โ†’ GlobalResponse โ†’ data ๊ตฌ์กฐ ์˜์กด์„ฑ** -- **๋ฆฌํ”Œ๋ ‰์…˜ ๊ธฐ๋ฐ˜ ํ•„๋“œ ์ ‘๊ทผ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ด์Šˆ** -- **Jackson ๊นŠ์€ ๋ณต์‚ฌ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€** - -### 2. ์ปจํŠธ๋กค๋Ÿฌ ๊ฒฐํ•ฉ๋„ -- **๊ฐ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋งˆ๋‹ค ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€ ํ•„์š”** -- **userField ์„ค์ •์ด ์‘๋‹ต ๊ตฌ์กฐ์— ์˜์กด์ ** -- **์ƒˆ๋กœ์šด ์‘๋‹ต ๊ตฌ์กฐ ์ถ”๊ฐ€ ์‹œ blurFields ์žฌ์„ค์ • ํ•„์š”** - -### 3. ์œ ์ง€๋ณด์ˆ˜ ์–ด๋ ค์›€ -- **AOP ์ฒ˜๋ฆฌ ๊ณผ์ •์ด ๋ณต์žกํ•˜์—ฌ ๋””๋ฒ„๊น… ์–ด๋ ค์›€** -- **์‘๋‹ต ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ํ•„ํ„ฐ๋ง ๋กœ์ง๋„ ํ•จ๊ป˜ ์ˆ˜์ • ํ•„์š”** -- **ํ…Œ์ŠคํŠธ์šฉ ๊ณ ์ • ์‚ฌ์šฉ์ž ID ๋“ฑ ์ž„์‹œ ๊ตฌํ˜„ ์กด์žฌ** - -### 4. ํ™•์žฅ์„ฑ ์ œํ•œ -- **ํŠน์ • ์‘๋‹ต ๊ตฌ์กฐ(GlobalResponse)์—๋งŒ ๋™์ž‘** -- **์ค‘์ฒฉ ๊ฐ์ฒด ๋‚ด๋ถ€ ํ•„๋“œ ์ฒ˜๋ฆฌ ๋ณต์žก** -- **๋‹ค์–‘ํ•œ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ ์šฉ ์–ด๋ ค์›€** - -## ๐Ÿ“ ๊ฒฐ๋ก  - -AOP ๋ฐฉ์‹์€ ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ๋ฒจ์—์„œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์„ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ๋ณต์žกํ•œ ๊ตฌ์กฐ์™€ ๋†’์€ ๊ฒฐํ•ฉ๋„๋กœ ์ธํ•ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์šด ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ์‘๋‹ต ๊ตฌ์กฐ์— ๋Œ€ํ•œ ์˜์กด์„ฑ์ด ๋†’๊ณ , ์„ฑ๋Šฅ์ƒ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.1.md b/src/main/java/app/bottlenote/common/block/v1.1.md deleted file mode 100644 index f3376315c..000000000 --- a/src/main/java/app/bottlenote/common/block/v1.1.md +++ /dev/null @@ -1,189 +0,0 @@ -# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.1 - ๋ณต์žกํ•œ Jackson ์ธํ„ฐํŽ˜์ด์Šค ๋ฐฉ์‹ - -## ๐Ÿ“‹ ๊ฐœ์š” - -**์‹ฌํ”Œํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜์„ ํ•„๋“œ์— ์ง์ ‘ ์ ์šฉํ•˜์—ฌ Jackson ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ์‹ - -## ๐ŸŽฏ ํ•ต์‹ฌ ์•„์ด๋””์–ด - -### ๊ธฐ๋ณธ ์›์น™ -- **ํ•„๋“œ ๋ ˆ๋ฒจ ์ œ์–ด**: ์‘๋‹ต ๊ฐ์ฒด ๊ตฌ์กฐ๋Š” ๊ทธ๋Œ€๋กœ, ํ•„๋“œ๋ณ„๋กœ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ -- **๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ๋ฐฉ์‹**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜์ง€ ์•Š๊ณ  ์ง€์ •๋œ ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด -- **Jackson ์ง๋ ฌํ™” ํ™œ์šฉ**: JSON ๋ณ€ํ™˜ ์‹œ์ ์—์„œ ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋ง ์ฒ˜๋ฆฌ -- **์ตœ์†Œํ•œ์˜ ๋ณ€๊ฒฝ**: ๊ธฐ์กด ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ตœ๋Œ€ํ•œ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ธ”๋ก ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - -## ๐Ÿ—๏ธ ์„ค๊ณ„ ์ƒ์„ธ - -### 1. ์‘๋‹ต ๊ตฌ์กฐ ์œ ์ง€ -```java -// ReviewListResponse - ๊ธฐ์กด ๊ตฌ์กฐ ๊ทธ๋Œ€๋กœ ์œ ์ง€ -public record ReviewListResponse( - Long totalCount, - List reviewList // ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ -) { - public static ReviewListResponse of(Long totalCount, List reviewList) { - return new ReviewListResponse(totalCount, reviewList); - } -} -``` - -### 2. ํ•„๋“œ ๋ ˆ๋ฒจ ๋ธ”๋ก ํ•„ํ„ฐ๋ง -```java -// ReviewInfo - ํŠน์ • ํ•„๋“œ์—๋งŒ @BlockWord ์ ์šฉ -@Builder -public record ReviewInfo( - Long reviewId, - - @BlockWord // ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ ๋‚ด์šฉ ํ•„ํ„ฐ๋ง - String reviewContent, - - String reviewImageUrl, - // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค (๋ธ”๋ก ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š”) - - UserInfo userInfo, // ๋‚ด๋ถ€์—์„œ ๋‹‰๋„ค์ž„ ํ•„ํ„ฐ๋ง - // ... ๋‚˜๋จธ์ง€ ํ•„๋“œ๋“ค -) { -} -``` - -### 3. ์‚ฌ์šฉ์ž ์ •๋ณด ๋ธ”๋ก ํ•„ํ„ฐ๋ง -```java -// UserInfo - ๋‹‰๋„ค์ž„์— ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์ ์šฉ -public record UserInfo( - Long userId, // userId๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ (์ฐจ๋‹จ ๊ด€๊ณ„ ํŒ๋‹จ์šฉ) - - @BlockWord(๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž") // ์ปค์Šคํ…€ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ - String nickName, - - String userProfileImage // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ์œ ์ง€ -) { - public static UserInfo of(Long userId, String nickName, String userProfileImage) { - return new UserInfo(userId, nickName, userProfileImage); - } -} -``` - -## ๐Ÿ”ง @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์„ค๊ณ„ - -### ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ -```java -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockWord { - /** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ - * ๊ธฐ๋ณธ๊ฐ’: ๋นˆ ๋ฌธ์ž์—ด (์ปจํ…์ธ  ์™„์ „ ์ˆจ๊น€) - */ - String ๋Œ€์ฒด๋ฉ”์‹œ์ง€() default ""; - - /** - * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… - * ๊ธฐ๋ณธ๊ฐ’: ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ ์‚ฌ์šฉ - */ - String userIdField() default "userId"; -} -``` - -### ์‚ฌ์šฉ ์˜ˆ์‹œ -```java -// ์ผ€์ด์Šค 1: ์ปจํ…์ธ  ์™„์ „ ์ˆจ๊น€ -@BlockWord -String reviewContent; // โ†’ "" - -// ์ผ€์ด์Šค 2: ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด -@BlockWord(๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž") -String nickName; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" - -// ์ผ€์ด์Šค 3: ๋‹ค๋ฅธ ํ•„๋“œ์˜ userId ์ฐธ์กฐ -@BlockWord(userIdField = "authorId", ๋Œ€์ฒด๋ฉ”์‹œ์ง€ = "์ฐจ๋‹จ๋œ ๋Œ“๊ธ€") -String commentContent; -``` - -## ๐ŸŽจ ์ฒ˜๋ฆฌ ํ๋ฆ„ - -### 1. ์ •์ƒ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  -```json -{ - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "userInfo": { - "userId": 123, - "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", - "userProfileImage": "profile.jpg" - } -} -``` - -### 2. ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  (ํ•„ํ„ฐ๋ง ํ›„) -```json -{ - "reviewContent": "", // @BlockWord๋กœ ๋นˆ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ - "userInfo": { - "userId": 456, // userId๋Š” ์œ ์ง€ (์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ) - "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", // ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๊ต์ฒด - "userProfileImage": "profile.jpg" // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” ์œ ์ง€ - } -} -``` - -## ๐Ÿ” ๋™์ž‘ ์›๋ฆฌ - -### Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ฒ˜๋ฆฌ ๊ณผ์ • - -1. **ํ•„๋“œ ์Šค์บ”**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํ•„๋“œ ๊ฐ์ง€ -2. **์‚ฌ์šฉ์ž ID ํš๋“**: ํ˜„์žฌ ์š”์ฒญ ์‚ฌ์šฉ์ž์™€ ์ปจํ…์ธ  ์ž‘์„ฑ์ž ID ํ™•์ธ -3. **์ฐจ๋‹จ ๊ด€๊ณ„ ๊ฒ€์‚ฌ**: `BlockService`๋ฅผ ํ†ตํ•ด ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์บ์‹œ ํ™œ์šฉ) -4. **์กฐ๊ฑด๋ถ€ ๋Œ€์ฒด**: ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ง€์ •๋œ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ -5. **JSON ์ถœ๋ ฅ**: ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ JSON์œผ๋กœ ์ง๋ ฌํ™” - -## ๐Ÿš€ ์žฅ์  - -### 1. ์‹ฌํ”Œํ•จ -- **์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ๋ณ€๊ฒฝ**: ๊ธฐ์กด DTO์— ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ -- **์ง๊ด€์ **: ์–ด๋–ค ํ•„๋“œ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ํ•œ๋ˆˆ์— ํŒŒ์•… ๊ฐ€๋Šฅ -- **์„ค์ • ๋ถˆํ•„์š”**: ๋ณต์žกํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋‚˜ ์„ค์ • ํด๋ž˜์Šค ๋ถˆํ•„์š” - -### 2. ์œ ์—ฐ์„ฑ -- **ํ•„๋“œ๋ณ„ ์ œ์–ด**: ๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๋‹ค๋ฅธ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์„ค์ • ๊ฐ€๋Šฅ -- **์„ ํƒ์  ์ ์šฉ**: ํ•„์š”ํ•œ ํ•„๋“œ์—๋งŒ ์„ ํƒ์ ์œผ๋กœ ์ ์šฉ -- **์ปค์Šคํ„ฐ๋งˆ์ด์ง•**: userIdField๋กœ ๋‹ค์–‘ํ•œ ๊ตฌ์กฐ์— ๋Œ€์‘ - -### 3. ์„ฑ๋Šฅ -- **Jackson ๋ ˆ๋ฒจ ์ฒ˜๋ฆฌ**: ์ง๋ ฌํ™” ์‹œ์ ์—๋งŒ ์‹คํ–‰์œผ๋กœ ํšจ์œจ์  -- **์บ์‹œ ํ™œ์šฉ**: ๊ธฐ์กด BlockService์˜ ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ทธ๋Œ€๋กœ ํ™œ์šฉ -- **์กฐ๊ฑด๋ถ€ ์‹คํ–‰**: ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ฒ˜๋ฆฌ - -### 4. ์œ ์ง€๋ณด์ˆ˜์„ฑ -- **๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ**: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์™„์ „ ๋ถ„๋ฆฌ -- **๊ธฐ์กด ๊ตฌ์กฐ ์œ ์ง€**: ์‘๋‹ต ๊ฐ์ฒด๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ -- **ํ™•์žฅ ์šฉ์ด**: ์ƒˆ๋กœ์šด DTO์— ์‰ฝ๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅ - -## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๋ฒ”์œ„ - -### Phase 1: ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ -- `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ -- Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๊ตฌํ˜„ -- ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ํš๋“ ๋กœ์ง - -### Phase 2: DTO ์ ์šฉ -- `ReviewInfo`์— `reviewContent` ํ•„๋“œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ -- `UserInfo`์— `nickName` ํ•„๋“œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ ์šฉ - -### Phase 3: ๊ธฐ์กด ์ฝ”๋“œ ์ •๋ฆฌ -- ์ปจํŠธ๋กค๋Ÿฌ์˜ `@BlockFilter` ์–ด๋…ธํ…Œ์ด์…˜ ์ œ๊ฑฐ -- AOP ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์ฝ”๋“œ ์ œ๊ฑฐ - -## ๐ŸŽฏ ์˜ˆ์ƒ ๊ฒฐ๊ณผ - -### ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜ -- **๊ฐ„๋‹จํ•œ ์ ์šฉ**: `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ํ•˜๋‚˜๋กœ ๋ธ”๋ก ํ•„ํ„ฐ๋ง ์™„์„ฑ -- **๋ช…ํ™•ํ•œ ์˜๋„**: ์ฝ”๋“œ๋งŒ ๋ด๋„ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ๋ธ”๋ก ๋Œ€์ƒ์ธ์ง€ ์ฆ‰์‹œ ํŒŒ์•… -- **๋””๋ฒ„๊น… ์šฉ์ด**: JSON ์‘๋‹ต์—์„œ ๋ฐ”๋กœ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ ํ™•์ธ ๊ฐ€๋Šฅ - -### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ -- **์ž์—ฐ์Šค๋Ÿฌ์šด ์ฐจ๋‹จ**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋Œ€์ฒด๋จ -- **์ •๋ณด ์œ ์ง€**: ์‚ฌ์šฉ์ž ID ๋“ฑ ํ•„์š”ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ -- **์ผ๊ด€์„ฑ**: ๋ชจ๋“  API์—์„œ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ - -## ๐Ÿ“ ๊ฒฐ๋ก  - -`@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์€ ๋ณต์žกํ•œ ์„ค์ • ์—†์ด๋„ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ๋ธ”๋ก ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ตœ๋Œ€ํ•œ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•œ ์šฐ์•„ํ•œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.3.md b/src/main/java/app/bottlenote/common/block/v1.3.md deleted file mode 100644 index 1ea582fc0..000000000 --- a/src/main/java/app/bottlenote/common/block/v1.3.md +++ /dev/null @@ -1,181 +0,0 @@ -# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.3 - ์ค‘์ฒฉ ๊ฐ์ฒด ์ง€์› @BlockWord ๋ฐฉ์‹ - -## ๐Ÿ“‹ ๊ฐœ์š” - -**@BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ๋ธ”๋ก ํ•„ํ„ฐ๋ง**: JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ - -## ๐ŸŽฏ ํ•ต์‹ฌ ์„ค๊ณ„ - -### @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ -```java -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockWord { - /** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ - * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" - */ - String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; - - /** - * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ - * ๊ธฐ๋ณธ๊ฐ’: ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ - * ์ค‘์ฒฉ ๊ฐ์ฒด: "userInfo.userId" ํ˜•ํƒœ ์ง€์› - */ - String userIdPath() default "userId"; -} -``` - -### ์‚ฌ์šฉ ์˜ˆ์‹œ - -#### 1. ๊ธฐ๋ณธ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ -```java -@BlockWord(userIdPath = "userInfo.userId") -String reviewContent; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" -``` - -#### 2. ์ปค์Šคํ…€ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ -```java -@BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userInfo.userId") -String nickName; // โ†’ "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" -``` - -#### 3. ์ค‘์ฒฉ ๊ฐ์ฒด์—์„œ userId ์ฐธ์กฐ -```java -// ReviewInfo ๊ตฌ์กฐ -public record ReviewInfo( - @BlockWord(userIdPath = "userInfo.userId") // ๋‚ด๋ถ€ ๊ฐ์ฒด์˜ userId ์ฐธ์กฐ - String reviewContent, - - UserInfo userInfo // userId๋ฅผ ํฌํ•จํ•œ ์ค‘์ฒฉ ๊ฐ์ฒด -) {} - -public record UserInfo( - Long userId, // ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€ - - @BlockWord(userIdPath = "userId") // ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ์ฐธ์กฐ - String nickName -) {} -``` - -## ๐Ÿ”ง ๋™์ž‘ ์›๋ฆฌ - -### 1. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ํš๋“ -- **๋‹จ์ˆœ ๊ฒฝ๋กœ**: `"userId"` โ†’ ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ํ•„๋“œ -- **์ค‘์ฒฉ ๊ฒฝ๋กœ**: `"userInfo.userId"` โ†’ userInfo ๊ฐ์ฒด ๋‚ด๋ถ€์˜ userId ํ•„๋“œ -- **๋ฆฌํ”Œ๋ ‰์…˜ ํ™œ์šฉ**: ๋Ÿฐํƒ€์ž„์— ์ง€์ •๋œ ๊ฒฝ๋กœ๋กœ userId ๊ฐ’ ์ถ”์ถœ - -### 2. ์ฐจ๋‹จ ๊ด€๊ณ„ ๊ฒ€์‚ฌ ํ๋ฆ„ -``` -1. @BlockWord ํ•„๋“œ ๊ฐ์ง€ -2. userIdPath๋กœ ์ž‘์„ฑ์ž userId ์ถ”์ถœ -3. ํ˜„์žฌ ์š”์ฒญ ์‚ฌ์šฉ์ž ID ํš๋“ (SecurityContext) -4. BlockService.isBlocked(currentUserId, authorId) ํ˜ธ์ถœ -5. ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ โ†’ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ -6. ์ •์ƒ์ธ ๊ฒฝ์šฐ โ†’ ์›๋ณธ ๊ฐ’ ์œ ์ง€ -``` - -## ๐Ÿ“ ์ ์šฉ ์˜ˆ์‹œ - -### ReviewInfo ์ ์šฉ -```java -@Builder -public record ReviewInfo( - Long reviewId, - - // ๋ฆฌ๋ทฐ ๋‚ด์šฉ - userInfo ๋‚ด๋ถ€์˜ userId๋กœ ์ฐจ๋‹จ ํŒ๋‹จ - @BlockWord(userIdPath = "userInfo.userId") - String reviewContent, - - String reviewImageUrl, - LocalDateTime createAt, - - // ์‚ฌ์šฉ์ž ์ •๋ณด - ๋‚ด๋ถ€์—์„œ nickName ํ•„ํ„ฐ๋ง - UserInfo userInfo, - - Boolean isMyReview, - // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค -) {} -``` - -### UserInfo ์ ์šฉ -```java -public record UserInfo( - Long userId, // ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€ - - // ๋‹‰๋„ค์ž„ - ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId๋กœ ์ฐจ๋‹จ ํŒ๋‹จ - @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") - String nickName, - - String userProfileImage -) { - public static UserInfo of(Long userId, String nickName, String userProfileImage) { - return new UserInfo(userId, nickName, userProfileImage); - } -} -``` - -## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ - -### ์ •์ƒ ์‚ฌ์šฉ์ž ์‘๋‹ต -```json -{ - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "userInfo": { - "userId": 123, - "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", - "userProfileImage": "profile.jpg" - } -} -``` - -### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์‘๋‹ต (ํ•„ํ„ฐ๋ง ํ›„) -```json -{ - "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", - "userInfo": { - "userId": 456, // userId๋Š” ์œ ์ง€ (์‹œ์Šคํ…œ ๋™์ž‘์šฉ) - "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", - "userProfileImage": "profile.jpg" - } -} -``` - -## ๐Ÿš€ ์ฃผ์š” ํŠน์ง• - -### 1. ์œ ์—ฐํ•œ ๊ฒฝ๋กœ ์ง€์ • -- **๋‹จ์ˆœ ๊ตฌ์กฐ**: `userIdPath = "userId"` -- **์ค‘์ฒฉ ๊ตฌ์กฐ**: `userIdPath = "userInfo.userId"` -- **๊นŠ์€ ์ค‘์ฒฉ**: `userIdPath = "author.profile.userId"` (ํ•„์š”์‹œ ํ™•์žฅ) - -### 2. ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต -- **๋Œ€์ฒด ๋ฉ”์‹œ์ง€**: ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" -- **๊ฒฝ๋กœ**: ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด "userId" ํ•„๋“œ ์‚ฌ์šฉ - -### 3. ์„ฑ๋Šฅ ์ตœ์ ํ™” -- **์บ์‹œ ํ™œ์šฉ**: BlockService์˜ ๊ธฐ์กด ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ํ™œ์šฉ -- **์กฐ๊ฑด๋ถ€ ์‹คํ–‰**: ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๋Œ€์ฒด ์ฒ˜๋ฆฌ -- **์ง๋ ฌํ™” ์‹œ์ **: JSON ๋ณ€ํ™˜ํ•  ๋•Œ๋งŒ ์‹คํ–‰ - -## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ๊ณ ๋ ค์‚ฌํ•ญ - -### 1. ๊ฒฝ๋กœ ํŒŒ์‹ฑ -- dot notation ํŒŒ์‹ฑ ๋กœ์ง ๊ตฌํ˜„ -- ์ค‘์ฒฉ ๊ฐ์ฒด ์ ‘๊ทผ์„ ์œ„ํ•œ ๋ฆฌํ”Œ๋ ‰์…˜ ํ™œ์šฉ -- ๊ฒฝ๋กœ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - -### 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ -- SecurityContextUtil์„ ํ†ตํ•œ ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ -- ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ (ํ•„ํ„ฐ๋ง ์Šคํ‚ต) - -### 3. Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € -- @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์Šค์บ” -- ํ•„๋“œ๋ณ„ ์กฐ๊ฑด๋ถ€ ๊ฐ’ ๋ณ€๊ฒฝ -- ์›๋ณธ ๊ฐ์ฒด ์ˆ˜์ • ์—†์ด ์ง๋ ฌํ™” ์‹œ์—๋งŒ ๋ณ€๊ฒฝ - -## ๐Ÿ“‹ ์žฅ์  - -- **์ง๊ด€์ **: ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ๋ด๋„ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ•„ํ„ฐ๋ง๋˜๋Š”์ง€ ๋ช…ํ™• -- **์œ ์—ฐํ•จ**: ๋‹ค์–‘ํ•œ ๊ฐ์ฒด ๊ตฌ์กฐ์— ๋Œ€์‘ ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ง€์ • ๋ฐฉ์‹ -- **์ตœ์†Œ ๋ณ€๊ฒฝ**: ๊ธฐ์กด DTO ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์ด ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ -- **์„ฑ๋Šฅ**: ํ•„์š”ํ•  ๋•Œ๋งŒ ์‹คํ–‰๋˜๋Š” ํšจ์œจ์ ์ธ ๊ตฌ์กฐ \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.4.md b/src/main/java/app/bottlenote/common/block/v1.4.md deleted file mode 100644 index 9972a656b..000000000 --- a/src/main/java/app/bottlenote/common/block/v1.4.md +++ /dev/null @@ -1,154 +0,0 @@ -# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.4 - ์ตœ์ข… ๋‹จ์ˆœํ™”๋œ @BlockWord ๋ฐฉ์‹ - -## ๐Ÿ“‹ ๊ฐœ์š” - -**@BlockWord ์–ด๋…ธํ…Œ์ด์…˜**: JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ์ž๋™ ๋ณ€๊ฒฝ - -## ๐ŸŽฏ @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ - -```java -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockWord { - /** - * ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ - * ๊ธฐ๋ณธ๊ฐ’: "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" - */ - String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; - - /** - * ์ฐจ๋‹จ ํŒ๋‹จ ๊ธฐ์ค€์ด ๋˜๋Š” ์‚ฌ์šฉ์ž ID ๊ฒฝ๋กœ - * ๊ธฐ๋ณธ๊ฐ’: "userId" - * ์ค‘์ฒฉ ๊ฐ์ฒด: "userInfo.userId" ํ˜•ํƒœ ์ง€์› - */ - String userIdPath() default "userId"; -} -``` - -## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ - -### ReviewInfo -```java -@Builder -public record ReviewInfo( - Long reviewId, - - @BlockWord(userIdPath = "userInfo.userId") - String reviewContent, - - UserInfo userInfo, - // ... ๊ธฐํƒ€ ํ•„๋“œ๋“ค -) {} -``` - -### UserInfo -```java -public record UserInfo( - Long userId, - - @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") - String nickName, - - String userProfileImage -) {} -``` - -## ๐Ÿ”ง ๋™์ž‘ ํ๋ฆ„ - -### 1. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ -```java -// SecurityContextUtils ํ™œ์šฉ -Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(null); - -// ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ -if (currentUserId == null) { - // ํ•„ํ„ฐ๋ง ์Šคํ‚ต, ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ - return originalValue; -} -``` - -### 2. ์ž‘์„ฑ์ž userId ์ถ”์ถœ -```java -// userIdPath๋กœ ๋ฆฌํ”Œ๋ ‰์…˜์„ ํ†ตํ•ด userId ์ถ”์ถœ -// "userId" โ†’ ๊ฐ™์€ ๊ฐ์ฒด ๋‚ด userId ํ•„๋“œ -// "userInfo.userId" โ†’ userInfo ๊ฐ์ฒด ๋‚ด๋ถ€์˜ userId ํ•„๋“œ -Long authorId = extractUserIdByPath(object, userIdPath); -``` - -### 3. ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ -```java -// BlockService๋ฅผ ํ†ตํ•œ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์บ์‹œ ์ œ์™ธ) -boolean isBlocked = blockService.isBlocked(currentUserId, authorId); - -if (isBlocked) { - return ๋Œ€์ฒด๋ฉ”์‹œ์ง€; // "@BlockWord" ์–ด๋…ธํ…Œ์ด์…˜์˜ value ๊ฐ’ -} else { - return ์›๋ณธ๊ฐ’; -} -``` - -## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ - -### ์ •์ƒ ์‚ฌ์šฉ์ž -```json -{ - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "userInfo": { - "userId": 123, - "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" - } -} -``` - -### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž -```json -{ - "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", - "userInfo": { - "userId": 456, - "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" - } -} -``` - -### ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž -```json -{ - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "userInfo": { - "userId": 123, - "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" - } -} -``` - -## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ํ•ต์‹ฌ ์š”์†Œ - -### 1. Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € -- `@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐ์ง€ -- ํ•„๋“œ๋ณ„ ์กฐ๊ฑด๋ถ€ ๊ฐ’ ๋ณ€๊ฒฝ -- ์ง๋ ฌํ™” ์‹œ์ ์—์„œ๋งŒ ์ฒ˜๋ฆฌ - -### 2. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ์ถ”์ถœ -- dot notation ํŒŒ์‹ฑ: `"userInfo.userId"` -- ๋ฆฌํ”Œ๋ ‰์…˜์„ ํ†ตํ•œ ์ค‘์ฒฉ ๊ฐ์ฒด ์ ‘๊ทผ -- ๊ฒฝ๋กœ ์˜ค๋ฅ˜ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - -### 3. ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ์ฒ˜๋ฆฌ -- `SecurityContextUtil.getUserIdByContext()` ํ™œ์šฉ -- `Optional` ๋ฐ˜ํ™˜๊ฐ’ ์ฒ˜๋ฆฌ -- ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ํ•„ํ„ฐ๋ง ์Šคํ‚ต - -### 4. ๋‹จ์ˆœํ•œ ๋ธ”๋ก ๊ฒ€์‚ฌ -- `BlockService.isBlocked(currentUserId, authorId)` ํ˜ธ์ถœ -- ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์ œ์™ธ -- ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ์—๋งŒ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ ์ ์šฉ - -## ๐Ÿ“‹ ์ฒ˜๋ฆฌ ์กฐ๊ฑด - -| ์ƒํ™ฉ | ๋™์ž‘ | -|------|------| -| ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ (ํ•„ํ„ฐ๋ง ์•ˆ ํ•จ) | -| ๋กœ๊ทธ์ธ + ์ฐจ๋‹จ ์•ˆ ๋จ | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ | -| ๋กœ๊ทธ์ธ + ์ฐจ๋‹จ๋จ | ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ | -| userIdPath ์˜ค๋ฅ˜ | ์›๋ณธ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + ๋กœ๊ทธ ๊ธฐ๋ก | \ No newline at end of file diff --git a/src/main/java/app/bottlenote/common/block/v1.5.md b/src/main/java/app/bottlenote/common/block/v1.5.md deleted file mode 100644 index 9c9b92861..000000000 --- a/src/main/java/app/bottlenote/common/block/v1.5.md +++ /dev/null @@ -1,192 +0,0 @@ -# ๋ธ”๋ก ํ•„ํ„ฐ๋ง v1.5 - Jackson ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๊ตฌํ˜„ - -## ๐Ÿ“‹ ๊ฐœ์š” - -**์‹ค์ œ ๊ตฌํ˜„๋œ @BlockWord ๋ฐฉ์‹**: Jackson ContextualSerializer๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ตฌํ˜„ - -## ๐ŸŽฏ ๊ตฌํ˜„ ๊ตฌ์„ฑ์š”์†Œ - -### 1. @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ -```java -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockWord { - String value() default "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค"; - String userIdPath() default "userId"; -} -``` - -### 2. BlockWordSerializer -```java -@Component -public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer { - - @Override - public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) { - // 1. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ (SecurityContextUtil) - // 2. ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ํ•„ํ„ฐ๋ง ์Šคํ‚ต - // 3. userIdPath๋กœ ์ž‘์„ฑ์ž ID ์ถ”์ถœ (๋ฆฌํ”Œ๋ ‰์…˜) - // 4. BlockService.isBlocked() ํ˜ธ์ถœ - // 5. ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ - } - - @Override - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { - // @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐ์ง€ํ•˜์—ฌ ์ปจํ…์ŠคํŠธ๋ณ„ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ƒ์„ฑ - } -} -``` - -### 3. BlockWordConfig -```java -@Configuration -public class BlockWordConfig { - - @PostConstruct - public void configureObjectMapper() { - SimpleModule blockWordModule = new SimpleModule("BlockWordModule"); - blockWordModule.addSerializer(String.class, blockWordSerializer); - objectMapper.registerModule(blockWordModule); - } -} -``` - -## ๐Ÿ”ง ๋™์ž‘ ๊ณผ์ • - -### 1. ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๋“ฑ๋ก -- ObjectMapper์— BlockWordSerializer๋ฅผ String ํƒ€์ž…์— ๋Œ€ํ•ด ๋“ฑ๋ก -- @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š” ํ•„๋“œ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋„๋ก ContextualSerializer ํ™œ์šฉ - -### 2. ์ง๋ ฌํ™” ์‹œ์  ์ฒ˜๋ฆฌ -```java -// JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ž๋™ ํ˜ธ์ถœ -serialize(String value, JsonGenerator gen, SerializerProvider serializers) { - - // ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํš๋“ - Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(null); - if (currentUserId == null) { - gen.writeString(value); // ๋น„๋กœ๊ทธ์ธ โ†’ ์›๋ณธ ๋ฐ˜ํ™˜ - return; - } - - // ์ž‘์„ฑ์ž ID ์ถ”์ถœ (๋ฆฌํ”Œ๋ ‰์…˜) - Object currentObject = gen.getCurrentValue(); - Long authorId = extractUserIdByPath(currentObject, userIdPath); - - // ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ - boolean isBlocked = blockService.isBlocked(currentUserId, authorId); - if (isBlocked) { - gen.writeString(๋Œ€์ฒด๋ฉ”์‹œ์ง€); // ์ฐจ๋‹จ๋จ โ†’ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€ - } else { - gen.writeString(value); // ์ •์ƒ โ†’ ์›๋ณธ ๊ฐ’ - } -} -``` - -### 3. ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ userId ์ถ”์ถœ -```java -private Long extractUserIdByPath(Object object, String userIdPath) { - String[] pathParts = userIdPath.split("\\."); - Object currentObject = object; - - // dot notation ๊ฒฝ๋กœ ํƒ์ƒ‰ - for (String part : pathParts) { - Field field = findField(currentObject.getClass(), part); - field.setAccessible(true); - currentObject = field.get(currentObject); - } - - return (Long) currentObject; -} -``` - -## ๐Ÿ“ ์‚ฌ์šฉ ์˜ˆ์‹œ - -### ReviewInfo ์ ์šฉ -```java -@Builder -public record ReviewInfo( - Long reviewId, - - @BlockWord(userIdPath = "userInfo.userId") - String reviewContent, - - UserInfo userInfo -) {} -``` - -### UserInfo ์ ์šฉ -```java -public record UserInfo( - Long userId, - - @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") - String nickName, - - String userProfileImage -) {} -``` - -## ๐ŸŽจ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ - -### ์ •์ƒ ์‚ฌ์šฉ์ž -```json -{ - "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "userInfo": { - "userId": 123, - "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" - } -} -``` - -### ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž -```json -{ - "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", - "userInfo": { - "userId": 456, - "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" - } -} -``` - -## ๐Ÿ› ๏ธ ๊ตฌํ˜„ ํŠน์ง• - -### 1. ContextualSerializer ํ™œ์šฉ -- BeanProperty์—์„œ @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”์ถœ -- ์–ด๋…ธํ…Œ์ด์…˜๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ -- ํ•„๋“œ๋ณ„ ๊ฐœ๋ณ„ ์„ค์ • ์ ์šฉ - -### 2. ๋ฆฌํ”Œ๋ ‰์…˜ ๊ธฐ๋ฐ˜ ๊ฒฝ๋กœ ํƒ์ƒ‰ -- dot notation ์ง€์›: "userInfo.userId" -- ํด๋ž˜์Šค ์ƒ์† ๊ณ„์ธต ๊ณ ๋ คํ•œ ํ•„๋“œ ๊ฒ€์ƒ‰ -- ์‹คํŒจ ์‹œ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜์œผ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด - -### 3. ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ -- SecurityContextUtil.getUserIdByContext() ํ™œ์šฉ -- Optional ๋ฐ˜ํ™˜๊ฐ’ ์ฒ˜๋ฆฌ๋กœ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ๊ณ ๋ ค -- ์˜ˆ์™ธ ์ƒํ™ฉ์—์„œ ์›๋ณธ ๊ฐ’ ๋ณด์กด - -### 4. ์—๋Ÿฌ ํ•ธ๋“ค๋ง -- ๊ฐ ๋‹จ๊ณ„๋ณ„ try-catch๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด -- ์‹คํŒจ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก ํ›„ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ -- ์šด์˜ ์ค‘ ์žฅ์•  ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ฐฉ์–ด์  ํ”„๋กœ๊ทธ๋ž˜๋ฐ - -## ๐Ÿ“‹ ๊ตฌํ˜„ ํŒŒ์ผ ๊ตฌ์กฐ - -``` -common/block/ -โ”œโ”€โ”€ annotation/ -โ”‚ โ””โ”€โ”€ BlockWord.java # ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜ -โ”œโ”€โ”€ serializer/ -โ”‚ โ””โ”€โ”€ BlockWordSerializer.java # Jackson ์ปค์Šคํ…€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € -โ”œโ”€โ”€ config/ -โ”‚ โ””โ”€โ”€ BlockWordConfig.java # ObjectMapper ์„ค์ • -โ””โ”€โ”€ service/ - โ””โ”€โ”€ BlockService.java # ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ (๊ธฐ์กด) -``` - -## ๐Ÿ“ ๊ฒฐ๋ก  - -Jackson์˜ ContextualSerializer๋ฅผ ํ™œ์šฉํ•œ ๋ฐฉ์‹์œผ๋กœ ํ•„๋“œ ๋ ˆ๋ฒจ์—์„œ ์„ธ๋ฐ€ํ•œ ๋ธ”๋ก ํ•„ํ„ฐ๋ง์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. AOP ๋ฐฉ์‹์˜ ๋ณต์žก์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/main/java/app/bottlenote/review/controller/ReviewController.java b/src/main/java/app/bottlenote/review/controller/ReviewController.java index 4e154a604..012d9476e 100644 --- a/src/main/java/app/bottlenote/review/controller/ReviewController.java +++ b/src/main/java/app/bottlenote/review/controller/ReviewController.java @@ -1,6 +1,5 @@ package app.bottlenote.review.controller; -import app.bottlenote.common.block.annotation.BlockFilter; import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.security.SecurityContextUtil; import app.bottlenote.global.service.cursor.PageResponse; @@ -43,7 +42,6 @@ public ResponseEntity createReview(@RequestBody @Valid ReviewCreateRequest re return GlobalResponse.ok(reviewService.createReview(reviewCreateRequest, currentUserId)); } - @BlockFilter(userField = "authorId") @GetMapping("/{alcoholId}") public ResponseEntity getReviews(@PathVariable Long alcoholId, @ModelAttribute ReviewPageableRequest reviewPageableRequest) { Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); @@ -56,7 +54,6 @@ public ResponseEntity getReviews(@PathVariable Long alcoholId, @ModelAttribut return GlobalResponse.ok(pageResponse.content(), MetaService.createMetaInfo().add("pageable", pageResponse.cursorPageable())); } - @BlockFilter(userField = "authorId") @GetMapping("/detail/{reviewId}") public ResponseEntity getDetailReview(@PathVariable Long reviewId) { diff --git a/src/main/java/app/bottlenote/review/facade/payload/ReviewInfo.java b/src/main/java/app/bottlenote/review/facade/payload/ReviewInfo.java index e2c80527b..bebc8ef96 100644 --- a/src/main/java/app/bottlenote/review/facade/payload/ReviewInfo.java +++ b/src/main/java/app/bottlenote/review/facade/payload/ReviewInfo.java @@ -1,5 +1,6 @@ package app.bottlenote.review.facade.payload; +import app.bottlenote.common.block.annotation.BlockWord; import app.bottlenote.global.data.serializers.CustomDeserializers.TagListDeserializer; import app.bottlenote.global.data.serializers.CustomSerializers.TagListSerializer; import app.bottlenote.review.constant.ReviewDisplayStatus; @@ -14,37 +15,38 @@ @Builder public record ReviewInfo( - // ๊ธฐ๋ณธ ๋ฆฌ๋ทฐ ์ •๋ณด - Long reviewId, - String reviewContent, - String reviewImageUrl, - LocalDateTime createAt, - Long totalImageCount, - - // ์‚ฌ์šฉ์ž ์ •๋ณด - UserInfo userInfo, - Boolean isMyReview, - - // ๋ฆฌ๋ทฐ ์ƒํƒœ ๋ฐ ์†์„ฑ - ReviewDisplayStatus status, - Boolean isBestReview, - ReviewLocation locationInfo, - SizeType sizeType, - - // ๊ฐ€๊ฒฉ ๋ฐ ํ‰์  ์ •๋ณด - BigDecimal price, - Double rating, - - // ์ข‹์•„์š” ๋ฐ ๋Œ“๊ธ€ ์ •๋ณด - Long likeCount, - Long replyCount, - Boolean isLikedByMe, - Boolean hasReplyByMe, - - // ๊ธฐํƒ€ ์ •๋ณด - Long viewCount, - @JsonSerialize(using = TagListSerializer.class) - @JsonDeserialize(using = TagListDeserializer.class) - String tastingTagList + // ๊ธฐ๋ณธ ๋ฆฌ๋ทฐ ์ •๋ณด + Long reviewId, + @BlockWord(userIdPath = "userInfo.userId") + String reviewContent, + String reviewImageUrl, + LocalDateTime createAt, + Long totalImageCount, + + // ์‚ฌ์šฉ์ž ์ •๋ณด + UserInfo userInfo, + Boolean isMyReview, + + // ๋ฆฌ๋ทฐ ์ƒํƒœ ๋ฐ ์†์„ฑ + ReviewDisplayStatus status, + Boolean isBestReview, + ReviewLocation locationInfo, + SizeType sizeType, + + // ๊ฐ€๊ฒฉ ๋ฐ ํ‰์  ์ •๋ณด + BigDecimal price, + Double rating, + + // ์ข‹์•„์š” ๋ฐ ๋Œ“๊ธ€ ์ •๋ณด + Long likeCount, + Long replyCount, + Boolean isLikedByMe, + Boolean hasReplyByMe, + + // ๊ธฐํƒ€ ์ •๋ณด + Long viewCount, + @JsonSerialize(using = TagListSerializer.class) + @JsonDeserialize(using = TagListDeserializer.class) + String tastingTagList ) { } diff --git a/src/main/java/app/bottlenote/review/facade/payload/UserInfo.java b/src/main/java/app/bottlenote/review/facade/payload/UserInfo.java index a7d4ddf33..2cf4ef9fd 100644 --- a/src/main/java/app/bottlenote/review/facade/payload/UserInfo.java +++ b/src/main/java/app/bottlenote/review/facade/payload/UserInfo.java @@ -1,9 +1,12 @@ package app.bottlenote.review.facade.payload; +import app.bottlenote.common.block.annotation.BlockWord; + public record UserInfo( - Long userId, - String nickName, - String userProfileImage + Long userId, + @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค") + String nickName, + String userProfileImage ) { public static UserInfo of(Long userId, String nickName, String userProfileImage) { diff --git a/src/main/java/app/bottlenote/support/block/controller/BlockController.java b/src/main/java/app/bottlenote/support/block/controller/BlockController.java new file mode 100644 index 000000000..39ab96cd8 --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/controller/BlockController.java @@ -0,0 +1,73 @@ +package app.bottlenote.support.block.controller; + +import app.bottlenote.global.data.response.GlobalResponse; +import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.support.block.exception.BlockException; +import app.bottlenote.support.block.service.BlockService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static app.bottlenote.support.block.exception.BlockExceptionCode.REQUIRED_USER_ID; + +/** + * ์ฐจ๋‹จ ๊ด€๋ฆฌ API + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/blocks") +public class BlockController { + + private final BlockService blockService; + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ + */ + @PostMapping("/create") + public ResponseEntity createBlock(@RequestBody BlockCreateRequest request) { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + blockService.blockUser(currentUserId, request.blockedUserId()); + + return GlobalResponse.ok("์ฐจ๋‹จ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + /** + * ์ฐจ๋‹จ ํ•ด์ œ + */ + @DeleteMapping("/{blockedUserId}") + public ResponseEntity unblockUser(@PathVariable Long blockedUserId) { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + blockService.unblockUser(currentUserId, blockedUserId); + + return GlobalResponse.ok("์ฐจ๋‹จ์ด ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + /** + * ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ + */ + @GetMapping + public ResponseEntity getBlockedUsers() { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + return GlobalResponse.ok(blockService.getBlockedUserIds(currentUserId)); + } + + /** + * ์ฐจ๋‹จ ์š”์ฒญ DTO + */ + public record BlockCreateRequest(Long blockedUserId) { + } +} diff --git a/src/main/java/app/bottlenote/user/domain/UserBlock.java b/src/main/java/app/bottlenote/support/block/domain/UserBlock.java similarity index 87% rename from src/main/java/app/bottlenote/user/domain/UserBlock.java rename to src/main/java/app/bottlenote/support/block/domain/UserBlock.java index e16958ea8..e57544dba 100644 --- a/src/main/java/app/bottlenote/user/domain/UserBlock.java +++ b/src/main/java/app/bottlenote/support/block/domain/UserBlock.java @@ -1,4 +1,4 @@ -package app.bottlenote.user.domain; +package app.bottlenote.support.block.domain; import app.bottlenote.common.domain.BaseTimeEntity; import jakarta.persistence.Column; @@ -6,7 +6,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Index; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; @@ -28,10 +27,6 @@ name = "uk_blocker_blocked", columnNames = {"blocker_id", "blocked_id"} ) - }, - indexes = { - @Index(name = "idx_user_block_blocker", columnList = "blocker_id"), - @Index(name = "idx_user_block_blocked", columnList = "blocked_id") } ) public class UserBlock extends BaseTimeEntity { diff --git a/src/main/java/app/bottlenote/support/block/exception/BlockException.java b/src/main/java/app/bottlenote/support/block/exception/BlockException.java new file mode 100644 index 000000000..b2c3c1683 --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/exception/BlockException.java @@ -0,0 +1,10 @@ +package app.bottlenote.support.block.exception; + +import app.bottlenote.global.exception.custom.AbstractCustomException; + +public class BlockException extends AbstractCustomException { + + public BlockException(BlockExceptionCode blockExceptionCode) { + super(blockExceptionCode); + } +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/support/block/exception/BlockExceptionCode.java b/src/main/java/app/bottlenote/support/block/exception/BlockExceptionCode.java new file mode 100644 index 000000000..d5b500fb5 --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/exception/BlockExceptionCode.java @@ -0,0 +1,29 @@ +package app.bottlenote.support.block.exception; + +import app.bottlenote.global.exception.custom.code.ExceptionCode; +import org.springframework.http.HttpStatus; + +public enum BlockExceptionCode implements ExceptionCode { + USER_ALREADY_BLOCKED(HttpStatus.CONFLICT, "์ด๋ฏธ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."), + USER_BLOCK_NOT_FOUND(HttpStatus.NOT_FOUND, "์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + CANNOT_BLOCK_SELF(HttpStatus.BAD_REQUEST, "์ž๊ธฐ ์ž์‹ ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + REQUIRED_USER_ID(HttpStatus.BAD_REQUEST, "์œ ์ € ์•„์ด๋””๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final String message; + + BlockExceptionCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } +} \ No newline at end of file diff --git a/src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java similarity index 89% rename from src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java rename to src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java index 9325db9bb..fe7a4751c 100644 --- a/src/main/java/app/bottlenote/user/repository/JpaUserBlockRepository.java +++ b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java @@ -1,7 +1,6 @@ -package app.bottlenote.user.repository; +package app.bottlenote.support.block.repository; -import app.bottlenote.user.domain.UserBlock; -import app.bottlenote.user.domain.UserBlockRepository; +import app.bottlenote.support.block.domain.UserBlock; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/src/main/java/app/bottlenote/user/domain/UserBlockRepository.java b/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java similarity index 94% rename from src/main/java/app/bottlenote/user/domain/UserBlockRepository.java rename to src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java index 1fb9ee847..434bc4a80 100644 --- a/src/main/java/app/bottlenote/user/domain/UserBlockRepository.java +++ b/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java @@ -1,4 +1,6 @@ -package app.bottlenote.user.domain; +package app.bottlenote.support.block.repository; + +import app.bottlenote.support.block.domain.UserBlock; import java.util.Optional; import java.util.Set; diff --git a/src/main/java/app/bottlenote/support/block/service/BlockService.java b/src/main/java/app/bottlenote/support/block/service/BlockService.java new file mode 100644 index 000000000..a7a16178b --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/service/BlockService.java @@ -0,0 +1,174 @@ +package app.bottlenote.support.block.service; + +import app.bottlenote.support.block.domain.UserBlock; +import app.bottlenote.support.block.exception.BlockException; +import app.bottlenote.support.block.repository.UserBlockRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +import static app.bottlenote.support.block.exception.BlockExceptionCode.CANNOT_BLOCK_SELF; +import static app.bottlenote.support.block.exception.BlockExceptionCode.REQUIRED_USER_ID; +import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_ALREADY_BLOCKED; +import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_BLOCK_NOT_FOUND; + +/** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๋ฆฌ ์„œ๋น„์Šค + * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ, ํ•ด์ œ, ์กฐํšŒ ๋ฐ ์บ์‹ฑ ์ฒ˜๋ฆฌ + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BlockService { + + private final UserBlockRepository userBlockRepository; + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ + * + * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public void blockUser(Long blockerId, Long blockedId, String reason) { + validateBlockRequest(blockerId, blockedId); + + // ์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ธ์ง€ ํ™•์ธ + if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { + throw new BlockException(USER_ALREADY_BLOCKED); + } + + // ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ + UserBlock userBlock = UserBlock.create(blockerId, blockedId, reason); + userBlockRepository.save(userBlock); + + log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‚ฌ์œ  ์—†์Œ) + */ + @Transactional + public void blockUser(Long blockerId, Long blockedId) { + blockUser(blockerId, blockedId, null); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ + * + * @param blockerId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ ํ•ด์ œ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public void unblockUser(Long blockerId, Long blockedId) { + validateBlockRequest(blockerId, blockedId); + + UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) + .orElseThrow(() -> new BlockException(USER_BLOCK_NOT_FOUND)); + + userBlockRepository.delete(userBlock); + + log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ (์บ์‹œ ์ ์šฉ) + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + */ + @Cacheable(value = "blocked_users", key = "#userId") + public Set getBlockedUserIds(Long userId) { + if (userId == null) { + return Set.of(); + } + + return userBlockRepository.findBlockedUserIdsByBlockerId(userId); + } + + /** + * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ + * + * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + public boolean isBlocked(Long blockerId, Long blockedId) { + if (blockerId == null || blockedId == null) { + return false; + } + + Set blockedUsers = getBlockedUserIds(blockerId); + return blockedUsers.contains(blockedId); + } + + /** + * ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ + * + * @param userId1 ์‚ฌ์šฉ์ž 1 ID + * @param userId2 ์‚ฌ์šฉ์ž 2 ID + * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + public boolean isMutualBlocked(Long userId1, Long userId2) { + if (userId1 == null || userId2 == null) { + return false; + } + + return userBlockRepository.existsMutualBlock(userId1, userId2); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + public long getBlockedByCount(Long userId) { + if (userId == null) { + return 0L; + } + + return userBlockRepository.countByBlockedId(userId); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + public long getBlockingCount(Long userId) { + if (userId == null) { + return 0L; + } + + return userBlockRepository.countByBlockerId(userId); + } + + /** + * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + private void validateBlockRequest(Long blockerId, Long blockedId) { + if (blockerId == null) { + throw new BlockException(REQUIRED_USER_ID); + } + + if (blockedId == null) { + throw new BlockException(REQUIRED_USER_ID); + } + + if (blockerId.equals(blockedId)) { + throw new BlockException(CANNOT_BLOCK_SELF); + } + } + +} diff --git a/src/main/resources/application-logging.yml b/src/main/resources/application-logging.yml index 2c4e1dc30..3d844c830 100644 --- a/src/main/resources/application-logging.yml +++ b/src/main/resources/application-logging.yml @@ -12,6 +12,9 @@ logging: level: org.hibernate.SQL: off # SQL ์ฟผ๋ฆฌ ๋กœ๊น… ๋น„ํ™œ์„ฑํ™” org.hibernate.orm.jdbc.bind: off # ๋ฐ”์ธ๋”ฉ ํŒŒ๋ผ๋ฏธํ„ฐ ๋กœ๊น… ๋น„ํ™œ์„ฑํ™” + app.bottlenote.common.block.serializer: debug # BlockWordSerializer ๋””๋ฒ„๊ทธ + app.bottlenote.common.block.service: debug # BlockService ๋””๋ฒ„๊ทธ + app.bottlenote.common.block.config: debug # BlockWordConfig ๋””๋ฒ„๊ทธ root: info --- # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ From e86d8afd8104754feab068241f52d39e399ce3a8 Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 13:01:22 +0900 Subject: [PATCH 04/16] docs: remove md doc Signed-off-by: hgkim --- BLOCK_FEATURE_USAGE.md | 241 ------------ USER_BLOCK_IMPLEMENTATION.md | 687 ----------------------------------- 2 files changed, 928 deletions(-) delete mode 100644 BLOCK_FEATURE_USAGE.md delete mode 100644 USER_BLOCK_IMPLEMENTATION.md diff --git a/BLOCK_FEATURE_USAGE.md b/BLOCK_FEATURE_USAGE.md deleted file mode 100644 index 0f1d9e175..000000000 --- a/BLOCK_FEATURE_USAGE.md +++ /dev/null @@ -1,241 +0,0 @@ -# ๐Ÿšซ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ - -## ๐ŸŽฏ ๊ตฌํ˜„ ์™„๋ฃŒ๋œ ๊ธฐ๋Šฅ๋“ค - -### โœ… 1. BlockFilter ์–ด๋…ธํ…Œ์ด์…˜ -- **์œ„์น˜**: `app.bottlenote.global.annotation.BlockFilter` -- **์šฉ๋„**: ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์ฐจ๋‹จ ํ•„ํ„ฐ๋ง ์ ์šฉ - -### โœ… 2. BlockFilterAspect AOP -- **์œ„์น˜**: `app.bottlenote.global.aspect.BlockFilterAspect` -- **์šฉ๋„**: ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ์ž๋™ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ - -### โœ… 3. BlockService -- **์œ„์น˜**: `app.bottlenote.global.service.BlockService` -- **์šฉ๋„**: ์ฐจ๋‹จ ๊ด€๊ณ„ ๊ด€๋ฆฌ (ํ˜„์žฌ ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜) - -### โœ… 4. ReviewController ์ ์šฉ ์ƒ˜ํ”Œ -- **์ ์šฉ๋œ ๋ฉ”์„œ๋“œ**: `getReviews()`, `getDetailReview()` - -### โœ… 5. BlockTestController -- **์œ„์น˜**: `app.bottlenote.global.controller.BlockTestController` -- **์šฉ๋„**: ์ฐจ๋‹จ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ - ---- - -## ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• - -### 1๏ธโƒฃ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• -```java -// ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ๋ฅผ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -@BlockFilter(userField = "authorId", type = BlockType.BLUR) -@GetMapping("/reviews") -public ResponseEntity getReviews() { - // ๊ธฐ์กด ๋กœ์ง ๊ทธ๋Œ€๋กœ ์œ ์ง€ -} -``` - -### 2๏ธโƒฃ ์ฐจ๋‹จ ํƒ€์ž… ์„ ํƒ -```java -// ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -@BlockFilter(type = BlockType.BLUR) - -// ์™„์ „ ์ œ์™ธ -@BlockFilter(type = BlockType.EXCLUDE) - -// ์ ‘๊ทผ ์ฐจ๋‹จ (403 ์—๋Ÿฌ) -@BlockFilter(type = BlockType.ACCESS_DENY) -``` - -### 3๏ธโƒฃ ์‚ฌ์šฉ์ž ID ํ•„๋“œ ์ง€์ • -```java -// ๊ธฐ๋ณธ๊ฐ’: authorId -@BlockFilter(userField = "authorId") - -// ๋‹ค๋ฅธ ํ•„๋“œ๋ช… ์‚ฌ์šฉ -@BlockFilter(userField = "userId") -@BlockFilter(userField = "writerId") -``` - ---- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ• - -### 1. ์„œ๋ฒ„ ์‹คํ–‰ -```bash -./gradlew bootRun -``` - -### 2. ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ -```bash -# ์‚ฌ์šฉ์ž 1์ด ์‚ฌ์šฉ์ž 2๋ฅผ ์ฐจ๋‹จ -curl -X POST "http://localhost:8080/api/v1/test/block/add?blockerId=1&blockedId=2" -``` - -### 3. ์ฐจ๋‹จ ๋ชฉ๋ก ํ™•์ธ -```bash -# ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ -curl "http://localhost:8080/api/v1/test/block/list/1" -``` - -### 4. ๋ฆฌ๋ทฐ ์กฐํšŒ ํ…Œ์ŠคํŠธ -```bash -# JWT ํ† ํฐ์œผ๋กœ ๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์—์„œ ๋ฆฌ๋ทฐ ์กฐํšŒ -curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - "http://localhost:8080/api/v1/reviews/1" -``` - -### 5. ์ฐจ๋‹จ ๊ด€๊ณ„ ํ•ด์ œ -```bash -curl -X DELETE "http://localhost:8080/api/v1/test/block/remove?blockerId=1&blockedId=2" -``` - ---- - -## ๐Ÿ“‹ ๋™์ž‘ ํ™•์ธ ํฌ์ธํŠธ - -### โœ… ๋กœ๊ทธ ํ™•์ธ -``` -2024-01-15 10:30:00 INFO - ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ: 1 -> 2 -2024-01-15 10:31:00 DEBUG - ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ: 1 ๋ช… -2024-01-15 10:32:00 INFO - ์‚ฌ์šฉ์ž 1์˜ ์ฐจ๋‹จ ํ•„ํ„ฐ ์ ์šฉ ์™„๋ฃŒ - ์ฐจ๋‹จ ๋ชฉ๋ก: 1 ๊ฐœ -``` - -### โœ… ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋ณ€ํ™” -**์ฐจ๋‹จ ์ „:** -```json -{ - "success": true, - "data": [ - { - "reviewId": 123, - "content": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "authorId": 2, - "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" - } - ] -} -``` - -**์ฐจ๋‹จ ํ›„:** -```json -{ - "success": true, - "data": [ - { - "reviewId": 123, - "content": "*** ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค ***", - "authorId": 2, - "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", - "isBlocked": true, - "blockReason": "BLOCKED_USER" - } - ] -} -``` - ---- - -## ๐Ÿ”ง ๋‹ค๋ฅธ ์ปจํŠธ๋กค๋Ÿฌ์— ์ ์šฉํ•˜๊ธฐ - -### 1. Import ์ถ”๊ฐ€ -```java -import app.bottlenote.global.annotation.BlockFilter; -import app.bottlenote.global.annotation.BlockFilter.BlockType; -``` - -### 2. ์–ด๋…ธํ…Œ์ด์…˜ ์ ์šฉ -```java -// ๋Œ“๊ธ€ ๋ชฉ๋ก ์กฐํšŒ -@BlockFilter(userField = "authorId", type = BlockType.BLUR) -@GetMapping("/comments") -public ResponseEntity getComments() { ... } - -// ํŒ”๋กœ์›Œ ๋ชฉ๋ก ์กฐํšŒ (์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ) -@BlockFilter(userField = "userId", type = BlockType.EXCLUDE) -@GetMapping("/followers") -public ResponseEntity getFollowers() { ... } - -// ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ (์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ ‘๊ทผ ๊ธˆ์ง€) -@BlockFilter(userField = "userId", type = BlockType.ACCESS_DENY) -@GetMapping("/mypage/{userId}") -public ResponseEntity getMyPage(@PathVariable Long userId) { ... } -``` - ---- - -## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - -### 1. ํ˜„์žฌ ์ œํ•œ์‚ฌํ•ญ -- **๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ €์žฅ**: ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ์ฐจ๋‹จ ๊ด€๊ณ„ ์ดˆ๊ธฐํ™” -- **๋‹จ์ˆœ ๊ฐ์ฒด ๋ณต์‚ฌ**: ๋ณต์žกํ•œ ๊ฐ์ฒด ๊ตฌ์กฐ๋Š” ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ ์ œํ•œ -- **ํ•„๋“œ๋ช… ์˜์กด**: ์‘๋‹ต DTO์˜ ํ•„๋“œ๋ช…์ด ์ •ํ™•ํ•ด์•ผ ํ•จ - -### 2. ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ -- **์ต๋ช… ์‚ฌ์šฉ์ž**: ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ ์•ˆํ•จ (์„ฑ๋Šฅ ์ตœ์ ํ™”) -- **์บ์‹œ ์ ์šฉ**: `@Cacheable`๋กœ ๋ฐ˜๋ณต ์กฐํšŒ ์ตœ์ ํ™” -- **๋กœ๊ทธ ๋ ˆ๋ฒจ**: ์šด์˜ ์‹œ DEBUG ๋กœ๊ทธ ๋น„ํ™œ์„ฑํ™” ๊ถŒ์žฅ - -### 3. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ -- **ํด๋ผ์ด์–ธํŠธ ๊ฒ€์ฆ**: ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ถ”๊ฐ€ ๊ฒ€์ฆ ํ•„์š” -- **API ์ ‘๊ทผ ์ œ์–ด**: ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ง์ ‘ API ํ˜ธ์ถœ ์ฐจ๋‹จ ๊ณ ๋ ค - ---- - -## ๐Ÿ”„ ๋‹ค์Œ ๋‹จ๊ณ„ (์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ) - -### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™ -```sql -CREATE TABLE user_block ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - blocker_id BIGINT NOT NULL, - blocked_id BIGINT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uk_blocker_blocked (blocker_id, blocked_id) -); -``` - -### 2. Redis ์บ์‹œ ์ ์šฉ -```yaml -spring: - cache: - type: redis - redis: - host: localhost - port: 6379 -``` - -### 3. ์ฐจ๋‹จ ๊ด€๋ฆฌ API ๊ฐœ๋ฐœ -- ์ฐจ๋‹จํ•˜๊ธฐ/ํ•ด์ œํ•˜๊ธฐ API -- ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ API -- ์ฐจ๋‹จ ์‚ฌ์œ  ๊ด€๋ฆฌ - -### 4. ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ -- ๋ธ”๋Ÿฌ UI ์ปดํฌ๋„ŒํŠธ -- ์ฐจ๋‹จ ๋ฒ„ํŠผ ์ถ”๊ฐ€ -- ์ฐจ๋‹จ ๋ชฉ๋ก ๊ด€๋ฆฌ ํŽ˜์ด์ง€ - ---- - -## ๐Ÿ†˜ ๋ฌธ์ œ ํ•ด๊ฒฐ - -### Q: AOP๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์Œ -**A:** `@EnableAspectJAutoProxy` ํ™•์ธ ๋˜๋Š” Spring Boot์˜ ์ž๋™ ์„ค์ • ํ™•์ธ - -### Q: ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค๋Š” ์˜ค๋ฅ˜ -**A:** `userField` ๊ฐ’์ด ์‘๋‹ต DTO์˜ ์‹ค์ œ ํ•„๋“œ๋ช…๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ - -### Q: ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š์Œ -**A:** ๊ฐ์ฒด์˜ setter๊ฐ€ ์žˆ๋Š”์ง€, ๋˜๋Š” ํ•„๋“œ๊ฐ€ final์ด ์•„๋‹Œ์ง€ ํ™•์ธ - -### Q: ์„ฑ๋Šฅ์ด ๋А๋ฆผ -**A:** ์ฐจ๋‹จ ๋ชฉ๋ก์ด ๋งŽ์€ ๊ฒฝ์šฐ ๋ฐฐ์น˜ ์กฐํšŒ๋‚˜ ์บ์‹œ TTL ์กฐ์ • ๊ณ ๋ ค - ---- - -## ๐Ÿ“ž ์—ฐ๋ฝ์ฒ˜ - -๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋ฌธ์˜์‚ฌํ•ญ์ด๋‚˜ ๊ฐœ์„  ์ œ์•ˆ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์—ฐ๋ฝ์ฃผ์„ธ์š”! - -- ๊ธฐ์ˆ  ๋ฌธ์˜: ๊ฐœ๋ฐœํŒ€ ์ฑ„๋„ -- ๊ธฐ๋Šฅ ์ œ์•ˆ: ๊ธฐํšํŒ€ ์ฑ„๋„ \ No newline at end of file diff --git a/USER_BLOCK_IMPLEMENTATION.md b/USER_BLOCK_IMPLEMENTATION.md deleted file mode 100644 index 6703b3756..000000000 --- a/USER_BLOCK_IMPLEMENTATION.md +++ /dev/null @@ -1,687 +0,0 @@ -# ๐Ÿšซ Bottle Note ์œ ์ € ์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ - -## ๐Ÿ“‹ ๋ชฉ์ฐจ -1. [ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„](#ํ˜„์žฌ-์‹œ์Šคํ…œ-๋ถ„์„) -2. [์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ์•ˆ ๋น„๊ต](#์ฐจ๋‹จ-๊ธฐ๋Šฅ-๊ตฌํ˜„-๋ฐฉ์•ˆ-๋น„๊ต) -3. [์ถ”์ฒœ ๊ตฌํ˜„ ๋ฐฉ์•ˆ: AOP ๊ธฐ๋ฐ˜](#์ถ”์ฒœ-๊ตฌํ˜„-๋ฐฉ์•ˆ-aop-๊ธฐ๋ฐ˜) -4. [์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต](#์„ฑ๋Šฅ-์ตœ์ ํ™”-์ „๋žต) -5. [ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ ๋ฐฉ์•ˆ](#ํ”„๋ก ํŠธ์—”๋“œ-์—ฐ๋™-๋ฐฉ์•ˆ) -6. [๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ](#๊ตฌํ˜„-๋‹จ๊ณ„๋ณ„-๊ฐ€์ด๋“œ) - ---- - -## ๐Ÿ” ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„ - -### ์ฐจ๋‹จ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์กฐํšŒ ์ง€์ ๋“ค -- **๋ฆฌ๋ทฐ ๋ชฉ๋ก/์ƒ์„ธ ์กฐํšŒ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -- **๋ฆฌ๋ทฐ ๋Œ“๊ธ€ ์กฐํšŒ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋Œ“๊ธ€ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -- **ํŒ”๋กœ์›Œ/ํŒ”๋กœ์ž‰ ๋ชฉ๋ก** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ ๋˜๋Š” ๋ธ”๋Ÿฌ ํ‘œ์‹œ -- **๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ ์ œํ•œ -- **์œ ์ € ๊ฒ€์ƒ‰/์ถ”์ฒœ** - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ œ์™ธ - -### ํ˜„์žฌ ์ธ์ฆ ์‹œ์Šคํ…œ -```java -// ์ต๋ช… ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ -Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); - -// ๋กœ๊ทธ์ธ ํ•„์ˆ˜ ์ฒ˜๋ฆฌ -Long currentUserId = SecurityContextUtil.getUserIdByContext() - .orElseThrow(() -> new UserException(REQUIRED_USER_ID)); -``` - ---- - -## โš–๏ธ ์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐฉ์•ˆ ๋น„๊ต - -### 1๏ธโƒฃ ๋ฐฉ์•ˆ 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ˆ˜์ • - -#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ -```sql --- ๊ธฐ์กด ์ฟผ๋ฆฌ -SELECT * FROM review WHERE alcohol_id = ?; - --- ์ˆ˜์ •๋œ ์ฟผ๋ฆฌ -SELECT r.*, - CASE WHEN ub.blocker_id IS NOT NULL THEN 1 ELSE 0 END as is_blocked -FROM review r -LEFT JOIN user_block ub ON ub.blocker_id = ? AND ub.blocked_id = r.user_id -WHERE r.alcohol_id = ?; -``` - -#### โœ… ์žฅ์  -- **๊ฐ€์žฅ ์ง๊ด€์ ์ธ ๋ฐฉ๋ฒ•** -- **DB ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ**๋กœ ํ™•์‹คํ•œ ์ฐจ๋‹จ -- **์„ฑ๋Šฅ์ด ์ข‹์Œ** (ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ํ•ด๊ฒฐ) - -#### โŒ ๋‹จ์  -- **๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•จ** (50+ ๊ฐœ์˜ ์ฟผ๋ฆฌ) -- **๊ธฐ์กด ์ฝ”๋“œ ๋Œ€๋Ÿ‰ ์ˆ˜์ •** ํ•„์š” -- **์œ ์ง€๋ณด์ˆ˜ ๋ณต์žก๋„ ์ฆ๊ฐ€** -- **ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋Œ€๋Ÿ‰ ์ˆ˜์ •** ํ•„์š” - -### 2๏ธโƒฃ ๋ฐฉ์•ˆ 2: ์„œ๋น„์Šค ๋ ˆ์ด์–ด ํ›„์ฒ˜๋ฆฌ - -#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ -```java -@Service -public class ReviewService { - - public PageResponse getReviews(Long alcoholId, ...) { - // ๊ธฐ์กด ๋กœ์ง์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ - var reviews = reviewRepository.findByAlcoholId(alcoholId); - - // ์ฐจ๋‹จ ๋กœ์ง ํ›„์ฒ˜๋ฆฌ - var filteredReviews = blockFilterService.filterBlockedContent( - reviews, currentUserId, ReviewListResponse::getAuthorId - ); - - return PageResponse.of(filteredReviews); - } -} -``` - -#### โœ… ์žฅ์  -- **๊ธฐ์กด Repository ์ฝ”๋“œ ์œ ์ง€** -- **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฐจ๋‹จ ๋กœ์ง ๋ถ„๋ฆฌ** -- **ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›€** - -#### โŒ ๋‹จ์  -- **๋ชจ๋“  ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ˆ˜์ •** ํ•„์š” -- **์ค‘๋ณต ์ฝ”๋“œ ๋ฐœ์ƒ** ๊ฐ€๋Šฅ์„ฑ -- **๊นœ๋นกํ•˜๊ธฐ ์‰ฌ์›€** (์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์‹œ) - -### 3๏ธโƒฃ ๋ฐฉ์•ˆ 3: AOP ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ โญ **์ถ”์ฒœ** - -#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ -```java -// ์ปจํŠธ๋กค๋Ÿฌ์— ์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€ -@BlockFilter(userField = "authorId", type = BlockType.BLUR) -@GetMapping("/{alcoholId}") -public ResponseEntity getReviews(@PathVariable Long alcoholId, ...) { - // ๊ธฐ์กด ๋กœ์ง ๊ทธ๋Œ€๋กœ ์œ ์ง€ (์ˆ˜์ • ์—†์Œ!) - return GlobalResponse.ok(reviewService.getReviews(alcoholId, request, currentUserId)); -} -``` - -#### โœ… ์žฅ์  -- **๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์ตœ์†Œํ™”** (์–ด๋…ธํ…Œ์ด์…˜๋งŒ ์ถ”๊ฐ€) -- **์ค‘์•™ ์ง‘์ค‘์‹ ๊ด€๋ฆฌ** (์ฐจ๋‹จ ๋กœ์ง์ด ํ•œ ๊ณณ์—) -- **๊นœ๋นกํ•  ์—ผ๋ ค ์—†์Œ** (์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋ช…์‹œ์ ) -- **์„ฑ๋Šฅ ์˜ํ–ฅ ์ตœ์†Œ** (์‘๋‹ต ํ›„์ฒ˜๋ฆฌ) -- **ํ…Œ์ŠคํŠธ ์šฉ์ด** (AOP ๋กœ์ง ๋…๋ฆฝ ํ…Œ์ŠคํŠธ) -- **ํ™•์žฅ์„ฑ ์ข‹์Œ** (๋‹ค์–‘ํ•œ ์ฐจ๋‹จ ํƒ€์ž… ์ง€์›) - -#### โŒ ๋‹จ์  -- **AOP ๊ฐœ๋… ์ดํ•ด** ํ•„์š” -- **๋””๋ฒ„๊น…์ด ์•ฝ๊ฐ„ ๋ณต์žก**ํ•  ์ˆ˜ ์žˆ์Œ - -### 4๏ธโƒฃ ๋ฐฉ์•ˆ 4: ํ”„๋ก ํŠธ์—”๋“œ ์ฒ˜๋ฆฌ - -#### ๐ŸŽฏ ๊ตฌํ˜„ ๋ฐฉ์‹ -```javascript -// ๋ฐฑ์—”๋“œ์—์„œ ์ฐจ๋‹จ ์ •๋ณด๋งŒ ์ œ๊ณต -const reviews = await api.getReviews(alcoholId); -const blockedUsers = await api.getBlockedUsers(); - -// ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -const filteredReviews = reviews.map(review => - blockedUsers.includes(review.authorId) - ? { ...review, isBlocked: true } - : review -); -``` - -#### โœ… ์žฅ์  -- **๋ฐฑ์—”๋“œ ์ฝ”๋“œ ์ˆ˜์ • ์—†์Œ** -- **์‹ค์‹œ๊ฐ„ ์ฐจ๋‹จ/ํ•ด์ œ** ๊ฐ€๋Šฅ - -#### โŒ ๋‹จ์  -- **๋ณด์•ˆ ์ทจ์•ฝ** (ํด๋ผ์ด์–ธํŠธ์—์„œ ์šฐํšŒ ๊ฐ€๋Šฅ) -- **๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ฆ๊ฐ€** -- **์ฐจ๋‹จ๋œ ๋ฐ์ดํ„ฐ๋„ ์ „์†ก**๋จ - ---- - -## ๐Ÿ† ์ถ”์ฒœ ๊ตฌํ˜„ ๋ฐฉ์•ˆ: AOP ๊ธฐ๋ฐ˜ - -### ๐Ÿ“ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Controller โ”‚ โ”‚ AOP Aspect โ”‚ โ”‚ Response โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ @BlockFilter โ”œโ”€โ”€โ”€โ”€โ”ค BlockFilterAsp- โ”œโ”€โ”€โ”€โ”€โ”ค ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ โ”‚ -โ”‚ @GetMapping โ”‚ โ”‚ ect โ”‚ โ”‚ ์‘๋‹ต ๋ฐ์ดํ„ฐ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ โ”‚ - โ”‚ โ”‚ โ”‚ - โ–ผ โ–ผ โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Service โ”‚ โ”‚ BlockCacheManagerโ”‚ โ”‚ Frontend โ”‚ -โ”‚ (๊ธฐ์กด ๋กœ์ง) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ ์ˆ˜์ • ์—†์Œ โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ ๋ธ”๋Ÿฌ UI ์ฒ˜๋ฆฌ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### ๐Ÿ”ง ํ•ต์‹ฌ ๊ตฌํ˜„ ์ฝ”๋“œ - -#### 1. ์ฐจ๋‹จ ํ•„ํ„ฐ ์–ด๋…ธํ…Œ์ด์…˜ -```java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockFilter { - BlockType type() default BlockType.BLUR; - String userField() default "authorId"; // ์ฐจ๋‹จ ๋Œ€์ƒ ์‚ฌ์šฉ์ž ID ํ•„๋“œ๋ช… -} - -public enum BlockType { - BLUR, // ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ (๋‚ด์šฉ ๊ฐ€๋ฆผ) - EXCLUDE, // ์™„์ „ ์ œ์™ธ (๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ) - ACCESS_DENY // ์ ‘๊ทผ ์ฐจ๋‹จ (403 ์—๋Ÿฌ) -} -``` - -#### 2. AOP ์–ด์ŠคํŽ™ํŠธ -```java -@Component -@Aspect -public class BlockFilterAspect { - - private final BlockService blockService; - - @Around("@annotation(blockFilter)") - public Object applyBlockFilter(ProceedingJoinPoint joinPoint, BlockFilter blockFilter) { - // 1. ๊ธฐ์กด ๋กœ์ง ์‹คํ–‰ (์ˆ˜์ • ์—†์Œ) - Object result = joinPoint.proceed(); - - // 2. ํ˜„์žฌ ์‚ฌ์šฉ์ž ID ํ™•์ธ - Long currentUserId = SecurityContextUtil.getUserIdByContext().orElse(-1L); - if (currentUserId == -1L) { - return result; // ์ต๋ช… ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ ์•ˆํ•จ - } - - // 3. ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ์ฐจ๋‹จ ๋กœ์ง ์ ์šฉ - return applyBlockLogic(result, currentUserId, blockFilter); - } - - private Object applyBlockLogic(Object result, Long currentUserId, BlockFilter filter) { - // ResponseEntity์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ - if (result instanceof ResponseEntity) { - ResponseEntity response = (ResponseEntity) result; - GlobalResponse globalResponse = (GlobalResponse) response.getBody(); - Object data = globalResponse.getData(); - - // ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - if (data instanceof List) { - List list = (List) data; - List filteredList = processListData(list, currentUserId, filter); - return ResponseEntity.ok(GlobalResponse.success(filteredList)); - } - - // ๋‹จ์ผ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - Object filteredData = processSingleData(data, currentUserId, filter); - return ResponseEntity.ok(GlobalResponse.success(filteredData)); - } - - return result; - } -} -``` - -#### 3. ์ฐจ๋‹จ ์„œ๋น„์Šค -```java -@Service -public class BlockService { - - private final BlockRepository blockRepository; - private final RedisTemplate> redisTemplate; - - @Cacheable(value = "blocked_users", key = "#userId") - public Set getBlockedUserIds(Long userId) { - return blockRepository.findBlockedUserIdsByBlockerId(userId); - } - - public T applyBlockFilter(T item, Long currentUserId, String userField, BlockType type) { - Set blockedUsers = getBlockedUserIds(currentUserId); - Long authorId = extractUserIdFromObject(item, userField); - - if (blockedUsers.contains(authorId)) { - switch (type) { - case BLUR: - return blurContent(item); - case EXCLUDE: - return null; // ๋ฆฌ์ŠคํŠธ์—์„œ ์ œ๊ฑฐ๋จ - case ACCESS_DENY: - throw new BlockedException("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ์ž…๋‹ˆ๋‹ค."); - } - } - - return item; - } -} -``` - -### ๐ŸŽจ ์‚ฌ์šฉ ์˜ˆ์‹œ - -```java -// ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ฆฌ๋ทฐ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ -@BlockFilter(userField = "authorId", type = BlockType.BLUR) -@GetMapping("/{alcoholId}") -public ResponseEntity getReviews(@PathVariable Long alcoholId, ...) { - // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ -} - -// ํŒ”๋กœ์›Œ ๋ชฉ๋ก ์กฐํšŒ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์™„์ „ ์ œ์™ธ -@BlockFilter(userField = "userId", type = BlockType.EXCLUDE) -@GetMapping("/{targetUserId}/follower-list") -public ResponseEntity findFollowerList(@PathVariable Long targetUserId, ...) { - // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ -} - -// ๋งˆ์ดํŽ˜์ด์ง€ ์ ‘๊ทผ - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ ‘๊ทผ ์ฐจ๋‹จ -@BlockFilter(userField = "userId", type = BlockType.ACCESS_DENY) -@GetMapping("/{userId}") -public ResponseEntity getMyPage(@PathVariable Long userId) { - // ๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ์œ ์ง€ -} -``` - ---- - -## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต - -### 1๏ธโƒฃ Redis ์บ์‹ฑ ์ „๋žต - -```java -@Component -public class BlockCacheManager { - - // ์‚ฌ์šฉ์ž๋ณ„ ์ฐจ๋‹จ ๋ชฉ๋ก ์บ์‹ฑ (TTL: 1์‹œ๊ฐ„) - @Cacheable(value = "blocked_users", key = "#userId", unless = "#userId == -1") - public Set getBlockedUserIds(Long userId) { - return blockRepository.findBlockedUserIdsByBlockerId(userId); - } - - // ์ฐจ๋‹จ ๊ด€๊ณ„ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” - @CacheEvict(value = "blocked_users", key = "#userId") - public void evictBlockCache(Long userId) { - log.info("์ฐจ๋‹จ ์บ์‹œ ๋ฌดํšจํ™”: userId={}", userId); - } - - // ๋ฐฐ์น˜๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ - public Map> getBulkBlockedUsers(Set userIds) { - return userIds.stream() - .filter(id -> id != -1L) // ์ต๋ช… ์‚ฌ์šฉ์ž ์ œ์™ธ - .collect(Collectors.toMap( - Function.identity(), - this::getBlockedUserIds - )); - } -} -``` - -### 2๏ธโƒฃ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” - -```sql --- ์ฐจ๋‹จ ๊ด€๊ณ„ ํ…Œ์ด๋ธ” -CREATE TABLE user_block ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - blocker_id BIGINT NOT NULL COMMENT '์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž', - blocked_id BIGINT NOT NULL COMMENT '์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž', - block_type VARCHAR(20) DEFAULT 'FULL', - reason VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uk_blocker_blocked (blocker_id, blocked_id) -); - --- ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ธ๋ฑ์Šค -CREATE INDEX idx_user_block_blocker ON user_block(blocker_id); -CREATE INDEX idx_user_block_blocked ON user_block(blocked_id); -``` - -### 3๏ธโƒฃ ์„ฑ๋Šฅ ๋น„๊ต ๋ถ„์„ - -| ๋ฐฉ์‹ | DB ์ฟผ๋ฆฌ ํšŸ์ˆ˜ | ์‘๋‹ต ์‹œ๊ฐ„ | ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ | ์œ ์ง€๋ณด์ˆ˜์„ฑ | -|------|-------------|-----------|--------------|------------| -| **์ฟผ๋ฆฌ ์ˆ˜์ •** | 1ํšŒ | 50ms | ๋‚ฎ์Œ | โญโญ | -| **์„œ๋น„์Šค ํ›„์ฒ˜๋ฆฌ** | 2ํšŒ | 80ms | ์ค‘๊ฐ„ | โญโญโญ | -| **AOP (์บ์‹œ ์ ์šฉ)** | 1ํšŒ + ์บ์‹œ | 60ms | ์ค‘๊ฐ„ | โญโญโญโญโญ | -| **ํ”„๋ก ํŠธ์—”๋“œ** | 2ํšŒ | 100ms | ๋†’์Œ | โญ | - ---- - -## ๐ŸŽญ ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ ๋ฐฉ์•ˆ - -### ๐Ÿ“ก API ์‘๋‹ต ํ˜•์‹ - -#### ๊ธฐ์กด ์‘๋‹ต -```json -{ - "success": true, - "data": [ - { - "reviewId": 123, - "content": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", - "authorNickname": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„", - "authorProfileImage": "https://...", - "rating": 4.5, - "createdAt": "2024-01-15T10:30:00" - } - ] -} -``` - -#### ์ฐจ๋‹จ ์ ์šฉ ํ›„ ์‘๋‹ต -```json -{ - "success": true, - "data": [ - { - "reviewId": 123, - "content": "*** ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค ***", - "authorNickname": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", - "authorProfileImage": null, - "rating": 4.5, - "createdAt": "2024-01-15T10:30:00", - "isBlocked": true, - "blockReason": "BLOCKED_USER" - } - ] -} -``` - -### ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ UI ์ปดํฌ๋„ŒํŠธ - -#### React ์˜ˆ์‹œ -```tsx -interface ReviewCardProps { - review: ReviewResponse; -} - -const ReviewCard: React.FC = ({ review }) => { - const [showBlocked, setShowBlocked] = useState(false); - - if (review.isBlocked && !showBlocked) { - return ( -

-
-
- - ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๋ฆฌ๋ทฐ -
- -
-
- ); - } - - return ( -
-
- ํ”„๋กœํ•„ - {review.authorNickname} - -
-
- {review.content} -
- {review.isBlocked && ( -
- ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ  - -
- )} -
- ); -}; -``` - -#### CSS ์Šคํƒ€์ผ -```css -.review-card.blocked { - position: relative; - filter: blur(5px); - opacity: 0.6; -} - -.blur-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.8); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - backdrop-filter: blur(10px); -} - -.blocked-message { - color: white; - text-align: center; - margin-bottom: 16px; -} - -.show-content-btn { - background: #007bff; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; -} - -.blocked-indicator { - background: #fff3cd; - border: 1px solid #ffeaa7; - padding: 8px; - border-radius: 4px; - margin-top: 8px; - font-size: 12px; - color: #856404; -} -``` - ---- - -## ๐Ÿ“‹ ๊ตฌํ˜„ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ - -### ๐Ÿ—๏ธ Phase 1: ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ ๊ตฌ์ถ• - -#### 1.1 ์ฐจ๋‹จ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ƒ์„ฑ -```java -// 1. Entity ์ƒ์„ฑ -@Entity -@Table(name = "user_block") -public class UserBlock extends BaseTimeEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "blocker_id", nullable = false) - private Long blockerId; - - @Column(name = "blocked_id", nullable = false) - private Long blockedId; - - @Enumerated(EnumType.STRING) - private BlockType blockType = BlockType.FULL; - - private String reason; -} - -// 2. Repository ์ƒ์„ฑ -public interface BlockRepository extends JpaRepository { - Set findBlockedIdsByBlockerId(Long blockerId); - boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); -} - -// 3. Service ์ƒ์„ฑ -@Service -public class BlockService { - public void createBlock(Long blockerId, Long blockedId) { ... } - public void removeBlock(Long blockerId, Long blockedId) { ... } - public Set getBlockedUserIds(Long userId) { ... } -} -``` - -#### 1.2 ์บ์‹œ ์„ค์ • -```yaml -# application.yml -spring: - cache: - type: caffeine - caffeine: - spec: maximumSize=10000,expireAfterWrite=1h -``` - -### ๐Ÿ”ง Phase 2: AOP ๊ตฌํ˜„ - -#### 2.1 ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐ AOP ๊ตฌํ˜„ -```java -// ์•ž์„œ ์ œ์‹œํ•œ ์ฝ”๋“œ ๊ตฌํ˜„ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface BlockFilter { ... } - -@Component -@Aspect -public class BlockFilterAspect { ... } -``` - -#### 2.2 ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ -```java -@ExtendWith(MockitoExtension.class) -class BlockFilterAspectTest { - - @Mock - private BlockService blockService; - - @InjectMocks - private BlockFilterAspect blockFilterAspect; - - @Test - void ์ฐจ๋‹จ๋œ_์‚ฌ์šฉ์ž_๋ฆฌ๋ทฐ_๋ธ”๋Ÿฌ_์ฒ˜๋ฆฌ_ํ…Œ์ŠคํŠธ() { - // Given - Long currentUserId = 1L; - Long blockedUserId = 2L; - when(blockService.getBlockedUserIds(currentUserId)) - .thenReturn(Set.of(blockedUserId)); - - // When & Then - // AOP ๋กœ์ง ํ…Œ์ŠคํŠธ - } -} -``` - -### ๐Ÿ“ฑ Phase 3: ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ - -#### 3.1 API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • -```typescript -// API ํƒ€์ž… ์ •์˜ ํ™•์žฅ -interface BaseResponse { - isBlocked?: boolean; - blockReason?: string; -} - -interface ReviewResponse extends BaseResponse { - reviewId: number; - content: string; - authorNickname: string; - // ... ๊ธฐํƒ€ ํ•„๋“œ -} -``` - -#### 3.2 UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ -```tsx -// ์•ž์„œ ์ œ์‹œํ•œ React ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ -``` - -### ๐Ÿš€ Phase 4: ์ ์ง„์  ์ ์šฉ - -#### 4.1 ์šฐ์„ ์ˆœ์œ„๋ณ„ ์ ์šฉ -1. **High Priority**: ๋ฆฌ๋ทฐ ์กฐํšŒ, ๋Œ“๊ธ€ ์กฐํšŒ -2. **Medium Priority**: ํŒ”๋กœ์šฐ ๋ชฉ๋ก, ๋งˆ์ดํŽ˜์ด์ง€ -3. **Low Priority**: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ, ์ถ”์ฒœ ๋ชฉ๋ก - -#### 4.2 ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • -```java -@Component -public class BlockMetrics { - private final MeterRegistry meterRegistry; - - public void recordBlockFilter(String endpoint, BlockType type, int blockedCount) { - Counter.builder("block.filter.applied") - .tag("endpoint", endpoint) - .tag("type", type.name()) - .register(meterRegistry) - .increment(blockedCount); - } -} -``` - -### ๐Ÿ“Š Phase 5: ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง - -#### 5.1 ์„ฑ๋Šฅ ์ง€ํ‘œ ์ˆ˜์ง‘ -- ์ฐจ๋‹จ ์บ์‹œ ํžˆํŠธ์œจ -- ์‘๋‹ต ์‹œ๊ฐ„ ๋ณ€ํ™” -- ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ณ€ํ™” - -#### 5.2 ์•Œ๋žŒ ์„ค์ • -```yaml -# Prometheus ๊ทœ์น™ -groups: - - name: block_system - rules: - - alert: BlockCacheHitRateLow - expr: block_cache_hit_rate < 0.8 - for: 5m - annotations: - summary: "์ฐจ๋‹จ ์บ์‹œ ํžˆํŠธ์œจ์ด ๋‚ฎ์Šต๋‹ˆ๋‹ค" -``` - ---- - -## ๐ŸŽฏ ๊ฒฐ๋ก  - -### โœจ ์™œ AOP ๋ฐฉ์‹์„ ์ถ”์ฒœํ•˜๋Š”๊ฐ€? - -1. **์ตœ์†Œ ์นจ์Šต์ ** - ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๊ฑฐ์˜ ์ˆ˜์ •ํ•˜์ง€ ์•Š์Œ -2. **์ค‘์•™ ์ง‘์ค‘์‹** - ์ฐจ๋‹จ ๋กœ์ง์ด ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ๋จ -3. **ํ™•์žฅ ๊ฐ€๋Šฅ** - ๋‹ค์–‘ํ•œ ์ฐจ๋‹จ ํƒ€์ž…์„ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ -4. **์„ฑ๋Šฅ ํšจ์œจ์ ** - ์บ์‹œ๋ฅผ ํ†ตํ•œ ์ตœ์ ํ™” -5. **ํ…Œ์ŠคํŠธ ์šฉ์ด** - ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ - -### ๐Ÿš€ ๊ธฐ๋Œ€ ํšจ๊ณผ - -- **๊ฐœ๋ฐœ ์‹œ๊ฐ„ ๋‹จ์ถ•**: ๊ธฐ์กด ์ฟผ๋ฆฌ ์ˆ˜์ • ๋ถˆํ•„์š” -- **์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ**: ์ฐจ๋‹จ ๋กœ์ง ์ค‘์•™ํ™” -- **์„ฑ๋Šฅ ์•ˆ์ •์„ฑ**: ์บ์‹œ ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” -- **์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ **: ์„ธ๋ฐ€ํ•œ ์ฐจ๋‹จ ์˜ต์…˜ ์ œ๊ณต - -### ๐Ÿ“… ๊ตฌํ˜„ ์ผ์ • (์˜ˆ์ƒ) - -- **Week 1**: Phase 1-2 (๊ธฐ๋ฐ˜ ๊ตฌ์กฐ + AOP) -- **Week 2**: Phase 3 (ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™) -- **Week 3**: Phase 4-5 (์ ์ง„์  ์ ์šฉ + ๋ชจ๋‹ˆํ„ฐ๋ง) - -**์ด ์˜ˆ์ƒ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„: 3์ฃผ** - ---- - -## ๐Ÿ“ž ๋ฌธ์˜ ๋ฐ ์ง€์› - -์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋ฌธ์˜์‚ฌํ•ญ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ์—ฐ๋ฝ์ฃผ์„ธ์š”! - -- ๊ธฐ์ˆ ์  ๋ฌธ์˜: ๊ฐœ๋ฐœํŒ€ ์ฑ„๋„ -- ๊ธฐํš ๊ด€๋ จ: ๊ธฐํšํŒ€ ์ฑ„๋„ -- ์„ฑ๋Šฅ ๊ด€๋ จ: DevOps ํŒ€ ์ฑ„๋„ \ No newline at end of file From 62aae17a73e91707bab13debb4f0519119e33bf2 Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 13:21:26 +0900 Subject: [PATCH 05/16] docs: add block readme make by claude Signed-off-by: hgkim --- .../app/bottlenote/support/block/README.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/main/java/app/bottlenote/support/block/README.md diff --git a/src/main/java/app/bottlenote/support/block/README.md b/src/main/java/app/bottlenote/support/block/README.md new file mode 100644 index 000000000..1bac86057 --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/README.md @@ -0,0 +1,114 @@ +# Block ๊ธฐ๋Šฅ - ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์‹œ์Šคํ…œ + +## ๐Ÿ“‹ ๊ฐœ์š” + +์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ/ํ•ด์ œ ๋ฐ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ  ์ž๋™ ํ•„ํ„ฐ๋ง์„ ์ œ๊ณตํ•˜๋Š” ๋…๋ฆฝ์ ์ธ ๋„๋ฉ”์ธ ๋ชจ๋“ˆ์ž…๋‹ˆ๋‹ค. + +## ๐Ÿ—๏ธ ๊ตฌ์กฐ + +``` +support/block/ # Block ๋„๋ฉ”์ธ +โ”œโ”€โ”€ controller/BlockController.java # REST API +โ”œโ”€โ”€ domain/UserBlock.java # ์ฐจ๋‹จ ๊ด€๊ณ„ ์—”ํ‹ฐํ‹ฐ +โ”œโ”€โ”€ exception/ # Block ์ „์šฉ ์˜ˆ์™ธ +โ”œโ”€โ”€ repository/ # Repository ์ธํ„ฐํŽ˜์ด์Šค & ๊ตฌํ˜„์ฒด +โ””โ”€โ”€ service/BlockService.java # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง + +common/block/ # ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ +โ”œโ”€โ”€ annotation/BlockWord.java # @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ +โ”œโ”€โ”€ config/BlockWordConfig.java # Spring ์„ค์ • +โ””โ”€โ”€ serializer/BlockWordSerializer.java # Jackson ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € +``` + +## ๐ŸŽฏ ํ•ต์‹ฌ ๊ธฐ๋Šฅ + +### 1. ์ฐจ๋‹จ ๊ด€๋ฆฌ API + +```http +POST /api/v1/blocks/create # ์‚ฌ์šฉ์ž ์ฐจ๋‹จ +DELETE /api/v1/blocks/{blockedUserId} # ์ฐจ๋‹จ ํ•ด์ œ +GET /api/v1/blocks # ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ +``` + +### 2. ์ž๋™ ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง + +`@BlockWord` ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด JSON ์ง๋ ฌํ™” ์‹œ์ ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ์ปจํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค. + +```java +public record ReviewInfo( + @BlockWord(userIdPath = "userInfo.userId") + String reviewContent, // "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค" + UserInfo userInfo +) {} + +public record UserInfo( + Long userId, + @BlockWord(value = "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž", userIdPath = "userId") + String nickName, // "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" + String userProfileImage +) {} +``` + +**๊ฒฐ๊ณผ:** +```json +// ์ฐจ๋‹จ ์ „ +{ + "reviewContent": "์ •๋ง ๋ง›์žˆ๋Š” ์œ„์Šคํ‚ค์ž…๋‹ˆ๋‹ค!", + "userInfo": { + "nickName": "์œ„์Šคํ‚ค๋Ÿฌ๋ฒ„" + } +} + +// ์ฐจ๋‹จ ํ›„ +{ + "reviewContent": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ๊ธ€์ž…๋‹ˆ๋‹ค", + "userInfo": { + "nickName": "์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž" + } +} +``` + +## ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค + +### UserBlock ์—”ํ‹ฐํ‹ฐ +- **์ฐจ๋‹จ ๊ด€๊ณ„**: blocker_id โ†” blocked_id +- **์œ ๋‹ˆํฌ ์ œ์•ฝ**: ์ค‘๋ณต ์ฐจ๋‹จ ๋ฐฉ์ง€ +- **์ธ๋ฑ์Šค**: ์กฐํšŒ ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **BaseTimeEntity**: ์ƒ์„ฑ/์ˆ˜์ • ์‹œ๊ฐ„ ์ž๋™ ๊ด€๋ฆฌ + +## โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### ์บ์‹ฑ +- **์ฐจ๋‹จ ๋ชฉ๋ก**: `@Cacheable(value = "blocked_users")` - 2์‹œ๊ฐ„ TTL +- **์บ์‹œ ๋ฌดํšจํ™”**: `@CacheEvict` - ์ฐจ๋‹จ/ํ•ด์ œ ์‹œ ์ž๋™ ๋ฌดํšจํ™” + +### Repository ์ฃผ์š” ๋ฉ”์„œ๋“œ +```java +Set findBlockedUserIdsByBlockerId(Long blockerId); // ์ฐจ๋‹จ ๋ชฉ๋ก +boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); // ์ฐจ๋‹จ ์—ฌ๋ถ€ +boolean existsMutualBlock(Long userId1, Long userId2); // ์ƒํ˜ธ ์ฐจ๋‹จ +long countByBlockerId(Long blockerId); // ์ฐจ๋‹จํ•œ ์ˆ˜ +long countByBlockedId(Long blockedId); // ์ฐจ๋‹จ๋‹นํ•œ ์ˆ˜ +``` + +## ๐Ÿงช ํ…Œ์ŠคํŠธ + +### HTTP ํ…Œ์ŠคํŠธ ํŒŒ์ผ +``` +http/block/์ฐจ๋‹จ_ํ•„ํ„ฐ๋ง_ํ…Œ์ŠคํŠธ.http +``` + +### ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค +1. ํ† ํฐ ๋ฐœ๊ธ‰ โ†’ ์‚ฌ์šฉ์ž ์ฐจ๋‹จ โ†’ ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ โ†’ ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง ํ™•์ธ โ†’ ์ฐจ๋‹จ ํ•ด์ œ + +## ๐Ÿ”ง ๋™์ž‘ ์›๋ฆฌ + +### ์ฐจ๋‹จ ์ƒ์„ฑ +1. API ํ˜ธ์ถœ โ†’ ์œ ํšจ์„ฑ ๊ฒ€์ฆ โ†’ ์ค‘๋ณต ํ™•์ธ โ†’ DB ์ €์žฅ โ†’ ์บ์‹œ ๋ฌดํšจํ™” + +### ์ปจํ…์ธ  ํ•„ํ„ฐ๋ง +1. JSON ์ง๋ ฌํ™” โ†’ @BlockWord ๊ฐ์ง€ โ†’ ํ˜„์žฌ ์‚ฌ์šฉ์ž ํ™•์ธ โ†’ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ โ†’ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€/์›๋ณธ ๋ฐ˜ํ™˜ + +--- + +**ํ•ต์‹ฌ**: ๊ฐ„๋‹จํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ํ•˜๋‚˜๋กœ ์ „์ฒด ์‹œ์Šคํ…œ์—์„œ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ปจํ…์ธ ๊ฐ€ ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค. \ No newline at end of file From ad992f73ed40d6e1116192b29cbddcffe7322ec1 Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 14:17:40 +0900 Subject: [PATCH 06/16] feat: enhance block functionality with validation and service integration Signed-off-by: hgkim --- git.environment-variables | 2 +- .../support/block/controller/BlockController.java | 12 ++++-------- .../support/block/service/BlockService.java | 5 +++++ .../review/controller/ReviewControllerTest.java | 5 +++++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/git.environment-variables b/git.environment-variables index b146e17ba..793c442a0 160000 --- a/git.environment-variables +++ b/git.environment-variables @@ -1 +1 @@ -Subproject commit b146e17baab4747e4f7045340b8cb13636db09e8 +Subproject commit 793c442a01a00ba80b0c39faf132b33a01f92939 diff --git a/src/main/java/app/bottlenote/support/block/controller/BlockController.java b/src/main/java/app/bottlenote/support/block/controller/BlockController.java index 39ab96cd8..f5a60f43e 100644 --- a/src/main/java/app/bottlenote/support/block/controller/BlockController.java +++ b/src/main/java/app/bottlenote/support/block/controller/BlockController.java @@ -2,8 +2,10 @@ import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.support.block.dto.request.BlockCreateRequest; import app.bottlenote.support.block.exception.BlockException; import app.bottlenote.support.block.service.BlockService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -32,7 +34,7 @@ public class BlockController { * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ */ @PostMapping("/create") - public ResponseEntity createBlock(@RequestBody BlockCreateRequest request) { + public ResponseEntity createBlock(@RequestBody @Valid BlockCreateRequest request) { Long currentUserId = SecurityContextUtil.getUserIdByContext() .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); @@ -45,7 +47,7 @@ public ResponseEntity createBlock(@RequestBody BlockCreateRequest request) { * ์ฐจ๋‹จ ํ•ด์ œ */ @DeleteMapping("/{blockedUserId}") - public ResponseEntity unblockUser(@PathVariable Long blockedUserId) { + public ResponseEntity deleteBlock(@PathVariable Long blockedUserId) { Long currentUserId = SecurityContextUtil.getUserIdByContext() .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); @@ -64,10 +66,4 @@ public ResponseEntity getBlockedUsers() { return GlobalResponse.ok(blockService.getBlockedUserIds(currentUserId)); } - - /** - * ์ฐจ๋‹จ ์š”์ฒญ DTO - */ - public record BlockCreateRequest(Long blockedUserId) { - } } diff --git a/src/main/java/app/bottlenote/support/block/service/BlockService.java b/src/main/java/app/bottlenote/support/block/service/BlockService.java index a7a16178b..dbb96e140 100644 --- a/src/main/java/app/bottlenote/support/block/service/BlockService.java +++ b/src/main/java/app/bottlenote/support/block/service/BlockService.java @@ -86,6 +86,7 @@ public void unblockUser(Long blockerId, Long blockedId) { * @param userId ์‚ฌ์šฉ์ž ID * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก */ + @Transactional(readOnly = true) @Cacheable(value = "blocked_users", key = "#userId") public Set getBlockedUserIds(Long userId) { if (userId == null) { @@ -102,6 +103,7 @@ public Set getBlockedUserIds(Long userId) { * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ */ + @Transactional(readOnly = true) public boolean isBlocked(Long blockerId, Long blockedId) { if (blockerId == null || blockedId == null) { return false; @@ -118,6 +120,7 @@ public boolean isBlocked(Long blockerId, Long blockedId) { * @param userId2 ์‚ฌ์šฉ์ž 2 ID * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ */ + @Transactional(readOnly = true) public boolean isMutualBlocked(Long userId1, Long userId2) { if (userId1 == null || userId2 == null) { return false; @@ -132,6 +135,7 @@ public boolean isMutualBlocked(Long userId1, Long userId2) { * @param userId ์‚ฌ์šฉ์ž ID * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ */ + @Transactional(readOnly = true) public long getBlockedByCount(Long userId) { if (userId == null) { return 0L; @@ -146,6 +150,7 @@ public long getBlockedByCount(Long userId) { * @param userId ์‚ฌ์šฉ์ž ID * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ */ + @Transactional(readOnly = true) public long getBlockingCount(Long userId) { if (userId == null) { return 0L; diff --git a/src/test/java/app/bottlenote/review/controller/ReviewControllerTest.java b/src/test/java/app/bottlenote/review/controller/ReviewControllerTest.java index 079626282..1d0186d0d 100644 --- a/src/test/java/app/bottlenote/review/controller/ReviewControllerTest.java +++ b/src/test/java/app/bottlenote/review/controller/ReviewControllerTest.java @@ -24,6 +24,7 @@ import app.bottlenote.review.exception.ReviewException; import app.bottlenote.review.fixture.ReviewObjectFixture; import app.bottlenote.review.service.ReviewService; +import app.bottlenote.support.block.service.BlockService; import app.bottlenote.user.exception.UserExceptionCode; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.ExpiredJwtException; @@ -97,12 +98,16 @@ class ReviewControllerTest { protected MockMvc mockMvc; @MockBean private ReviewService reviewService; + @MockBean + private BlockService blockService; private MockedStatic mockedSecurityUtil; @BeforeEach void setup() { mockedSecurityUtil = mockStatic(SecurityContextUtil.class); + // BlockService mock ์„ค์ • - ์ฐจ๋‹จ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์„ค์ • + when(blockService.isBlocked(any(), any())).thenReturn(false); } @AfterEach From 4641a8689c20181bf85bda4ac4d4970ac96213fa Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 14:19:34 +0900 Subject: [PATCH 07/16] feat: add BlockCreateRequest DTO for block request handling Signed-off-by: hgkim --- .../support/block/dto/request/BlockCreateRequest.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/app/bottlenote/support/block/dto/request/BlockCreateRequest.java diff --git a/src/main/java/app/bottlenote/support/block/dto/request/BlockCreateRequest.java b/src/main/java/app/bottlenote/support/block/dto/request/BlockCreateRequest.java new file mode 100644 index 000000000..8c2ef5ef9 --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/dto/request/BlockCreateRequest.java @@ -0,0 +1,7 @@ +package app.bottlenote.support.block.dto.request; + +/** + * ์ฐจ๋‹จ ์š”์ฒญ DTO + */ +public record BlockCreateRequest(Long blockedUserId) { +} \ No newline at end of file From dc2aa769c42d8dd3199d75923470d7743dc45411 Mon Sep 17 00:00:00 2001 From: hgkim Date: Fri, 27 Jun 2025 20:27:36 +0900 Subject: [PATCH 08/16] feat: add no arg construct --- .../common/block/serializer/BlockWordSerializer.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java index f3adf9aba..54da6db97 100644 --- a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java +++ b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java @@ -29,10 +29,15 @@ public class BlockWordSerializer extends JsonSerializer implements Conte public BlockWordSerializer(BlockService blockService) { this.blockService = blockService; } + + public BlockWordSerializer() { + this.blockService = null; + } @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - if (blockWordAnnotation == null) { + if (blockWordAnnotation == null || blockService == null) { + // ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ BlockService๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋“ฑ) gen.writeString(value); return; } @@ -88,7 +93,9 @@ public JsonSerializer createContextual(SerializerProvider prov, BeanProperty } if (annotation != null) { - BlockWordSerializer serializer = new BlockWordSerializer(blockService); + BlockWordSerializer serializer = blockService != null + ? new BlockWordSerializer(blockService) + : new BlockWordSerializer(); serializer.blockWordAnnotation = annotation; return serializer; } From e24d6e7a48fd91a608000790cdf9132774cecd33 Mon Sep 17 00:00:00 2001 From: hgkim Date: Sun, 29 Jun 2025 13:31:03 +0900 Subject: [PATCH 09/16] feat: refactor BlockWord configuration and serializer for improved dependency injection Signed-off-by: hgkim --- .../common/block/config/BlockWordConfig.java | 12 +- .../block/serializer/BlockWordSerializer.java | 47 ++- .../support/block/service/BlockService.java | 296 +++++++++--------- 3 files changed, 190 insertions(+), 165 deletions(-) diff --git a/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java index a1df56fc5..3352343be 100644 --- a/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java +++ b/src/main/java/app/bottlenote/common/block/config/BlockWordConfig.java @@ -1,19 +1,11 @@ package app.bottlenote.common.block.config; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; -/** - * BlockWord ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์„ค์ • - * - * @BlockWord ์–ด๋…ธํ…Œ์ด์…˜์— @JsonSerialize๊ฐ€ ์ง์ ‘ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ - * ๊ธ€๋กœ๋ฒŒ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ๋“ฑ๋ก์€ ๋ถˆํ•„์š”ํ•จ - */ @Slf4j @Configuration +@RequiredArgsConstructor public class BlockWordConfig { - - public BlockWordConfig() { - log.debug("BlockWordConfig ์ดˆ๊ธฐํ™” ์™„๋ฃŒ - @BlockWord ์–ด๋…ธํ…Œ์ด์…˜ ์ž์ฒด์— @JsonSerialize ํฌํ•จ"); - } } diff --git a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java index 54da6db97..93b24a481 100644 --- a/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java +++ b/src/main/java/app/bottlenote/common/block/serializer/BlockWordSerializer.java @@ -10,6 +10,9 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import java.io.IOException; @@ -21,8 +24,9 @@ */ @Slf4j @Component -public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer { +public class BlockWordSerializer extends JsonSerializer implements ContextualSerializer, ApplicationContextAware { + private static ApplicationContext applicationContext; private final BlockService blockService; private BlockWord blockWordAnnotation; @@ -34,10 +38,31 @@ public BlockWordSerializer() { this.blockService = null; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + BlockWordSerializer.applicationContext = applicationContext; + } + @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - if (blockWordAnnotation == null || blockService == null) { - // ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ BlockService๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋“ฑ) + if (blockWordAnnotation == null) { + // ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ + gen.writeString(value); + return; + } + + // BlockService ํš๋“ (์˜์กด์„ฑ ์ฃผ์ž…๋œ ๊ฒƒ์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ApplicationContext์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) + BlockService currentBlockService = blockService; + if (currentBlockService == null && applicationContext != null) { + try { + currentBlockService = applicationContext.getBean(BlockService.class); + } catch (Exception e) { + log.warn("ApplicationContext์—์„œ BlockService๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจ: {}", e.getMessage()); + } + } + + if (currentBlockService == null) { + // BlockService๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์›๋ณธ ๊ฐ’ ๋ฐ˜ํ™˜ (ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋“ฑ) gen.writeString(value); return; } @@ -66,7 +91,7 @@ public void serialize(String value, JsonGenerator gen, SerializerProvider serial } // ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ - boolean isBlocked = blockService.isBlocked(currentUserId, authorId); + boolean isBlocked = currentBlockService.isBlocked(currentUserId, authorId); if (isBlocked) { // ์ฐจ๋‹จ๋œ ๊ฒฝ์šฐ ๋Œ€์ฒด ๋ฉ”์‹œ์ง€๋กœ ๋ณ€๊ฒฝ gen.writeString(blockWordAnnotation.value()); @@ -93,9 +118,17 @@ public JsonSerializer createContextual(SerializerProvider prov, BeanProperty } if (annotation != null) { - BlockWordSerializer serializer = blockService != null - ? new BlockWordSerializer(blockService) - : new BlockWordSerializer(); + // ApplicationContext์—์„œ BlockService ๊ฐ€์ ธ์˜ค๊ธฐ + BlockService contextBlockService = null; + if (applicationContext != null) { + try { + contextBlockService = applicationContext.getBean(BlockService.class); + } catch (Exception e) { + log.warn("ApplicationContext์—์„œ BlockService ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: {}", e.getMessage()); + } + } + + BlockWordSerializer serializer = new BlockWordSerializer(contextBlockService); serializer.blockWordAnnotation = annotation; return serializer; } diff --git a/src/main/java/app/bottlenote/support/block/service/BlockService.java b/src/main/java/app/bottlenote/support/block/service/BlockService.java index dbb96e140..28ee824c5 100644 --- a/src/main/java/app/bottlenote/support/block/service/BlockService.java +++ b/src/main/java/app/bottlenote/support/block/service/BlockService.java @@ -27,153 +27,153 @@ @Transactional(readOnly = true) public class BlockService { - private final UserBlockRepository userBlockRepository; - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ - * - * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#blockerId") - public void blockUser(Long blockerId, Long blockedId, String reason) { - validateBlockRequest(blockerId, blockedId); - - // ์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ธ์ง€ ํ™•์ธ - if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { - throw new BlockException(USER_ALREADY_BLOCKED); - } - - // ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ - UserBlock userBlock = UserBlock.create(blockerId, blockedId, reason); - userBlockRepository.save(userBlock); - - log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); - } - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‚ฌ์œ  ์—†์Œ) - */ - @Transactional - public void blockUser(Long blockerId, Long blockedId) { - blockUser(blockerId, blockedId, null); - } - - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ - * - * @param blockerId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ ํ•ด์ œ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - */ - @Transactional - @CacheEvict(value = "blocked_users", key = "#blockerId") - public void unblockUser(Long blockerId, Long blockedId) { - validateBlockRequest(blockerId, blockedId); - - UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) - .orElseThrow(() -> new BlockException(USER_BLOCK_NOT_FOUND)); - - userBlockRepository.delete(userBlock); - - log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); - } - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ (์บ์‹œ ์ ์šฉ) - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - */ - @Transactional(readOnly = true) - @Cacheable(value = "blocked_users", key = "#userId") - public Set getBlockedUserIds(Long userId) { - if (userId == null) { - return Set.of(); - } - - return userBlockRepository.findBlockedUserIdsByBlockerId(userId); - } - - /** - * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ - * - * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ - @Transactional(readOnly = true) - public boolean isBlocked(Long blockerId, Long blockedId) { - if (blockerId == null || blockedId == null) { - return false; - } - - Set blockedUsers = getBlockedUserIds(blockerId); - return blockedUsers.contains(blockedId); - } - - /** - * ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ - * - * @param userId1 ์‚ฌ์šฉ์ž 1 ID - * @param userId2 ์‚ฌ์šฉ์ž 2 ID - * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ - @Transactional(readOnly = true) - public boolean isMutualBlocked(Long userId1, Long userId2) { - if (userId1 == null || userId2 == null) { - return false; - } - - return userBlockRepository.existsMutualBlock(userId1, userId2); - } - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ - @Transactional(readOnly = true) - public long getBlockedByCount(Long userId) { - if (userId == null) { - return 0L; - } - - return userBlockRepository.countByBlockedId(userId); - } - - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ - @Transactional(readOnly = true) - public long getBlockingCount(Long userId) { - if (userId == null) { - return 0L; - } - - return userBlockRepository.countByBlockerId(userId); - } - - /** - * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ - */ - private void validateBlockRequest(Long blockerId, Long blockedId) { - if (blockerId == null) { - throw new BlockException(REQUIRED_USER_ID); - } - - if (blockedId == null) { - throw new BlockException(REQUIRED_USER_ID); - } - - if (blockerId.equals(blockedId)) { - throw new BlockException(CANNOT_BLOCK_SELF); - } - } + private final UserBlockRepository userBlockRepository; + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ + * + * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public void blockUser(Long blockerId, Long blockedId, String reason) { + validateBlockRequest(blockerId, blockedId); + + // ์ด๋ฏธ ์ฐจ๋‹จ๋œ ๊ด€๊ณ„์ธ์ง€ ํ™•์ธ + if (userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)) { + throw new BlockException(USER_ALREADY_BLOCKED); + } + + // ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ + UserBlock userBlock = UserBlock.create(blockerId, blockedId, reason); + userBlockRepository.save(userBlock); + + log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‚ฌ์œ  ์—†์Œ) + */ + @Transactional + public void blockUser(Long blockerId, Long blockedId) { + blockUser(blockerId, blockedId, null); + } + + /** + * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ + * + * @param blockerId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ ํ•ด์ œ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + */ + @Transactional + @CacheEvict(value = "blocked_users", key = "#blockerId") + public void unblockUser(Long blockerId, Long blockedId) { + validateBlockRequest(blockerId, blockedId); + + UserBlock userBlock = userBlockRepository.findByBlockerIdAndBlockedId(blockerId, blockedId) + .orElseThrow(() -> new BlockException(USER_BLOCK_NOT_FOUND)); + + userBlockRepository.delete(userBlock); + + log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ (์บ์‹œ ์ ์šฉ) + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก + */ + @Transactional(readOnly = true) + @Cacheable(value = "blocked_users", key = "#userId") + public Set getBlockedUserIds(Long userId) { + if (userId == null) { + return Set.of(); + } + + return userBlockRepository.findBlockedUserIdsByBlockerId(userId); + } + + /** + * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ + * + * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID + * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + @Transactional(readOnly = true) + public boolean isBlocked(Long blockerId, Long blockedId) { + if (blockerId == null || blockedId == null) { + return false; + } + + Set blockedUsers = getBlockedUserIds(blockerId); + return blockedUsers.contains(blockedId); + } + + /** + * ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ + * + * @param userId1 ์‚ฌ์šฉ์ž 1 ID + * @param userId2 ์‚ฌ์šฉ์ž 2 ID + * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ + */ + @Transactional(readOnly = true) + public boolean isMutualBlocked(Long userId1, Long userId2) { + if (userId1 == null || userId2 == null) { + return false; + } + + return userBlockRepository.existsMutualBlock(userId1, userId2); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + @Transactional(readOnly = true) + public long getBlockedByCount(Long userId) { + if (userId == null) { + return 0L; + } + + return userBlockRepository.countByBlockedId(userId); + } + + /** + * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ + */ + @Transactional(readOnly = true) + public long getBlockingCount(Long userId) { + if (userId == null) { + return 0L; + } + + return userBlockRepository.countByBlockerId(userId); + } + + /** + * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + private void validateBlockRequest(Long blockerId, Long blockedId) { + if (blockerId == null) { + throw new BlockException(REQUIRED_USER_ID); + } + + if (blockedId == null) { + throw new BlockException(REQUIRED_USER_ID); + } + + if (blockerId.equals(blockedId)) { + throw new BlockException(CANNOT_BLOCK_SELF); + } + } } From 0d841700d7259f6615062fa8c04231f36fadce7f Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 01:11:43 +0900 Subject: [PATCH 10/16] feat: enhance block management with new endpoints and response structure Signed-off-by: hgkim --- .../block/controller/BlockController.java | 66 +++++++++++++---- .../block/dto/response/UserBlockItem.java | 10 +++ .../block/repository/UserBlockRepository.java | 52 +------------ .../support/block/service/BlockService.java | 73 +++++-------------- 4 files changed, 83 insertions(+), 118 deletions(-) create mode 100644 src/main/java/app/bottlenote/support/block/dto/response/UserBlockItem.java diff --git a/src/main/java/app/bottlenote/support/block/controller/BlockController.java b/src/main/java/app/bottlenote/support/block/controller/BlockController.java index f5a60f43e..d190ad2b5 100644 --- a/src/main/java/app/bottlenote/support/block/controller/BlockController.java +++ b/src/main/java/app/bottlenote/support/block/controller/BlockController.java @@ -1,8 +1,10 @@ package app.bottlenote.support.block.controller; +import app.bottlenote.global.data.response.CollectionResponse; import app.bottlenote.global.data.response.GlobalResponse; import app.bottlenote.global.security.SecurityContextUtil; import app.bottlenote.support.block.dto.request.BlockCreateRequest; +import app.bottlenote.support.block.dto.response.UserBlockItem; import app.bottlenote.support.block.exception.BlockException; import app.bottlenote.support.block.service.BlockService; import jakarta.validation.Valid; @@ -19,9 +21,6 @@ import static app.bottlenote.support.block.exception.BlockExceptionCode.REQUIRED_USER_ID; -/** - * ์ฐจ๋‹จ ๊ด€๋ฆฌ API - */ @Slf4j @RestController @RequiredArgsConstructor @@ -30,40 +29,77 @@ public class BlockController { private final BlockService blockService; - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ - */ - @PostMapping("/create") + @PostMapping public ResponseEntity createBlock(@RequestBody @Valid BlockCreateRequest request) { Long currentUserId = SecurityContextUtil.getUserIdByContext() .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); blockService.blockUser(currentUserId, request.blockedUserId()); + CollectionResponse blockedUsers = blockService.getBlockedUserItems(currentUserId); - return GlobalResponse.ok("์ฐจ๋‹จ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + return GlobalResponse.ok(blockedUsers); } - /** - * ์ฐจ๋‹จ ํ•ด์ œ - */ @DeleteMapping("/{blockedUserId}") public ResponseEntity deleteBlock(@PathVariable Long blockedUserId) { Long currentUserId = SecurityContextUtil.getUserIdByContext() .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); blockService.unblockUser(currentUserId, blockedUserId); + CollectionResponse blockedUsers = blockService.getBlockedUserItems(currentUserId); - return GlobalResponse.ok("์ฐจ๋‹จ์ด ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + return GlobalResponse.ok(blockedUsers); } - /** - * ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ - */ @GetMapping public ResponseEntity getBlockedUsers() { Long currentUserId = SecurityContextUtil.getUserIdByContext() .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + return GlobalResponse.ok(blockService.getBlockedUserItems(currentUserId)); + } + + @GetMapping("/ids") + public ResponseEntity getBlockedUserIds() { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + return GlobalResponse.ok(blockService.getBlockedUserIds(currentUserId)); } + + @GetMapping("/check/{targetUserId}") + public ResponseEntity checkBlocked(@PathVariable Long targetUserId) { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + boolean isBlocked = blockService.isBlocked(currentUserId, targetUserId); + return GlobalResponse.ok(isBlocked); + } + + @GetMapping("/mutual-check/{targetUserId}") + public ResponseEntity checkMutualBlocked(@PathVariable Long targetUserId) { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + boolean isMutualBlocked = blockService.isMutualBlocked(currentUserId, targetUserId); + return GlobalResponse.ok(isMutualBlocked); + } + + @GetMapping("/stats/blocked-by-count") + public ResponseEntity getBlockedByCount() { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + long count = blockService.getBlockedByCount(currentUserId); + return GlobalResponse.ok(count); + } + + @GetMapping("/stats/blocking-count") + public ResponseEntity getBlockingCount() { + Long currentUserId = SecurityContextUtil.getUserIdByContext() + .orElseThrow(() -> new BlockException(REQUIRED_USER_ID)); + + long count = blockService.getBlockingCount(currentUserId); + return GlobalResponse.ok(count); + } } diff --git a/src/main/java/app/bottlenote/support/block/dto/response/UserBlockItem.java b/src/main/java/app/bottlenote/support/block/dto/response/UserBlockItem.java new file mode 100644 index 000000000..e33f05e1a --- /dev/null +++ b/src/main/java/app/bottlenote/support/block/dto/response/UserBlockItem.java @@ -0,0 +1,10 @@ +package app.bottlenote.support.block.dto.response; + +import java.time.LocalDateTime; + +public record UserBlockItem( + Long userId, + String userName, + LocalDateTime blockedAt +) { +} diff --git a/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java b/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java index 434bc4a80..7f0f64749 100644 --- a/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java +++ b/src/main/java/app/bottlenote/support/block/repository/UserBlockRepository.java @@ -1,73 +1,29 @@ package app.bottlenote.support.block.repository; import app.bottlenote.support.block.domain.UserBlock; +import app.bottlenote.support.block.dto.response.UserBlockItem; +import java.util.List; import java.util.Optional; import java.util.Set; -/** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๊ณ„ Repository - */ public interface UserBlockRepository { - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ €์žฅ - */ UserBlock save(UserBlock userBlock); - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์‚ญ์ œ - */ void delete(UserBlock userBlock); - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก ์กฐํšŒ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - */ Set findBlockedUserIdsByBlockerId(Long blockerId); - /** - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ๊ด€๊ณ„ ์กด์žฌ ์—ฌ๋ถ€ - */ boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); - /** - * ํŠน์ • ์ฐจ๋‹จ ๊ด€๊ณ„ ์กฐํšŒ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ๊ด€๊ณ„ ์—”ํ‹ฐํ‹ฐ - */ Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId); - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param blockedId ์ฐจ๋‹จ๋‹นํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ long countByBlockedId(Long blockedId); - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param blockerId ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ long countByBlockerId(Long blockerId); - /** - * ์ƒํ˜ธ ์ฐจ๋‹จ ๊ด€๊ณ„ ํ™•์ธ (์„œ๋กœ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ) - * - * @param userId1 ์‚ฌ์šฉ์ž 1 - * @param userId2 ์‚ฌ์šฉ์ž 2 - * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ boolean existsMutualBlock(Long userId1, Long userId2); + + List findBlockedUserItemsByBlockerId(Long blockerId); } \ No newline at end of file diff --git a/src/main/java/app/bottlenote/support/block/service/BlockService.java b/src/main/java/app/bottlenote/support/block/service/BlockService.java index 28ee824c5..67461bc24 100644 --- a/src/main/java/app/bottlenote/support/block/service/BlockService.java +++ b/src/main/java/app/bottlenote/support/block/service/BlockService.java @@ -1,6 +1,8 @@ package app.bottlenote.support.block.service; +import app.bottlenote.global.data.response.CollectionResponse; import app.bottlenote.support.block.domain.UserBlock; +import app.bottlenote.support.block.dto.response.UserBlockItem; import app.bottlenote.support.block.exception.BlockException; import app.bottlenote.support.block.repository.UserBlockRepository; import lombok.RequiredArgsConstructor; @@ -10,6 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Set; import static app.bottlenote.support.block.exception.BlockExceptionCode.CANNOT_BLOCK_SELF; @@ -17,10 +20,7 @@ import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_ALREADY_BLOCKED; import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_BLOCK_NOT_FOUND; -/** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๋ฆฌ ์„œ๋น„์Šค - * ์ฐจ๋‹จ ๊ด€๊ณ„ ์ƒ์„ฑ, ํ•ด์ œ, ์กฐํšŒ ๋ฐ ์บ์‹ฑ ์ฒ˜๋ฆฌ - */ + @Slf4j @Service @RequiredArgsConstructor @@ -29,13 +29,6 @@ public class BlockService { private final UserBlockRepository userBlockRepository; - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ - * - * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param reason ์ฐจ๋‹จ ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) - */ @Transactional @CacheEvict(value = "blocked_users", key = "#blockerId") public void blockUser(Long blockerId, Long blockedId, String reason) { @@ -53,20 +46,11 @@ public void blockUser(Long blockerId, Long blockedId, String reason) { log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); } - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ (์‚ฌ์œ  ์—†์Œ) - */ @Transactional public void blockUser(Long blockerId, Long blockedId) { blockUser(blockerId, blockedId, null); } - /** - * ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ - * - * @param blockerId ์ฐจ๋‹จ ํ•ด์ œํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ ํ•ด์ œ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - */ @Transactional @CacheEvict(value = "blocked_users", key = "#blockerId") public void unblockUser(Long blockerId, Long blockedId) { @@ -80,12 +64,6 @@ public void unblockUser(Long blockerId, Long blockedId) { log.info("์‚ฌ์šฉ์ž ์ฐจ๋‹จ ํ•ด์ œ ์™„๋ฃŒ - ์ฐจ๋‹จ์ž: {}, ํ”ผ์ฐจ๋‹จ์ž: {}", blockerId, blockedId); } - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID ๋ชฉ๋ก์„ ์กฐํšŒ (์บ์‹œ ์ ์šฉ) - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก - */ @Transactional(readOnly = true) @Cacheable(value = "blocked_users", key = "#userId") public Set getBlockedUserIds(Long userId) { @@ -96,13 +74,20 @@ public Set getBlockedUserIds(Long userId) { return userBlockRepository.findBlockedUserIdsByBlockerId(userId); } - /** - * ๋‘ ์‚ฌ์šฉ์ž ๊ฐ„ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ - * - * @param blockerId ์ฐจ๋‹จํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @param blockedId ์ฐจ๋‹จ๋‹นํ•˜๋Š” ์‚ฌ์šฉ์ž ID - * @return ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ + + @Transactional(readOnly = true) + @Cacheable(value = "blocked_user_items", key = "#userId") + public CollectionResponse getBlockedUserItems(Long userId) { + if (userId == null) { + return CollectionResponse.of(0L, List.of()); + } + + List items = userBlockRepository.findBlockedUserItemsByBlockerId(userId); + long totalCount = items.size(); + + return CollectionResponse.of(totalCount, items); + } + @Transactional(readOnly = true) public boolean isBlocked(Long blockerId, Long blockedId) { if (blockerId == null || blockedId == null) { @@ -113,13 +98,6 @@ public boolean isBlocked(Long blockerId, Long blockedId) { return blockedUsers.contains(blockedId); } - /** - * ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ - * - * @param userId1 ์‚ฌ์šฉ์ž 1 ID - * @param userId2 ์‚ฌ์šฉ์ž 2 ID - * @return ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ - */ @Transactional(readOnly = true) public boolean isMutualBlocked(Long userId1, Long userId2) { if (userId1 == null || userId2 == null) { @@ -129,12 +107,6 @@ public boolean isMutualBlocked(Long userId1, Long userId2) { return userBlockRepository.existsMutualBlock(userId1, userId2); } - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ @Transactional(readOnly = true) public long getBlockedByCount(Long userId) { if (userId == null) { @@ -144,12 +116,6 @@ public long getBlockedByCount(Long userId) { return userBlockRepository.countByBlockedId(userId); } - /** - * ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ์กฐํšŒ - * - * @param userId ์‚ฌ์šฉ์ž ID - * @return ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ - */ @Transactional(readOnly = true) public long getBlockingCount(Long userId) { if (userId == null) { @@ -159,9 +125,6 @@ public long getBlockingCount(Long userId) { return userBlockRepository.countByBlockerId(userId); } - /** - * ์ฐจ๋‹จ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ - */ private void validateBlockRequest(Long blockerId, Long blockedId) { if (blockerId == null) { throw new BlockException(REQUIRED_USER_ID); From 31ea611647b83461c13f127edbc65fc1860f093a Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 01:11:59 +0900 Subject: [PATCH 11/16] feat: implement in-memory user block repository and associated service tests Signed-off-by: hgkim --- .../fixture/InMemoryUserBlockRepository.java | 107 +++++ .../block/service/BlockServiceTest.java | 424 ++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100644 src/test/java/app/bottlenote/support/block/fixture/InMemoryUserBlockRepository.java create mode 100644 src/test/java/app/bottlenote/support/block/service/BlockServiceTest.java diff --git a/src/test/java/app/bottlenote/support/block/fixture/InMemoryUserBlockRepository.java b/src/test/java/app/bottlenote/support/block/fixture/InMemoryUserBlockRepository.java new file mode 100644 index 000000000..678de7d73 --- /dev/null +++ b/src/test/java/app/bottlenote/support/block/fixture/InMemoryUserBlockRepository.java @@ -0,0 +1,107 @@ +package app.bottlenote.support.block.fixture; + +import app.bottlenote.support.block.domain.UserBlock; +import app.bottlenote.support.block.dto.response.UserBlockItem; +import app.bottlenote.support.block.repository.UserBlockRepository; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class InMemoryUserBlockRepository implements UserBlockRepository { + + private final Map userBlocks = new HashMap<>(); + private final Map userNames = new HashMap<>(); + private Long idGenerator = 1L; + + public InMemoryUserBlockRepository() { + userNames.put("1", "ํ…Œ์ŠคํŠธ์œ ์ €1"); + userNames.put("2", "ํ…Œ์ŠคํŠธ์œ ์ €2"); + userNames.put("3", "ํ…Œ์ŠคํŠธ์œ ์ €3"); + userNames.put("4", "ํ…Œ์ŠคํŠธ์œ ์ €4"); + userNames.put("5", "ํ…Œ์ŠคํŠธ์œ ์ €5"); + } + + @Override + public UserBlock save(UserBlock userBlock) { + Long id = idGenerator++; + ReflectionTestUtils.setField(userBlock, "id", id); + userBlocks.put(id, userBlock); + return userBlock; + } + + @Override + public void delete(UserBlock userBlock) { + userBlocks.remove(userBlock.getId()); + } + + @Override + public Set findBlockedUserIdsByBlockerId(Long blockerId) { + return userBlocks.values().stream() + .filter(block -> block.getBlockerId().equals(blockerId)) + .map(UserBlock::getBlockedId) + .collect(Collectors.toSet()); + } + + @Override + public boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId) { + return userBlocks.values().stream() + .anyMatch(block -> block.getBlockerId().equals(blockerId) + && block.getBlockedId().equals(blockedId)); + } + + @Override + public Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId) { + return userBlocks.values().stream() + .filter(block -> block.getBlockerId().equals(blockerId) + && block.getBlockedId().equals(blockedId)) + .findFirst(); + } + + @Override + public long countByBlockedId(Long blockedId) { + return userBlocks.values().stream() + .filter(block -> block.getBlockedId().equals(blockedId)) + .count(); + } + + @Override + public long countByBlockerId(Long blockerId) { + return userBlocks.values().stream() + .filter(block -> block.getBlockerId().equals(blockerId)) + .count(); + } + + @Override + public boolean existsMutualBlock(Long userId1, Long userId2) { + boolean user1BlocksUser2 = existsByBlockerIdAndBlockedId(userId1, userId2); + boolean user2BlocksUser1 = existsByBlockerIdAndBlockedId(userId2, userId1); + return user1BlocksUser2 && user2BlocksUser1; + } + + @Override + public List findBlockedUserItemsByBlockerId(Long blockerId) { + return userBlocks.values().stream() + .filter(block -> block.getBlockerId().equals(blockerId)) + .map(block -> new UserBlockItem( + block.getBlockedId(), + userNames.getOrDefault(block.getBlockedId().toString(), "์•Œ ์ˆ˜ ์—†๋Š” ์‚ฌ์šฉ์ž"), + LocalDateTime.now() + )) + .collect(Collectors.toList()); + } + + public void addUser(Long userId, String userName) { + userNames.put(userId.toString(), userName); + } + + public void clear() { + userBlocks.clear(); + idGenerator = 1L; + } +} diff --git a/src/test/java/app/bottlenote/support/block/service/BlockServiceTest.java b/src/test/java/app/bottlenote/support/block/service/BlockServiceTest.java new file mode 100644 index 000000000..431bf0dff --- /dev/null +++ b/src/test/java/app/bottlenote/support/block/service/BlockServiceTest.java @@ -0,0 +1,424 @@ +package app.bottlenote.support.block.service; + +import app.bottlenote.global.data.response.CollectionResponse; +import app.bottlenote.support.block.dto.response.UserBlockItem; +import app.bottlenote.support.block.exception.BlockException; +import app.bottlenote.support.block.fixture.InMemoryUserBlockRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Set; + +import static app.bottlenote.support.block.exception.BlockExceptionCode.CANNOT_BLOCK_SELF; +import static app.bottlenote.support.block.exception.BlockExceptionCode.REQUIRED_USER_ID; +import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_ALREADY_BLOCKED; +import static app.bottlenote.support.block.exception.BlockExceptionCode.USER_BLOCK_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("unit") +@DisplayName("[unit] [service] BlockService") +class BlockServiceTest { + + private BlockService blockService; + private InMemoryUserBlockRepository userBlockRepository; + + @BeforeEach + void setUp() { + userBlockRepository = new InMemoryUserBlockRepository(); + blockService = new BlockService(userBlockRepository); + + userBlockRepository.addUser(1L, "์ฐจ๋‹จ์ž"); + userBlockRepository.addUser(2L, "ํ”ผ์ฐจ๋‹จ์ž1"); + userBlockRepository.addUser(3L, "ํ”ผ์ฐจ๋‹จ์ž2"); + userBlockRepository.addUser(4L, "ํ”ผ์ฐจ๋‹จ์ž3"); + } + + @Nested + @DisplayName("์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•  ๋•Œ") + class BlockUser { + + @Test + @DisplayName("์ •์ƒ์ ์œผ๋กœ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค") + void ์ •์ƒ์ ์œผ๋กœ_์ฐจ๋‹จํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + String reason = "์ŠคํŒธ ๋ฉ”์‹œ์ง€"; + + // when + blockService.blockUser(blockerId, blockedId, reason); + + // then + assertTrue(userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)); + } + + @Test + @DisplayName("์‚ฌ์œ  ์—†์ด ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค") + void ์‚ฌ์œ _์—†์ด_์ฐจ๋‹จํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + + // when + blockService.blockUser(blockerId, blockedId); + + // then + assertTrue(userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)); + } + + @ParameterizedTest + @NullSource + @DisplayName("์ฐจ๋‹จ์ž ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ์ฐจ๋‹จ์ž_ID๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(Long blockerId) { + // given + Long blockedId = 2L; + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.blockUser(blockerId, blockedId)); + assertEquals(REQUIRED_USER_ID, exception.getExceptionCode()); + } + + @ParameterizedTest + @NullSource + @DisplayName("ํ”ผ์ฐจ๋‹จ์ž ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ํ”ผ์ฐจ๋‹จ์ž_ID๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(Long blockedId) { + // given + Long blockerId = 1L; + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.blockUser(blockerId, blockedId)); + assertEquals(REQUIRED_USER_ID, exception.getExceptionCode()); + } + + @Test + @DisplayName("์ž๊ธฐ ์ž์‹ ์„ ์ฐจ๋‹จํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ์ž๊ธฐ_์ž์‹ ์„_์ฐจ๋‹จํ•˜๋ ค๊ณ _ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + Long userId = 1L; + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.blockUser(userId, userId)); + assertEquals(CANNOT_BLOCK_SELF, exception.getExceptionCode()); + } + + @Test + @DisplayName("์ด๋ฏธ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฅผ ๋‹ค์‹œ ์ฐจ๋‹จํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ์ด๋ฏธ_์ฐจ๋‹จ๋œ_์‚ฌ์šฉ์ž๋ฅผ_๋‹ค์‹œ_์ฐจ๋‹จํ•˜๋ ค๊ณ _ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + blockService.blockUser(blockerId, blockedId); + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.blockUser(blockerId, blockedId)); + assertEquals(USER_ALREADY_BLOCKED, exception.getExceptionCode()); + } + } + + @Nested + @DisplayName("์‚ฌ์šฉ์ž ์ฐจ๋‹จ์„ ํ•ด์ œํ•  ๋•Œ") + class UnblockUser { + + @Test + @DisplayName("์ •์ƒ์ ์œผ๋กœ ์ฐจ๋‹จ์„ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋‹ค") + void ์ •์ƒ์ ์œผ๋กœ_์ฐจ๋‹จ์„_ํ•ด์ œํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + blockService.blockUser(blockerId, blockedId); + + // when + blockService.unblockUser(blockerId, blockedId); + + // then + assertFalse(userBlockRepository.existsByBlockerIdAndBlockedId(blockerId, blockedId)); + } + + @Test + @DisplayName("์ฐจ๋‹จํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋ฅผ ํ•ด์ œํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ์ฐจ๋‹จํ•˜์ง€_์•Š์€_์‚ฌ์šฉ์ž๋ฅผ_ํ•ด์ œํ•˜๋ ค๊ณ _ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.unblockUser(blockerId, blockedId)); + assertEquals(USER_BLOCK_NOT_FOUND, exception.getExceptionCode()); + } + + @ParameterizedTest + @NullSource + @DisplayName("์ฐจ๋‹จ์ž ID๊ฐ€ null์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + void ์ฐจ๋‹จ์ž_ID๊ฐ€_null์ด๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(Long blockerId) { + // given + Long blockedId = 2L; + + // when & then + BlockException exception = assertThrows(BlockException.class, + () -> blockService.unblockUser(blockerId, blockedId)); + assertEquals(REQUIRED_USER_ID, exception.getExceptionCode()); + } + } + + @Nested + @DisplayName("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ") + class GetBlockedUserIds { + + @Test + @DisplayName("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ID๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void ์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž๋“ค์˜_ID๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + blockService.blockUser(blockerId, 2L); + blockService.blockUser(blockerId, 3L); + + // when + Set blockedUserIds = blockService.getBlockedUserIds(blockerId); + + // then + assertEquals(2, blockedUserIds.size()); + assertTrue(blockedUserIds.contains(2L)); + assertTrue(blockedUserIds.contains(3L)); + } + + @Test + @DisplayName("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์œผ๋ฉด ๋นˆ Set์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž๊ฐ€_์—†์œผ๋ฉด_๋นˆ_Set์„_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long blockerId = 1L; + + // when + Set blockedUserIds = blockService.getBlockedUserIds(blockerId); + + // then + assertTrue(blockedUserIds.isEmpty()); + } + + @Test + @DisplayName("์‚ฌ์šฉ์ž ID๊ฐ€ null์ด๋ฉด ๋นˆ Set์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์‚ฌ์šฉ์ž_ID๊ฐ€_null์ด๋ฉด_๋นˆ_Set์„_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // when + Set blockedUserIds = blockService.getBlockedUserIds(null); + + // then + assertTrue(blockedUserIds.isEmpty()); + } + } + + @Nested + @DisplayName("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ๋•Œ") + class GetBlockedUserItems { + + @Test + @DisplayName("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void ์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž๋“ค์˜_์ƒ์„ธ_์ •๋ณด๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + blockService.blockUser(blockerId, 2L); + blockService.blockUser(blockerId, 3L); + + // when + CollectionResponse response = blockService.getBlockedUserItems(blockerId); + + // then + assertEquals(2L, response.getTotalCount()); + assertEquals(2, response.getItems().size()); + + UserBlockItem item1 = response.getItems().stream() + .filter(item -> item.userId().equals(2L)) + .findFirst() + .orElseThrow(); + assertEquals("ํ”ผ์ฐจ๋‹จ์ž1", item1.userName()); + assertNotNull(item1.blockedAt()); + } + + @Test + @DisplayName("์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž๊ฐ€_์—†์œผ๋ฉด_๋นˆ_๋ชฉ๋ก์„_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long blockerId = 1L; + + // when + CollectionResponse response = blockService.getBlockedUserItems(blockerId); + + // then + assertEquals(0L, response.getTotalCount()); + assertTrue(response.getItems().isEmpty()); + } + + @Test + @DisplayName("์‚ฌ์šฉ์ž ID๊ฐ€ null์ด๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์‚ฌ์šฉ์ž_ID๊ฐ€_null์ด๋ฉด_๋นˆ_๋ชฉ๋ก์„_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // when + CollectionResponse response = blockService.getBlockedUserItems(null); + + // then + assertEquals(0L, response.getTotalCount()); + assertTrue(response.getItems().isEmpty()); + } + } + + @Nested + @DisplayName("์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ๋•Œ") + class IsBlocked { + + @Test + @DisplayName("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋Š” true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์ฐจ๋‹จ๋œ_์‚ฌ์šฉ์ž๋Š”_true๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + blockService.blockUser(blockerId, blockedId); + + // when + boolean isBlocked = blockService.isBlocked(blockerId, blockedId); + + // then + assertTrue(isBlocked); + } + + @Test + @DisplayName("์ฐจ๋‹จ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋Š” false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์ฐจ๋‹จ๋˜์ง€_์•Š์€_์‚ฌ์šฉ์ž๋Š”_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long blockerId = 1L; + Long blockedId = 2L; + + // when + boolean isBlocked = blockService.isBlocked(blockerId, blockedId); + + // then + assertFalse(isBlocked); + } + + @ParameterizedTest + @NullSource + @DisplayName("์ฐจ๋‹จ์ž ID๊ฐ€ null์ด๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์ฐจ๋‹จ์ž_ID๊ฐ€_null์ด๋ฉด_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค(Long blockerId) { + // given + Long blockedId = 2L; + + // when + boolean isBlocked = blockService.isBlocked(blockerId, blockedId); + + // then + assertFalse(isBlocked); + } + } + + @Nested + @DisplayName("์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ๋•Œ") + class IsMutualBlocked { + + @Test + @DisplayName("์„œ๋กœ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์„œ๋กœ_์ฐจ๋‹จํ•œ_๊ฒฝ์šฐ_true๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long userId1 = 1L; + Long userId2 = 2L; + blockService.blockUser(userId1, userId2); + blockService.blockUser(userId2, userId1); + + // when + boolean isMutualBlocked = blockService.isMutualBlocked(userId1, userId2); + + // then + assertTrue(isMutualBlocked); + } + + @Test + @DisplayName("ํ•œ์ชฝ๋งŒ ์ฐจ๋‹จํ•œ ๊ฒฝ์šฐ false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ํ•œ์ชฝ๋งŒ_์ฐจ๋‹จํ•œ_๊ฒฝ์šฐ_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long userId1 = 1L; + Long userId2 = 2L; + blockService.blockUser(userId1, userId2); + + // when + boolean isMutualBlocked = blockService.isMutualBlocked(userId1, userId2); + + // then + assertFalse(isMutualBlocked); + } + + @Test + @DisplayName("๋‘˜ ๋‹ค ์ฐจ๋‹จํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ๋‘˜_๋‹ค_์ฐจ๋‹จํ•˜์ง€_์•Š์€_๊ฒฝ์šฐ_false๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + Long userId1 = 1L; + Long userId2 = 2L; + + // when + boolean isMutualBlocked = blockService.isMutualBlocked(userId1, userId2); + + // then + assertFalse(isMutualBlocked); + } + } + + @Nested + @DisplayName("์ฐจ๋‹จ ํ†ต๊ณ„๋ฅผ ์กฐํšŒํ•  ๋•Œ") + class BlockStatistics { + + @Test + @DisplayName("ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void ํŠน์ •_์‚ฌ์šฉ์ž๋ฅผ_์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž_์ˆ˜๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long targetUserId = 2L; + blockService.blockUser(1L, targetUserId); + blockService.blockUser(3L, targetUserId); + blockService.blockUser(4L, targetUserId); + + // when + long blockedByCount = blockService.getBlockedByCount(targetUserId); + + // then + assertEquals(3L, blockedByCount); + } + + @Test + @DisplayName("ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + void ํŠน์ •_์‚ฌ์šฉ์ž๊ฐ€_์ฐจ๋‹จํ•œ_์‚ฌ์šฉ์ž_์ˆ˜๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + Long blockerId = 1L; + blockService.blockUser(blockerId, 2L); + blockService.blockUser(blockerId, 3L); + blockService.blockUser(blockerId, 4L); + + // when + long blockingCount = blockService.getBlockingCount(blockerId); + + // then + assertEquals(3L, blockingCount); + } + + @ParameterizedTest + @ValueSource(longs = {999L}) + @NullSource + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž์˜ ํ†ต๊ณ„๋Š” 0์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์‚ฌ์šฉ์ž์˜_ํ†ต๊ณ„๋Š”_0์„_๋ฐ˜ํ™˜ํ•œ๋‹ค(Long userId) { + // when + long blockedByCount = blockService.getBlockedByCount(userId); + long blockingCount = blockService.getBlockingCount(userId); + + // then + assertEquals(0L, blockedByCount); + assertEquals(0L, blockingCount); + } + } +} From 2daaf7e5746b6c02aada3123ba6e9677799d0574 Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 01:34:00 +0900 Subject: [PATCH 12/16] feat: add user block management API documentation and tests Signed-off-by: hgkim --- .../api/support/block/user-block.adoc | 202 +++++++++++ src/docs/asciidoc/index.adoc | 2 + .../block/RestDocsBlockControllerTest.java | 317 ++++++++++++++++++ 3 files changed, 521 insertions(+) create mode 100644 src/docs/asciidoc/api/support/block/user-block.adoc create mode 100644 src/test/java/app/docs/support/block/RestDocsBlockControllerTest.java diff --git a/src/docs/asciidoc/api/support/block/user-block.adoc b/src/docs/asciidoc/api/support/block/user-block.adoc new file mode 100644 index 000000000..382467f3f --- /dev/null +++ b/src/docs/asciidoc/api/support/block/user-block.adoc @@ -0,0 +1,202 @@ +=== ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ๊ด€๋ฆฌ === + +์‚ฌ์šฉ์ž ๊ฐ„์˜ ์ฐจ๋‹จ ๊ด€๊ณ„๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. +์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ œํ•œํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค. + +==== ์ฐจ๋‹จ ๊ธฐ๋Šฅ ๊ฐœ์š” ==== + +* ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•˜์—ฌ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ฝ˜ํ…์ธ ๋‚˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +* ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋Š” ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž์˜ ์ฝ˜ํ…์ธ ์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +* ์ฐจ๋‹จ ๊ด€๊ณ„๋Š” ์ผ๋ฐฉํ–ฅ์„ฑ์ด๋ฉฐ, ์ƒํ˜ธ ์ฐจ๋‹จ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค + +''' + +==== ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ==== + +ํŠน์ • ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +POST /api/v1/blocks +---- + +[discrete] +===== ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-create/request-fields.adoc[] + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-create/response-fields.adoc[] + +์ฐจ๋‹จ ์™„๋ฃŒ ํ›„ ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +''' + +==== ์ฐจ๋‹จ ํ•ด์ œ ==== + +์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋ฅผ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +DELETE /api/v1/blocks/{blockedUserId} +---- + +[discrete] +===== ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-delete/path-parameters.adoc[] + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-delete/response-fields.adoc[] + +์ฐจ๋‹จ ํ•ด์ œ ํ›„ ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +''' + +==== ์ฐจ๋‹จ ๋ชฉ๋ก ์กฐํšŒ ==== + +ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks +---- + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-list/response-fields.adoc[] + +๊ฐ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์˜ ID, ์ด๋ฆ„, ์ฐจ๋‹จ ์‹œ๊ฐ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +''' + +==== ์ฐจ๋‹จ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก ์กฐํšŒ ==== + +๋น ๋ฅธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ์„ ์œ„ํ•ด ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID๋งŒ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks/ids +---- + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-ids/response-fields.adoc[] + +ํด๋ผ์ด์–ธํŠธ์—์„œ ๋น ๋ฅธ ํ•„ํ„ฐ๋ง์ด๋‚˜ ๊ถŒํ•œ ์ฒดํฌ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +''' + +==== ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ==== + +ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks/check/{targetUserId} +---- + +[discrete] +===== ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-check/path-parameters.adoc[] + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-check/response-fields.adoc[] + +''' + +==== ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€ ํ™•์ธ ==== + +๋‘ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋กœ ์ฐจ๋‹จํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks/mutual-check/{targetUserId} +---- + +[discrete] +===== ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-mutual-check/path-parameters.adoc[] + +[discrete] +===== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ===== + +include::{snippets}/block-mutual-check/response-fields.adoc[] + +์ƒํ˜ธ ์ฐจ๋‹จ ์ƒํƒœ์—์„œ๋Š” ์–‘์ชฝ ๋ชจ๋‘ ์ƒ๋Œ€๋ฐฉ์˜ ์ฝ˜ํ…์ธ ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + +''' + +==== ์ฐจ๋‹จ ํ†ต๊ณ„ ์กฐํšŒ ==== + +===== ๋‚˜๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ===== + +ํ˜„์žฌ ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•œ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์˜ ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks/stats/blocked-by-count +---- + +[discrete] +====== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ====== + +include::{snippets}/block-blocked-by-count/response-fields.adoc[] + +===== ๋‚ด๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜ ===== + +ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž๋“ค์˜ ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + +[source] +---- +GET /api/v1/blocks/stats/blocking-count +---- + +[discrete] +====== ์‘๋‹ต ํŒŒ๋ผ๋ฏธํ„ฐ ====== + +include::{snippets}/block-blocking-count/response-fields.adoc[] + +''' + +==== ์—๋Ÿฌ ์ฝ”๋“œ ==== + +์ฐจ๋‹จ API ์š”์ฒญ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค. + +[cols="1,2,3",options="header"] +|=== +| ์ฝ”๋“œ | ์ƒํ™ฉ | ์„ค๋ช… + +| 400 +| ์ž๊ธฐ ์ž์‹ ์„ ์ฐจ๋‹จํ•˜๋ ค๋Š” ๊ฒฝ์šฐ +| ์ž๊ธฐ ์ž์‹ ์€ ์ฐจ๋‹จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + +| 400 +| ์ด๋ฏธ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž๋ฅผ ๋‹ค์‹œ ์ฐจ๋‹จํ•˜๋ ค๋Š” ๊ฒฝ์šฐ +| ์ด๋ฏธ ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค. + +| 404 +| ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•˜๋ ค๋Š” ๊ฒฝ์šฐ +| ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + +| 404 +| ์ฐจ๋‹จํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋ฅผ ํ•ด์ œํ•˜๋ ค๋Š” ๊ฒฝ์šฐ +| ์ฐจ๋‹จ ๊ด€๊ณ„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +| 401 +| ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž +| ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. +|=== + +** ์ถ”๊ฐ€๋˜์ง€ ์•Š์€ ์—๋Ÿฌ ์ฝ”๋“œ๋Š” ์š”์ฒญ ๋ฐ”๋ž๋‹ˆ๋‹ค. +** Request validation์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ ๊ณตํ†ต ์˜ˆ์™ธ๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index b206b06c9..2a6220c7c 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -176,6 +176,8 @@ include::api/review/reply/sub-review-reply-list.adoc[] == ์ง€์› (support) ๊ด€๋ จ API +include::api/support/block/user-block.adoc[] + include::api/support/report/user-report.adoc[] include::api/support/help/help-register.adoc[] diff --git a/src/test/java/app/docs/support/block/RestDocsBlockControllerTest.java b/src/test/java/app/docs/support/block/RestDocsBlockControllerTest.java new file mode 100644 index 000000000..367d24f1a --- /dev/null +++ b/src/test/java/app/docs/support/block/RestDocsBlockControllerTest.java @@ -0,0 +1,317 @@ +package app.docs.support.block; + +import app.bottlenote.global.data.response.CollectionResponse; +import app.bottlenote.global.security.SecurityContextUtil; +import app.bottlenote.support.block.controller.BlockController; +import app.bottlenote.support.block.dto.request.BlockCreateRequest; +import app.bottlenote.support.block.dto.response.UserBlockItem; +import app.bottlenote.support.block.service.BlockService; +import app.docs.AbstractRestDocs; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("BlockController RestDocs ํ…Œ์ŠคํŠธ") +class RestDocsBlockControllerTest extends AbstractRestDocs { + + private final BlockService blockService = mock(BlockService.class); + private MockedStatic mockedSecurityUtil; + + @Override + protected Object initController() { + return new BlockController(blockService); + } + + @BeforeEach + void setup() { + mockedSecurityUtil = mockStatic(SecurityContextUtil.class); + mockedSecurityUtil.when(SecurityContextUtil::getUserIdByContext).thenReturn(Optional.of(1L)); + } + + @AfterEach + void tearDown() { + mockedSecurityUtil.close(); + } + + @DisplayName("[restdocs] ์‚ฌ์šฉ์ž๋ฅผ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void createBlockTest() throws Exception { + // given + Long currentUserId = 1L; + BlockCreateRequest request = new BlockCreateRequest(2L); + CollectionResponse response = CollectionResponse.of(1L, List.of( + new UserBlockItem(2L, "์ฐจ๋‹จ๋œ์‚ฌ์šฉ์ž", LocalDateTime.now()) + )); + + // when + doNothing().when(blockService).blockUser(currentUserId, 2L); + when(blockService.getBlockedUserItems(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(post("/api/v1/blocks") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(document("block-create", + requestFields( + fieldWithPath("blockedUserId").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จํ•  ์‚ฌ์šฉ์ž ID") + ), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data.totalCount").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ด ์ˆ˜"), + fieldWithPath("data.items").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก"), + fieldWithPath("data.items[].userId").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID"), + fieldWithPath("data.items[].userName").type(JsonFieldType.STRING).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„"), + fieldWithPath("data.items[].blockedAt").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‹œ๊ฐ") + ) + )); + } + + @DisplayName("[restdocs] ์‚ฌ์šฉ์ž ์ฐจ๋‹จ์„ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void deleteBlockTest() throws Exception { + // given + Long currentUserId = 1L; + Long blockedUserId = 2L; + CollectionResponse response = CollectionResponse.of(0L, List.of()); + + // when + doNothing().when(blockService).unblockUser(currentUserId, blockedUserId); + when(blockService.getBlockedUserItems(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(delete("/api/v1/blocks/{blockedUserId}", blockedUserId)) + .andExpect(status().isOk()) + .andDo(document("block-delete", + pathParameters( + parameterWithName("blockedUserId").description("์ฐจ๋‹จ ํ•ด์ œํ•  ์‚ฌ์šฉ์ž ID") + ), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data.totalCount").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ด ์ˆ˜"), + fieldWithPath("data.items").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก") + ) + )); + } + + @DisplayName("[restdocs] ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void getBlockedUsersTest() throws Exception { + // given + Long currentUserId = 1L; + CollectionResponse response = CollectionResponse.of(2L, List.of( + new UserBlockItem(2L, "์ฐจ๋‹จ๋œ์‚ฌ์šฉ์ž1", LocalDateTime.now()), + new UserBlockItem(3L, "์ฐจ๋‹จ๋œ์‚ฌ์šฉ์ž2", LocalDateTime.now()) + )); + + // when + when(blockService.getBlockedUserItems(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks")) + .andExpect(status().isOk()) + .andDo(document("block-list", + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data.totalCount").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ด ์ˆ˜"), + fieldWithPath("data.items").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ๋ชฉ๋ก"), + fieldWithPath("data.items[].userId").type(JsonFieldType.NUMBER).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID"), + fieldWithPath("data.items[].userName").type(JsonFieldType.STRING).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฆ„"), + fieldWithPath("data.items[].blockedAt").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‹œ๊ฐ") + ) + )); + } + + @DisplayName("[restdocs] ์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void getBlockedUserIdsTest() throws Exception { + // given + Long currentUserId = 1L; + Set response = Set.of(2L, 3L); + + // when + when(blockService.getBlockedUserIds(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks/ids")) + .andExpect(status().isOk()) + .andDo(document("block-ids", + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data").type(JsonFieldType.ARRAY).description("์ฐจ๋‹จ๋œ ์‚ฌ์šฉ์ž ID ๋ชฉ๋ก") + ) + )); + } + + @DisplayName("[restdocs] ํŠน์ • ์‚ฌ์šฉ์ž ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void checkBlockedTest() throws Exception { + // given + Long currentUserId = 1L; + Long targetUserId = 2L; + Boolean response = true; + + // when + when(blockService.isBlocked(currentUserId, targetUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks/check/{targetUserId}", targetUserId)) + .andExpect(status().isOk()) + .andDo(document("block-check", + pathParameters( + parameterWithName("targetUserId").description("์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์‚ฌ์šฉ์ž ID") + ), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data").type(JsonFieldType.BOOLEAN).description("์ฐจ๋‹จ ์—ฌ๋ถ€") + ) + )); + } + + @DisplayName("[restdocs] ์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void checkMutualBlockedTest() throws Exception { + // given + Long currentUserId = 1L; + Long targetUserId = 2L; + Boolean response = false; + + // when + when(blockService.isMutualBlocked(currentUserId, targetUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks/mutual-check/{targetUserId}", targetUserId)) + .andExpect(status().isOk()) + .andDo(document("block-mutual-check", + pathParameters( + parameterWithName("targetUserId").description("์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์‚ฌ์šฉ์ž ID") + ), + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data").type(JsonFieldType.BOOLEAN).description("์ƒํ˜ธ ์ฐจ๋‹จ ์—ฌ๋ถ€") + ) + )); + } + + @DisplayName("[restdocs] ๋‚˜๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void getBlockedByCountTest() throws Exception { + // given + Long currentUserId = 1L; + Long response = 5L; + + // when + when(blockService.getBlockedByCount(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks/stats/blocked-by-count")) + .andExpect(status().isOk()) + .andDo(document("block-blocked-by-count", + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data").type(JsonFieldType.NUMBER).description("๋‚˜๋ฅผ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜") + ) + )); + } + + @DisplayName("[restdocs] ๋‚ด๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void getBlockingCountTest() throws Exception { + // given + Long currentUserId = 1L; + Long response = 3L; + + // when + when(blockService.getBlockingCount(currentUserId)).thenReturn(response); + + // then + mockMvc.perform(get("/api/v1/blocks/stats/blocking-count")) + .andExpect(status().isOk()) + .andDo(document("block-blocking-count", + responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("์„ฑ๊ณต ์—ฌ๋ถ€").ignored(), + fieldWithPath("code").type(JsonFieldType.STRING).description("์‘๋‹ต ์ฝ”๋“œ").ignored(), + fieldWithPath("errors").type(JsonFieldType.ARRAY).description("์—๋Ÿฌ ๋ชฉ๋ก").ignored(), + fieldWithPath("meta").type(JsonFieldType.OBJECT).description("๋ฉ”ํƒ€ ์ •๋ณด").ignored(), + fieldWithPath("meta.serverVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๋ฒ„์ „").ignored(), + fieldWithPath("meta.serverEncoding").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ์ธ์ฝ”๋”ฉ").ignored(), + fieldWithPath("meta.serverResponseTime").type(JsonFieldType.ARRAY).description("์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„").ignored(), + fieldWithPath("meta.serverPathVersion").type(JsonFieldType.STRING).description("์„œ๋ฒ„ ๊ฒฝ๋กœ ๋ฒ„์ „").ignored(), + fieldWithPath("data").type(JsonFieldType.NUMBER).description("๋‚ด๊ฐ€ ์ฐจ๋‹จํ•œ ์‚ฌ์šฉ์ž ์ˆ˜") + ) + )); + } +} From 58c8f705ee546747cfc8c2600b0962efd7c8eaa9 Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 01:55:18 +0900 Subject: [PATCH 13/16] feat: update JpaUserBlockRepository with new query for blocked user items and rename test tasks Signed-off-by: hgkim --- build.gradle | 6 +-- .../repository/JpaUserBlockRepository.java | 42 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 99fac61f6..01d09f647 100644 --- a/build.gradle +++ b/build.gradle @@ -206,17 +206,17 @@ tasks.named('test') { excludeTags 'data-jpa-test', 'integration' } } -tasks.register('integrationTest', Test) { +tasks.register('integration_test', Test) { useJUnitPlatform { includeTags 'integration' } } -tasks.register('unitTest', Test) { +tasks.register('unit_test', Test) { useJUnitPlatform { includeTags 'unit' } } -tasks.register('check_rule', Test) { +tasks.register('check_rule_test', Test) { useJUnitPlatform { includeTags 'rule' } diff --git a/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java index fe7a4751c..5df8fafdb 100644 --- a/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java +++ b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java @@ -1,36 +1,44 @@ package app.bottlenote.support.block.repository; import app.bottlenote.support.block.domain.UserBlock; +import app.bottlenote.support.block.dto.response.UserBlockItem; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import java.util.Set; @Repository public interface JpaUserBlockRepository extends UserBlockRepository, JpaRepository { - @Override - @Query("SELECT ub.blockedId FROM userBlock ub WHERE ub.blockerId = :blockerId") - Set findBlockedUserIdsByBlockerId(@Param("blockerId") Long blockerId); + @Override + @Query("SELECT ub.blockedId FROM userBlock ub WHERE ub.blockerId = :blockerId") + Set findBlockedUserIdsByBlockerId(@Param("blockerId") Long blockerId); - @Override - boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + @Override + boolean existsByBlockerIdAndBlockedId(Long blockerId, Long blockedId); - @Override - Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId); + @Override + Optional findByBlockerIdAndBlockedId(Long blockerId, Long blockedId); - @Override - long countByBlockedId(Long blockedId); + @Override + long countByBlockedId(Long blockedId); - @Override - long countByBlockerId(Long blockerId); + @Override + long countByBlockerId(Long blockerId); - @Override - @Query("SELECT COUNT(ub) = 2 FROM userBlock ub " + - "WHERE (ub.blockerId = :userId1 AND ub.blockedId = :userId2) " + - "OR (ub.blockerId = :userId2 AND ub.blockedId = :userId1)") - boolean existsMutualBlock(@Param("userId1") Long userId1, @Param("userId2") Long userId2); -} \ No newline at end of file + @Override + @Query("SELECT COUNT(ub) = 2 FROM userBlock ub " + + "WHERE (ub.blockerId = :userId1 AND ub.blockedId = :userId2) " + + "OR (ub.blockerId = :userId2 AND ub.blockedId = :userId1)") + boolean existsMutualBlock(@Param("userId1") Long userId1, @Param("userId2") Long userId2); + + @Override + @Query("SELECT new app.bottlenote.support.block.dto.response.UserBlockItem(u.id, u.nickName, ub.createAt) " + + "FROM userBlock ub JOIN users u ON ub.blockedId = u.id " + + "WHERE ub.blockerId = :blockerId") + List findBlockedUserItemsByBlockerId(@Param("blockerId") Long blockerId); +} From 9dfa2e0a645e0fd88bfe16012a9c9f47fd113fab Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 02:00:17 +0900 Subject: [PATCH 14/16] feat: refactor JPQL queries in JpaUserBlockRepository for improved readability Signed-off-by: hgkim --- .../repository/JpaUserBlockRepository.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java index 5df8fafdb..0d6db8d36 100644 --- a/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java +++ b/src/main/java/app/bottlenote/support/block/repository/JpaUserBlockRepository.java @@ -15,7 +15,9 @@ public interface JpaUserBlockRepository extends UserBlockRepository, JpaRepository { @Override - @Query("SELECT ub.blockedId FROM userBlock ub WHERE ub.blockerId = :blockerId") + @Query(""" + SELECT ub.blockedId FROM userBlock ub WHERE ub.blockerId = :blockerId + """) Set findBlockedUserIdsByBlockerId(@Param("blockerId") Long blockerId); @Override @@ -31,14 +33,18 @@ public interface JpaUserBlockRepository extends UserBlockRepository, JpaReposito long countByBlockerId(Long blockerId); @Override - @Query("SELECT COUNT(ub) = 2 FROM userBlock ub " + - "WHERE (ub.blockerId = :userId1 AND ub.blockedId = :userId2) " + - "OR (ub.blockerId = :userId2 AND ub.blockedId = :userId1)") + @Query(""" + SELECT COUNT(ub) = 2 FROM userBlock ub + WHERE (ub.blockerId = :userId1 AND ub.blockedId = :userId2) + OR (ub.blockerId = :userId2 AND ub.blockedId = :userId1) + """) boolean existsMutualBlock(@Param("userId1") Long userId1, @Param("userId2") Long userId2); @Override - @Query("SELECT new app.bottlenote.support.block.dto.response.UserBlockItem(u.id, u.nickName, ub.createAt) " + - "FROM userBlock ub JOIN users u ON ub.blockedId = u.id " + - "WHERE ub.blockerId = :blockerId") + @Query(""" + SELECT new app.bottlenote.support.block.dto.response.UserBlockItem(u.id, u.nickName, ub.createAt) + FROM userBlock ub JOIN users u ON ub.blockedId = u.id + WHERE ub.blockerId = :blockerId + """) List findBlockedUserItemsByBlockerId(@Param("blockerId") Long blockerId); } From c13aadbc8c393fe2ae119542f357042cc64d12c6 Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 02:20:50 +0900 Subject: [PATCH 15/16] feat: standardize CI pipeline step names and improve test execution order Signed-off-by: hgkim --- .../workflows/dev_continuous_integration.yml | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dev_continuous_integration.yml b/.github/workflows/dev_continuous_integration.yml index a0e058298..e7511b7ed 100644 --- a/.github/workflows/dev_continuous_integration.yml +++ b/.github/workflows/dev_continuous_integration.yml @@ -1,4 +1,4 @@ -name: Dev CI Pipeline +name: dev ci pipeline on: workflow_dispatch: @@ -32,19 +32,19 @@ jobs: run: | echo "Branch: ${{ github.event.pull_request.base.ref || github.ref_name }}" - - name: Checkout Code (์ฝ”๋“œ ์ฒดํฌ์•„์›ƒ) + - name: checkout code uses: actions/checkout@v4 with: submodules: true token: ${{ secrets.GIT_ACCESS_TOKEN }} - - name: Setup JDK (์ž๋ฐ” ์„ค์ •) + - name: setup jdk uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - - name: Setup Gradle Cache (๊ทธ๋ž˜๋“ค ์บ์‹œ ์„ค์ •) + - name: setup gradle cache uses: actions/cache@v3 with: path: ~/.gradle/caches @@ -52,28 +52,29 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Install Dependencies (์ข…์†์„ฑ ์„ค์น˜) + - name: install dependencies run: ./gradlew dependencies - - name: Run Integration Tests (ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰) - run: ./gradlew clean integrationTest + - name: run unit tests + run: ./gradlew unit_test - - name: Run Unit Tests (๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์‹คํ–‰) - run: ./gradlew unitTest + - name: run rules tests + run: ./gradlew check_rule_test - - name: Build Project (ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ) - run: ./gradlew build + - name: run integration tests + run: ./gradlew integration_test - - name: Store Test Results (ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์ €์žฅ) + - name: build project + run: ./gradlew build -x test --build-cache --parallel + + - name: store test results if: always() uses: actions/upload-artifact@v4 with: name: test-results - path: | - build/reports/tests/ - build/reports/integration-tests/ + path: build/reports/ - - name: Publish Test Report (ํ…Œ์ŠคํŠธ ๋ณด๊ณ ์„œ ๋ฐœํ–‰) + - name: publish test report uses: mikepenz/action-junit-report@v3 if: always() with: From c1f813255fbb71eacb95415055f70fee9ba456c4 Mon Sep 17 00:00:00 2001 From: hgkim Date: Tue, 1 Jul 2025 21:13:00 +0900 Subject: [PATCH 16/16] feat: exclude asciidoctor from build step in CI configuration Signed-off-by: hgkim --- .github/workflows/dev_continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev_continuous_integration.yml b/.github/workflows/dev_continuous_integration.yml index e7511b7ed..113f35a1d 100644 --- a/.github/workflows/dev_continuous_integration.yml +++ b/.github/workflows/dev_continuous_integration.yml @@ -65,7 +65,7 @@ jobs: run: ./gradlew integration_test - name: build project - run: ./gradlew build -x test --build-cache --parallel + run: ./gradlew build -x test -x asciidoctor --build-cache --parallel - name: store test results if: always()