Skip to content

Commit 846bf75

Browse files
authored
Merge pull request #20 from MaxedOut-Muneo/feat/#19
[feat] 사용자 관리 API 구현
2 parents 5b4463f + 0e6cf40 commit 846bf75

22 files changed

Lines changed: 1541 additions & 8 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.example.muneoserver.domain.user.controller;
2+
3+
import com.example.muneoserver.domain.user.controller.docs.AdminAuthControllerDocs;
4+
import com.example.muneoserver.domain.user.dto.LoginRequest;
5+
import com.example.muneoserver.domain.user.dto.UserResponse;
6+
import com.example.muneoserver.domain.user.service.UserFacadeService;
7+
import com.example.muneoserver.global.dto.ApiResponse;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import jakarta.validation.Valid;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@RestController
18+
@RequestMapping("/api/v1/admin")
19+
@RequiredArgsConstructor
20+
public class AdminAuthController implements AdminAuthControllerDocs {
21+
22+
private final UserFacadeService userFacadeService;
23+
24+
@Override
25+
@PostMapping("/login")
26+
public ResponseEntity<ApiResponse<UserResponse>> login(
27+
@Valid @RequestBody LoginRequest request,
28+
HttpServletResponse response
29+
) {
30+
return ResponseEntity.ok(ApiResponse.success(userFacadeService.adminLogin(request, response), "관리자 로그인에 성공했습니다."));
31+
}
32+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.example.muneoserver.domain.user.controller;
2+
3+
import com.example.muneoserver.domain.user.controller.docs.AdminUserControllerDocs;
4+
import com.example.muneoserver.domain.user.domain.AuthProvider;
5+
import com.example.muneoserver.domain.user.domain.UserRole;
6+
import com.example.muneoserver.domain.user.dto.admin.AdminUserResponse;
7+
import com.example.muneoserver.domain.user.dto.admin.AdminUserRoleUpdateRequest;
8+
import com.example.muneoserver.domain.user.dto.admin.AdminUserSearchCondition;
9+
import com.example.muneoserver.domain.user.dto.admin.AdminUserUpdateRequest;
10+
import com.example.muneoserver.domain.user.service.admin.AdminUserService;
11+
import com.example.muneoserver.global.dto.ApiResponse;
12+
import com.example.muneoserver.global.dto.PageResponse;
13+
import com.example.muneoserver.global.security.auth.AuthUser;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.data.domain.PageRequest;
16+
import org.springframework.data.domain.Sort;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.validation.annotation.Validated;
20+
import org.springframework.web.bind.annotation.DeleteMapping;
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.PatchMapping;
23+
import org.springframework.web.bind.annotation.PathVariable;
24+
import org.springframework.web.bind.annotation.RequestBody;
25+
import org.springframework.web.bind.annotation.RequestMapping;
26+
import org.springframework.web.bind.annotation.RequestParam;
27+
import org.springframework.web.bind.annotation.RestController;
28+
29+
@Validated
30+
@RestController
31+
@RequestMapping("/api/v1/admin/users")
32+
@RequiredArgsConstructor
33+
public class AdminUserController implements AdminUserControllerDocs {
34+
35+
private final AdminUserService adminUserService;
36+
37+
@Override
38+
@GetMapping
39+
public ResponseEntity<ApiResponse<PageResponse<AdminUserResponse>>> getUsers(
40+
@AuthenticationPrincipal AuthUser authUser,
41+
@RequestParam(defaultValue = "0") int page,
42+
@RequestParam(defaultValue = "20") int size,
43+
@RequestParam(required = false) String keyword,
44+
@RequestParam(required = false) UserRole role,
45+
@RequestParam(required = false) Boolean deleted,
46+
@RequestParam(required = false) AuthProvider authProvider,
47+
@RequestParam(required = false) Boolean emailVerified,
48+
@RequestParam(required = false) Boolean profileCompleted
49+
) {
50+
AdminUserSearchCondition condition = new AdminUserSearchCondition(
51+
keyword,
52+
role,
53+
deleted,
54+
authProvider,
55+
emailVerified,
56+
profileCompleted
57+
);
58+
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));
59+
60+
return ResponseEntity.ok(ApiResponse.success(adminUserService.getUsers(authUser, condition, pageRequest), "사용자 목록 조회에 성공했습니다."));
61+
}
62+
63+
@Override
64+
@GetMapping("/{userId}")
65+
public ResponseEntity<ApiResponse<AdminUserResponse>> getUser(
66+
@AuthenticationPrincipal AuthUser authUser,
67+
@PathVariable Long userId
68+
) {
69+
return ResponseEntity.ok(ApiResponse.success(adminUserService.getUser(authUser, userId), "사용자 상세 조회에 성공했습니다."));
70+
}
71+
72+
@Override
73+
@PatchMapping("/{userId}")
74+
public ResponseEntity<ApiResponse<AdminUserResponse>> updateUser(
75+
@AuthenticationPrincipal AuthUser authUser,
76+
@PathVariable Long userId,
77+
@RequestBody AdminUserUpdateRequest request
78+
) {
79+
return ResponseEntity.ok(ApiResponse.success(adminUserService.updateUser(authUser, userId, request), "사용자 정보 수정에 성공했습니다."));
80+
}
81+
82+
@Override
83+
@PatchMapping("/{userId}/role")
84+
public ResponseEntity<ApiResponse<AdminUserResponse>> updateUserRole(
85+
@AuthenticationPrincipal AuthUser authUser,
86+
@PathVariable Long userId,
87+
@RequestBody AdminUserRoleUpdateRequest request
88+
) {
89+
return ResponseEntity.ok(ApiResponse.success(adminUserService.updateUserRole(authUser, userId, request), "사용자 권한 변경에 성공했습니다."));
90+
}
91+
92+
@Override
93+
@DeleteMapping("/{userId}")
94+
public ResponseEntity<ApiResponse<Void>> deleteUser(
95+
@AuthenticationPrincipal AuthUser authUser,
96+
@PathVariable Long userId
97+
) {
98+
adminUserService.deleteUser(authUser, userId);
99+
return ResponseEntity.ok(ApiResponse.success(null, "사용자 탈퇴 처리에 성공했습니다."));
100+
}
101+
102+
@Override
103+
@PatchMapping("/{userId}/restore")
104+
public ResponseEntity<ApiResponse<AdminUserResponse>> restoreUser(
105+
@AuthenticationPrincipal AuthUser authUser,
106+
@PathVariable Long userId
107+
) {
108+
return ResponseEntity.ok(ApiResponse.success(adminUserService.restoreUser(authUser, userId), "사용자 복구에 성공했습니다."));
109+
}
110+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package com.example.muneoserver.domain.user.controller.docs;
2+
3+
import com.example.muneoserver.domain.user.dto.LoginRequest;
4+
import com.example.muneoserver.domain.user.dto.UserResponse;
5+
import com.example.muneoserver.global.dto.ApiResponse;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.headers.Header;
8+
import io.swagger.v3.oas.annotations.media.Content;
9+
import io.swagger.v3.oas.annotations.media.ExampleObject;
10+
import io.swagger.v3.oas.annotations.media.Schema;
11+
import io.swagger.v3.oas.annotations.Parameter;
12+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
13+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
14+
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
15+
import io.swagger.v3.oas.annotations.tags.Tag;
16+
import jakarta.servlet.http.HttpServletResponse;
17+
import org.springframework.http.MediaType;
18+
import org.springframework.http.ResponseEntity;
19+
20+
@Tag(name = "Admin Auth", description = "관리자 인증 관련 API")
21+
public interface AdminAuthControllerDocs {
22+
23+
@Operation(
24+
summary = "관리자 로그인",
25+
description = "이메일과 비밀번호로 로그인합니다. UserRole이 ADMIN인 로컬 계정만 로그인할 수 있으며, "
26+
+ "성공 시 HttpOnly Cookie(access_token, refresh_token)를 발급합니다."
27+
)
28+
@SecurityRequirements
29+
@RequestBody(
30+
required = true,
31+
description = "관리자 로그인 요청 정보",
32+
content = @Content(
33+
mediaType = MediaType.APPLICATION_JSON_VALUE,
34+
schema = @Schema(implementation = LoginRequest.class),
35+
examples = @ExampleObject(
36+
name = "관리자 로그인 요청",
37+
value = """
38+
{
39+
"email": "admin@example.com",
40+
"password": "password1234"
41+
}
42+
"""
43+
)
44+
)
45+
)
46+
@ApiResponses(value = {
47+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
48+
responseCode = "200",
49+
description = "관리자 로그인 성공",
50+
headers = {
51+
@Header(name = "Set-Cookie", description = "access_token HttpOnly Cookie"),
52+
@Header(name = "Set-Cookie", description = "refresh_token HttpOnly Cookie")
53+
},
54+
content = @Content(
55+
mediaType = MediaType.APPLICATION_JSON_VALUE,
56+
schema = @Schema(implementation = ApiResponse.class),
57+
examples = @ExampleObject(
58+
name = "관리자 로그인 성공",
59+
value = """
60+
{
61+
"success": true,
62+
"status": 200,
63+
"code": "SUCCESS",
64+
"timestamp": "2026-05-17T08:00:00Z",
65+
"message": "관리자 로그인에 성공했습니다.",
66+
"result": {
67+
"id": 1,
68+
"email": "admin@example.com",
69+
"name": "관리자",
70+
"phoneNumber": "01012345678",
71+
"birthDate": "2000-01-01",
72+
"authProvider": "LOCAL",
73+
"profileCompleted": true,
74+
"role": "ADMIN"
75+
}
76+
}
77+
"""
78+
)
79+
)
80+
),
81+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
82+
responseCode = "400",
83+
description = "요청 값 오류 또는 요청 JSON 형식 오류",
84+
content = @Content(
85+
mediaType = MediaType.APPLICATION_JSON_VALUE,
86+
schema = @Schema(implementation = ApiResponse.class),
87+
examples = {
88+
@ExampleObject(
89+
name = "요청 값 오류",
90+
value = """
91+
{
92+
"success": false,
93+
"status": 400,
94+
"code": "VALIDATION_FAILED",
95+
"timestamp": "2026-05-17T08:00:00Z",
96+
"message": "요청 값이 올바르지 않습니다.",
97+
"error": {
98+
"email": "올바른 이메일 형식이어야 합니다.",
99+
"password": "비밀번호는 필수입니다."
100+
}
101+
}
102+
"""
103+
),
104+
@ExampleObject(
105+
name = "요청 JSON 형식 오류",
106+
value = """
107+
{
108+
"success": false,
109+
"status": 400,
110+
"code": "INVALID_REQUEST_FORMAT",
111+
"timestamp": "2026-05-17T08:00:00Z",
112+
"message": "올바르지 않은 요청 형식입니다."
113+
}
114+
"""
115+
)
116+
}
117+
)
118+
),
119+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
120+
responseCode = "401",
121+
description = "이메일 또는 비밀번호 불일치",
122+
content = @Content(
123+
mediaType = MediaType.APPLICATION_JSON_VALUE,
124+
schema = @Schema(implementation = ApiResponse.class),
125+
examples = @ExampleObject(
126+
name = "로그인 정보 불일치",
127+
value = """
128+
{
129+
"success": false,
130+
"status": 401,
131+
"code": "INVALID_LOGIN_INFO",
132+
"timestamp": "2026-05-17T08:00:00Z",
133+
"message": "이메일 또는 비밀번호가 올바르지 않습니다."
134+
}
135+
"""
136+
)
137+
)
138+
),
139+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
140+
responseCode = "403",
141+
description = "관리자 계정이 아님",
142+
content = @Content(
143+
mediaType = MediaType.APPLICATION_JSON_VALUE,
144+
schema = @Schema(implementation = ApiResponse.class),
145+
examples = @ExampleObject(
146+
name = "관리자 로그인 권한 없음",
147+
value = """
148+
{
149+
"success": false,
150+
"status": 403,
151+
"code": "ADMIN_LOGIN_FORBIDDEN",
152+
"timestamp": "2026-05-17T08:00:00Z",
153+
"message": "관리자 계정만 로그인할 수 있습니다."
154+
}
155+
"""
156+
)
157+
)
158+
),
159+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
160+
responseCode = "404",
161+
description = "삭제된 사용자 계정",
162+
content = @Content(
163+
mediaType = MediaType.APPLICATION_JSON_VALUE,
164+
schema = @Schema(implementation = ApiResponse.class),
165+
examples = @ExampleObject(
166+
name = "사용자 없음",
167+
value = """
168+
{
169+
"success": false,
170+
"status": 404,
171+
"code": "USER_NOT_FOUND",
172+
"timestamp": "2026-05-17T08:00:00Z",
173+
"message": "사용자를 찾을 수 없습니다."
174+
}
175+
"""
176+
)
177+
)
178+
),
179+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
180+
responseCode = "500",
181+
description = "서버 내부 오류",
182+
content = @Content(
183+
mediaType = MediaType.APPLICATION_JSON_VALUE,
184+
schema = @Schema(implementation = ApiResponse.class),
185+
examples = @ExampleObject(
186+
name = "서버 내부 오류",
187+
value = """
188+
{
189+
"success": false,
190+
"status": 500,
191+
"code": "INTERNAL_SERVER_ERROR",
192+
"timestamp": "2026-05-17T08:00:00Z",
193+
"message": "서버 내부 에러"
194+
}
195+
"""
196+
)
197+
)
198+
)
199+
})
200+
ResponseEntity<ApiResponse<UserResponse>> login(
201+
LoginRequest request,
202+
@Parameter(hidden = true) HttpServletResponse response
203+
);
204+
}

0 commit comments

Comments
 (0)