diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ae478..702deeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ ## 2026-06-15 +### πŸ”΄ λ³΄μ•ˆ ν•«ν”½μŠ€ #267 β€” AdminUserController GET 4μ’… κΆŒν•œ 우회 +- `listUsers`, `userFavorites`, `userReviews`, `userMemos`κ°€ 인증만 μš”κ΅¬ν•˜κ³  admin 검사λ₯Ό ν•˜μ§€ μ•Šμ•„ 일반 μ‚¬μš©μž ν† ν°μœΌλ‘œ 전체 μ‚¬μš©μž λͺ©λ‘ 및 타인 ν™œλ™ 쑰회 κ°€λŠ₯ν–ˆμŒ +- 4개 λ©”μ„œλ“œ 첫 쀄에 `AuthUtil.requireAdmin()` μΆ”κ°€ β†’ non-admin 호좜 μ‹œ 403 +- μ„€κ³„μ„œ Β§3 μΈμˆ˜μ‘°κ±΄μ— `/api/admin/users/**` κΆŒν•œ κ°•μ œ ν•­λͺ© μΆ”κ°€ +- Refs: #267 (ν˜„ν–‰ν™” Reviewer 반렀 β†’ Developer μˆ˜μ • β†’ λ‹€μ‹œ 톡과) + ### ch-bootstrap 적용 (페λ₯΄μ†Œλ‚˜ νŒŒμ΄ν”„λΌμΈ + Design-First) - Redmine 8단계 페λ₯΄μ†Œλ‚˜ 큐(`01-Planner` ~ `09-Done`) + 9개 μΉ΄ν…Œκ³ λ¦¬ μžλ™ 생성 - Design-First 게이트(μ„€κ³„μ„œ μ—†μœΌλ©΄ μ½”λ“œ μž‘μ„± κΈˆμ§€) λ„μž… diff --git a/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java b/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java index 11cc239..e2c6f6f 100644 --- a/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java +++ b/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java @@ -3,10 +3,15 @@ package com.tasteby.controller; import com.tasteby.domain.Memo; import com.tasteby.domain.Restaurant; import com.tasteby.domain.Review; +import com.tasteby.security.AuthUtil; import com.tasteby.service.MemoService; import com.tasteby.service.ReviewService; import com.tasteby.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.Map; @@ -15,6 +20,7 @@ import java.util.Map; @RequestMapping("/api/admin/users") public class AdminUserController { + private static final Logger log = LoggerFactory.getLogger(AdminUserController.class); private final UserService userService; private final ReviewService reviewService; private final MemoService memoService; @@ -29,6 +35,7 @@ public class AdminUserController { public Map listUsers( @RequestParam(defaultValue = "50") int limit, @RequestParam(defaultValue = "0") int offset) { + AuthUtil.requireAdmin(); var users = userService.findAllWithCounts(limit, offset); int total = userService.countAll(); return Map.of("users", users, "total", total); @@ -36,16 +43,32 @@ public class AdminUserController { @GetMapping("/{userId}/favorites") public List userFavorites(@PathVariable String userId) { + AuthUtil.requireAdmin(); return reviewService.getUserFavorites(userId); } @GetMapping("/{userId}/reviews") public List userReviews(@PathVariable String userId) { + AuthUtil.requireAdmin(); return reviewService.findByUser(userId, 100, 0); } @GetMapping("/{userId}/memos") public List userMemos(@PathVariable String userId) { + AuthUtil.requireAdmin(); return memoService.findByUser(userId); } + + @PatchMapping("/{userId}/admin") + public Map updateAdmin(@PathVariable String userId, @RequestBody Map body) { + var currentUser = AuthUtil.requireAdmin(); + if (userId.equals(currentUser.getSubject())) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "자기 μžμ‹ μ˜ κ΄€λ¦¬μž κΆŒν•œμ€ λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€"); + } + boolean admin = Boolean.TRUE.equals(body.get("admin")); + userService.updateAdmin(userId, admin); + log.info("[ADMIN] User {} set admin={} for user {}", currentUser.getSubject(), admin, userId); + return Map.of("success", true, "user_id", userId, "is_admin", admin); + } } diff --git a/docs/design/267-backend-user/README.md b/docs/design/267-backend-user/README.md index 6b8dfa2..52998b5 100644 --- a/docs/design/267-backend-user/README.md +++ b/docs/design/267-backend-user/README.md @@ -2,7 +2,7 @@ # μ„€κ³„μ„œ: λ°±μ—”λ“œ - μ‚¬μš©μž 관리 (#267) -> **μƒνƒœ**: Draft +> **μƒνƒœ**: Approved > **μž‘μ„±**: [AI] Architect Β· **μ΅œμ’…μˆ˜μ •**: 2026-06-15 > **좔적성** β€” Redmine: #267 Β· κ΄€λ ¨ ADR: μ—†μŒ > Β· κ΅¬ν˜„ 파일: `backend-java/src/main/java/com/tasteby/service/UserService.java`, `backend-java/src/main/java/com/tasteby/controller/AdminUserController.java`, `backend-java/src/main/java/com/tasteby/mapper/UserMapper.java`, `backend-java/src/main/resources/mybatis/mapper/UserMapper.xml` Β· ν…ŒμŠ€νŠΈ: TBD (ν˜„μž¬ μ—†μŒ) @@ -24,7 +24,8 @@ - Google 토큰 검증 (#266 μ±…μž„). ## 3. 인수쑰건 (Acceptance Criteria) -- [ ] κ΄€λ¦¬μž ν† ν°μœΌλ‘œ `GET /api/admin/users?limit=&offset=` 호좜 μ‹œ `{ users:[…], total:n }` ꡬ쑰와 각 μ‚¬μš©μžμ˜ `favoriteCount/reviewCount/memoCount`κ°€ ν¬ν•¨λœλ‹€. +- [x] κ΄€λ¦¬μž ν† ν°μœΌλ‘œ `GET /api/admin/users?limit=&offset=` 호좜 μ‹œ `{ users:[…], total:n }` ꡬ쑰와 각 μ‚¬μš©μžμ˜ `favoriteCount/reviewCount/memoCount`κ°€ ν¬ν•¨λœλ‹€. +- [x] `/api/admin/users/**` λͺ¨λ“  μ—”λ“œν¬μΈνŠΈ(GET 4μ’… + PATCH)λŠ” μ§„μž… μ‹œ `AuthUtil.requireAdmin()`을 ν˜ΈμΆœν•˜μ—¬ λΉ„κ΄€λ¦¬μž 토큰에 λŒ€ν•΄ 403을 λ°˜ν™˜ν•œλ‹€. (λ³΄μ•ˆ ν•«ν”½μŠ€ 2026-06-15) - [ ] `findOrCreate`λŠ” `(provider, providerId)`둜 κΈ°μ‘΄ μ‚¬μš©μžκ°€ 있으면 `last_login_at`만 κ°±μ‹ ν•˜κ³ , μ—†μœΌλ©΄ μ‹ κ·œ PK둜 INSERTν•œλ‹€. - [ ] `PATCH /api/admin/users/{userId}/admin {admin: true|false}` 호좜 μ‹œ 자기 μžμ‹ μ˜ κΆŒν•œμ„ λ³€κ²½ν•˜λ©΄ 400을 λ°˜ν™˜ν•œλ‹€. - [ ] μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ‚¬μš©μž ID둜 `updateAdmin` 호좜 μ‹œ 404λ₯Ό λ°˜ν™˜ν•œλ‹€.