diff --git a/module-app/src/main/resources/logback-spring.xml b/module-app/src/main/resources/logback-spring.xml
index 53a5324f..72d9f8df 100644
--- a/module-app/src/main/resources/logback-spring.xml
+++ b/module-app/src/main/resources/logback-spring.xml
@@ -1,16 +1,15 @@
+
+ converterClass="co.kr.pinhouse.common.logging.SanitizedMessageConverter"/>
- %d{yyyy-MM-dd HH:mm:ss.SSS} level=%-5level [%thread] [%X{traceId:-},%X{spanId:-}] %logger{36} -
- %sanitizedMsg%n
-
+ %d{yyyy-MM-dd HH:mm:ss.SSS} level=%-5level [%thread] [%X{traceId:-},%X{spanId:-}] %logger{36} - %sanitizedMsg%n
@@ -29,3 +28,4 @@
+
diff --git a/module-common/src/main/java/co/kr/pinhouse/common/exception/code/UserErrorCode.java b/module-common/src/main/java/co/kr/pinhouse/common/exception/code/UserErrorCode.java
index 60aee473..08cb7e1f 100644
--- a/module-common/src/main/java/co/kr/pinhouse/common/exception/code/UserErrorCode.java
+++ b/module-common/src/main/java/co/kr/pinhouse/common/exception/code/UserErrorCode.java
@@ -28,6 +28,7 @@ public enum UserErrorCode implements ErrorCode {
// ========================
// 403 Forbidden
// ========================
+ FORBIDDEN_SELF_ROLE_CHANGE(403_200, HttpStatus.FORBIDDEN, "자신의 권한은 변경할 수 없습니다."),
// ========================
// 404 Not Found
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/audit/domain/entity/AdminAuditTargetType.java b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/audit/domain/entity/AdminAuditTargetType.java
index d15b3986..c3da509e 100644
--- a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/audit/domain/entity/AdminAuditTargetType.java
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/audit/domain/entity/AdminAuditTargetType.java
@@ -2,6 +2,7 @@
public enum AdminAuditTargetType {
NOTICE,
+ USER,
CS_INQUIRY,
ADVERTISEMENT
}
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/dto/request/UpdateAdminUserRoleRequest.java b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/dto/request/UpdateAdminUserRoleRequest.java
new file mode 100644
index 00000000..02242a32
--- /dev/null
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/dto/request/UpdateAdminUserRoleRequest.java
@@ -0,0 +1,9 @@
+package co.kr.pinhouse.domain.admin.user.application.dto.request;
+
+import co.kr.pinhouse.domain.user.domain.entity.Role;
+import jakarta.validation.constraints.NotNull;
+
+public record UpdateAdminUserRoleRequest(
+ @NotNull Role role
+) {
+}
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/service/AdminUserService.java b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/service/AdminUserService.java
index a0df83aa..bb275f99 100644
--- a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/service/AdminUserService.java
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/service/AdminUserService.java
@@ -11,14 +11,20 @@
import co.kr.pinhouse.common.response.CustomException;
import co.kr.pinhouse.common.response.pageable.SliceRequest;
import co.kr.pinhouse.common.response.pageable.SliceResponse;
+import co.kr.pinhouse.domain.admin.application.usecase.AdminSessionUseCase;
+import co.kr.pinhouse.domain.admin.audit.application.usecase.AdminAuditLogUseCase;
+import co.kr.pinhouse.domain.admin.audit.domain.entity.AdminAuditActionType;
+import co.kr.pinhouse.domain.admin.audit.domain.entity.AdminAuditTargetType;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
import co.kr.pinhouse.domain.admin.user.application.usecase.AdminUserUseCase;
import co.kr.pinhouse.domain.diagnostic.diagnosis.domain.repository.DiagnosisJpaRepository;
import co.kr.pinhouse.domain.like.domain.LikeJpaRepository;
import co.kr.pinhouse.domain.pinpoint.domain.repository.PinPointMongoRepository;
+import co.kr.pinhouse.domain.user.domain.entity.Role;
import co.kr.pinhouse.domain.user.domain.entity.User;
import co.kr.pinhouse.domain.user.domain.repository.UserJpaRepository;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@Service
@@ -29,6 +35,8 @@ public class AdminUserService implements AdminUserUseCase {
private final LikeJpaRepository likeRepository;
private final PinPointMongoRepository pinPointRepository;
private final DiagnosisJpaRepository diagnosisRepository;
+ private final AdminSessionUseCase adminSessionService;
+ private final AdminAuditLogUseCase adminAuditLogService;
// =================
// 퍼블릭 로직
@@ -57,23 +65,72 @@ public SliceResponse getUsers(String keyword, SliceReq
@Transactional(readOnly = true)
@Override
public AdminUserDetailResponse getUser(UUID userId) {
- User user = userRepository.findWithFacilityTypesById(userId)
- .orElseThrow(() -> new CustomException(UserErrorCode.NOT_FOUND_USER));
+ User user = loadUserWithFacilityTypes(userId);
+
+ return toAdminUserDetailResponse(user);
+ }
+
+ /// 관리자 유저 권한 변경
+ @Transactional
+ @Override
+ public AdminUserDetailResponse updateUserRole(
+ UUID userId,
+ Role role,
+ UUID adminId,
+ HttpServletRequest httpServletRequest
+ ) {
+ adminSessionService.loadAdmin(adminId);
+
+ if (adminId.equals(userId)) {
+ throw new CustomException(UserErrorCode.FORBIDDEN_SELF_ROLE_CHANGE);
+ }
+
+ User user = loadUserWithFacilityTypes(userId);
+ AdminUserDetailResponse before = toAdminUserDetailResponse(user);
+
+ if (user.getRole() == role) {
+ return before;
+ }
+
+ user.changeRole(role);
+ AdminUserDetailResponse after = toAdminUserDetailResponse(user);
+
+ adminAuditLogService.log(
+ adminId,
+ AdminAuditActionType.UPDATE,
+ AdminAuditTargetType.USER,
+ userId.toString(),
+ "유저 권한 변경",
+ before,
+ after,
+ httpServletRequest
+ );
+
+ return after;
+ }
+
+ // =================
+ // 내부 로직
+ // =================
+ /// 관리자 유저 상세 응답 변환
+ private AdminUserDetailResponse toAdminUserDetailResponse(User user) {
return AdminUserDetailResponse.of(
user,
maskName(user.getName()),
maskEmail(user.getEmail()),
maskPhone(user.getPhoneNumber()),
- likeRepository.countByUser_Id(userId),
- pinPointRepository.countByUserId(userId.toString()),
- diagnosisRepository.countByUser_Id(userId)
+ likeRepository.countByUser_Id(user.getId()),
+ pinPointRepository.countByUserId(user.getId().toString()),
+ diagnosisRepository.countByUser_Id(user.getId())
);
}
- // =================
- // 내부 로직
- // =================
+ /// 유저 상세 조회
+ private User loadUserWithFacilityTypes(UUID userId) {
+ return userRepository.findWithFacilityTypesById(userId)
+ .orElseThrow(() -> new CustomException(UserErrorCode.NOT_FOUND_USER));
+ }
/// 이름 마스킹
private String maskName(String name) {
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/usecase/AdminUserUseCase.java b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/usecase/AdminUserUseCase.java
index c0a6a2f4..2afbe596 100644
--- a/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/usecase/AdminUserUseCase.java
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/admin/user/application/usecase/AdminUserUseCase.java
@@ -6,6 +6,8 @@
import co.kr.pinhouse.common.response.pageable.SliceResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
+import co.kr.pinhouse.domain.user.domain.entity.Role;
+import jakarta.servlet.http.HttpServletRequest;
public interface AdminUserUseCase {
@@ -18,4 +20,7 @@ public interface AdminUserUseCase {
/// 관리자 유저 상세 조회
AdminUserDetailResponse getUser(UUID userId);
+
+ /// 관리자 유저 권한 변경
+ AdminUserDetailResponse updateUserRole(UUID userId, Role role, UUID adminId, HttpServletRequest httpServletRequest);
}
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/Role.java b/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/Role.java
index 78aeef05..22ac65f3 100644
--- a/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/Role.java
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/Role.java
@@ -19,8 +19,13 @@ public enum Role {
/// JSON에서 생성
@JsonCreator
public static Role fromLabel(String label) {
+ if (label == null || label.isBlank()) {
+ throw new CustomException(UserErrorCode.BAD_REQUEST_ROLE);
+ }
+
+ String normalizedLabel = label.trim();
for (Role userRole : values()) {
- if (userRole.label.equals(label)) {
+ if (userRole.label.equals(normalizedLabel) || userRole.name().equalsIgnoreCase(normalizedLabel)) {
return userRole;
}
}
diff --git a/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/User.java b/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/User.java
index fb231371..cdb2d923 100644
--- a/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/User.java
+++ b/module-domain/src/main/java/co/kr/pinhouse/domain/user/domain/entity/User.java
@@ -159,4 +159,11 @@ public void updateFacilityTypes(List facilityTypes) {
this.facilityTypes = new ArrayList<>(facilityTypes);
}
}
+
+ /// 권한 업데이트
+ public void changeRole(Role role) {
+ if (role != null) {
+ this.role = role;
+ }
+ }
}
diff --git a/module-app/src/main/java/co/kr/pinhouse/app/logging/SanitizedMessageConverter.java b/module-infrastructure/src/main/java/co/kr/pinhouse/common/logging/SanitizedMessageConverter.java
similarity index 90%
rename from module-app/src/main/java/co/kr/pinhouse/app/logging/SanitizedMessageConverter.java
rename to module-infrastructure/src/main/java/co/kr/pinhouse/common/logging/SanitizedMessageConverter.java
index 68f6931c..5467804d 100644
--- a/module-app/src/main/java/co/kr/pinhouse/app/logging/SanitizedMessageConverter.java
+++ b/module-infrastructure/src/main/java/co/kr/pinhouse/common/logging/SanitizedMessageConverter.java
@@ -1,4 +1,4 @@
-package co.kr.pinhouse.app.logging;
+package co.kr.pinhouse.common.logging;
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
diff --git a/module-presentation/src/main/java/co/kr/pinhouse/domain/admin/user/AdminUserApi.java b/module-presentation/src/main/java/co/kr/pinhouse/domain/admin/user/AdminUserApi.java
index 17d0be49..669b8ce0 100644
--- a/module-presentation/src/main/java/co/kr/pinhouse/domain/admin/user/AdminUserApi.java
+++ b/module-presentation/src/main/java/co/kr/pinhouse/domain/admin/user/AdminUserApi.java
@@ -3,19 +3,25 @@
import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import co.kr.pinhouse.common.auth.CurrentUserId;
import co.kr.pinhouse.common.response.ApiResponse;
import co.kr.pinhouse.common.response.pageable.SliceRequest;
import co.kr.pinhouse.common.response.pageable.SliceResponse;
+import co.kr.pinhouse.domain.admin.user.application.dto.request.UpdateAdminUserRoleRequest;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
import co.kr.pinhouse.domain.admin.user.application.usecase.AdminUserUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RestController
@@ -42,4 +48,16 @@ public ApiResponse> getUsers(
public ApiResponse getUser(@PathVariable UUID userId) {
return ApiResponse.ok(adminUserService.getUser(userId));
}
+
+ /// 관리자 유저 권한 변경
+ @PatchMapping("/{userId}/role")
+ @Operation(summary = "관리자 유저 권한 변경", description = "유저의 권한을 변경합니다.")
+ public ApiResponse updateUserRole(
+ @PathVariable UUID userId,
+ @RequestBody @Valid UpdateAdminUserRoleRequest request,
+ @CurrentUserId(required = true) UUID adminId,
+ HttpServletRequest httpServletRequest
+ ) {
+ return ApiResponse.ok(adminUserService.updateUserRole(userId, request.role(), adminId, httpServletRequest));
+ }
}
diff --git a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/AuthExchangeResponse.java b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/AuthExchangeResponse.java
index 7136d5d2..a3f48687 100644
--- a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/AuthExchangeResponse.java
+++ b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/AuthExchangeResponse.java
@@ -1,39 +1,48 @@
package co.kr.pinhouse.security.auth.application.dto.response;
-import com.fasterxml.jackson.annotation.JsonInclude;
-
import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Builder;
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-@Schema(name = "[응답][인증] Exchange Code 교환 응답", description = "Exchange Code 해석 결과를 반환합니다.")
-@Builder
-public record AuthExchangeResponse(
- @Schema(description = "Exchange Code 처리 결과", example = "TOKEN_ISSUED")
- AuthExchangeResultType result,
-
- @Schema(description = "발급된 액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
- String accessToken,
-
- @Schema(description = "발급된 리프레쉬 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
- String refreshToken,
-
- @Schema(description = "회원가입이 필요한 경우 사용할 임시 키", example = "OAUTH2_TEMP_USER:1234...")
- String tempKey
-) {
-
- public static AuthExchangeResponse tokenIssued(String accessToken, String refreshToken) {
- return AuthExchangeResponse.builder()
- .result(AuthExchangeResultType.TOKEN_ISSUED)
- .accessToken(accessToken)
- .refreshToken(refreshToken)
- .build();
+
+@Schema(
+ name = "[응답][인증] Exchange Code 교환 응답",
+ description = "Exchange Code 해석 결과를 반환합니다.",
+ oneOf = {
+ TokenIssuedAuthExchangeResponse.class,
+ SignupRequiredAuthExchangeResponse.class
+ }
+)
+public interface AuthExchangeResponse {
+
+ AuthExchangeResultType result();
+
+ default String accessToken() {
+ return null;
+ }
+
+ default String refreshToken() {
+ return null;
+ }
+
+ default String pinpointId() {
+ return null;
+ }
+
+ default String tempKey() {
+ return null;
+ }
+
+ static AuthExchangeResponse tokenIssued(String accessToken, String refreshToken, String pinpointId) {
+ return new TokenIssuedAuthExchangeResponse(
+ AuthExchangeResultType.TOKEN_ISSUED,
+ accessToken,
+ refreshToken,
+ pinpointId
+ );
}
- public static AuthExchangeResponse signupRequired(String tempKey) {
- return AuthExchangeResponse.builder()
- .result(AuthExchangeResultType.SIGNUP_REQUIRED)
- .tempKey(tempKey)
- .build();
+ static AuthExchangeResponse signupRequired(String tempKey) {
+ return new SignupRequiredAuthExchangeResponse(
+ AuthExchangeResultType.SIGNUP_REQUIRED,
+ tempKey
+ );
}
}
diff --git a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/SignupRequiredAuthExchangeResponse.java b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/SignupRequiredAuthExchangeResponse.java
new file mode 100644
index 00000000..ae29f150
--- /dev/null
+++ b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/SignupRequiredAuthExchangeResponse.java
@@ -0,0 +1,16 @@
+package co.kr.pinhouse.security.auth.application.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(name = "[응답][인증] 회원가입 필요 응답", description = "회원가입이 필요한 상태를 반환합니다.")
+public record SignupRequiredAuthExchangeResponse(
+ @Schema(description = "Exchange Code 처리 결과", example = "SIGNUP_REQUIRED")
+ AuthExchangeResultType result,
+
+ @Schema(description = "회원가입이 필요한 경우 사용할 임시 키", example = "OAUTH2_TEMP_USER:1234...")
+ String tempKey
+) implements AuthExchangeResponse {
+}
diff --git a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/TokenIssuedAuthExchangeResponse.java b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/TokenIssuedAuthExchangeResponse.java
new file mode 100644
index 00000000..f03fcddb
--- /dev/null
+++ b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/dto/response/TokenIssuedAuthExchangeResponse.java
@@ -0,0 +1,23 @@
+package co.kr.pinhouse.security.auth.application.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Schema(name = "[응답][인증] 토큰 발급 응답", description = "토큰 발급 완료 상태를 반환합니다.")
+public record TokenIssuedAuthExchangeResponse(
+ @Schema(description = "Exchange Code 처리 결과", example = "TOKEN_ISSUED")
+ AuthExchangeResultType result,
+
+ @Schema(description = "발급된 액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
+ String accessToken,
+
+ @Schema(description = "발급된 리프레쉬 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
+ String refreshToken,
+
+ @Schema(description = "로그인 완료 시 대표 PinPoint ID", example = "f47ac10b-58cc-4372-a567-0e02b2c3d479")
+ @JsonInclude(JsonInclude.Include.ALWAYS)
+ String pinpointId
+) implements AuthExchangeResponse {
+}
diff --git a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/service/AuthService.java b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/service/AuthService.java
index 53289073..6451613e 100644
--- a/module-security/src/main/java/co/kr/pinhouse/security/auth/application/service/AuthService.java
+++ b/module-security/src/main/java/co/kr/pinhouse/security/auth/application/service/AuthService.java
@@ -14,6 +14,7 @@
import co.kr.pinhouse.common.exception.code.SecurityErrorCode;
import co.kr.pinhouse.common.response.CustomException;
import co.kr.pinhouse.common.util.KeyUtil;
+import co.kr.pinhouse.domain.pinpoint.application.usecase.PinPointUseCase;
import co.kr.pinhouse.domain.user.domain.entity.User;
import co.kr.pinhouse.domain.user.domain.onboarding.TempUserInfo;
import co.kr.pinhouse.domain.user.domain.repository.UserJpaRepository;
@@ -38,6 +39,7 @@ public class AuthService implements AuthUseCase {
/// 유저 저장소
private final UserJpaRepository repository;
+ private final PinPointUseCase pinPointUseCase;
/// 토큰 의존성
private final JwtValidator jwtValidator;
@@ -183,7 +185,20 @@ public AuthExchangeResponse exchangeCode(String exchangeCode) {
/// 토큰 발급
JwtTokenResponse tokenResponse = issueTokens(user);
- return AuthExchangeResponse.tokenIssued(tokenResponse.accessToken(), tokenResponse.refreshToken());
+ String pinpointId = resolvePrimaryPinPointId(user.getId());
+ return AuthExchangeResponse.tokenIssued(
+ tokenResponse.accessToken(),
+ tokenResponse.refreshToken(),
+ pinpointId
+ );
+ }
+
+ private String resolvePrimaryPinPointId(UUID userId) {
+ return pinPointUseCase.loadPinPoints(userId).pinPoints().stream()
+ .filter(pinPoint -> pinPoint.isFirst())
+ .findFirst()
+ .map(pinPoint -> pinPoint.id())
+ .orElse(null);
}
}