From 14d06852fb24a04f8c0a0654cc5bc9e592bf3aa6 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 21:28:53 +0900 Subject: [PATCH 1/7] feat(database): update redot_members status check constraint to allow specific statuses --- .../db/migration/V5__update_redot_member_status_check.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/resources/db/migration/V5__update_redot_member_status_check.sql diff --git a/src/main/resources/db/migration/V5__update_redot_member_status_check.sql b/src/main/resources/db/migration/V5__update_redot_member_status_check.sql new file mode 100644 index 0000000..70295ce --- /dev/null +++ b/src/main/resources/db/migration/V5__update_redot_member_status_check.sql @@ -0,0 +1,5 @@ +ALTER TABLE redot_members + DROP CONSTRAINT IF EXISTS redot_members_status_check; + +ALTER TABLE redot_members + ADD CONSTRAINT redot_members_status_check CHECK (status IN ('ACTIVE', 'BANNED', 'DELETED')); From 98bf67ff8ef7d49b9441bcf11d2f4c376bd055b1 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 21:29:11 +0900 Subject: [PATCH 2/7] feat(redot-member): add BANNED status to RedotMemberStatus for improved member management --- .../domain/redot/member/entity/RedotMemberStatus.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMemberStatus.java b/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMemberStatus.java index db66b57..5c3733f 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMemberStatus.java +++ b/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMemberStatus.java @@ -2,5 +2,6 @@ public enum RedotMemberStatus { ACTIVE, + BANNED, DELETED } From dbd14a8dd65a20bbaaf35494a1cdc24839c4a62b Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 21:29:33 +0900 Subject: [PATCH 3/7] feat(admin): implement admin member management endpoints and validation for status updates --- .../AdminRedotMemberController.java | 53 +++++++++ .../docs/AdminRedotMemberControllerDocs.java | 38 +++++++ .../dto/AdminRedotMemberSearchCondition.java | 12 +++ .../AdminRedotMemberUpdateRequest.java | 14 +++ .../response/AdminRedotMemberProjection.java | 23 ++++ .../AdminRedotMemberProjectionImpl.java | 57 ++++++++++ .../response/AdminRedotMemberResponse.java | 49 +++++++++ .../service/AdminRedotMemberService.java | 79 ++++++++++++++ .../domain/auth/exception/AuthErrorCode.java | 4 +- .../app/repository/RedotAppRepository.java | 2 + .../redot/member/entity/RedotMember.java | 9 +- .../repository/RedotMemberRepository.java | 7 +- .../RedotMemberRepositoryCustom.java | 10 ++ .../repository/RedotMemberRepositoryImpl.java | 102 ++++++++++++++++++ 14 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 src/main/java/redot/redot_server/domain/admin/controller/AdminRedotMemberController.java create mode 100644 src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotMemberControllerDocs.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/AdminRedotMemberSearchCondition.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/request/AdminRedotMemberUpdateRequest.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjection.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjectionImpl.java create mode 100644 src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberResponse.java create mode 100644 src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java create mode 100644 src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryCustom.java create mode 100644 src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryImpl.java diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotMemberController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotMemberController.java new file mode 100644 index 0000000..314392b --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotMemberController.java @@ -0,0 +1,53 @@ +package redot.redot_server.domain.admin.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.admin.controller.docs.AdminRedotMemberControllerDocs; +import redot.redot_server.domain.admin.dto.AdminRedotMemberSearchCondition; +import redot.redot_server.domain.admin.dto.request.AdminRedotMemberUpdateRequest; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberResponse; +import redot.redot_server.domain.admin.service.AdminRedotMemberService; +import redot.redot_server.global.util.dto.response.PageResponse; + +@RestController +@RequestMapping("/api/v1/redot/admin/members") +@RequiredArgsConstructor +public class AdminRedotMemberController implements AdminRedotMemberControllerDocs { + + private final AdminRedotMemberService adminRedotMemberService; + + @GetMapping + @Override + public ResponseEntity> getRedotMembers( + @ParameterObject AdminRedotMemberSearchCondition searchCondition, + @ParameterObject Pageable pageable + ) { + return ResponseEntity.ok(adminRedotMemberService.getRedotMembers(searchCondition, pageable)); + } + + @PutMapping("/{memberId}") + @Override + public ResponseEntity updateRedotMember( + @PathVariable("memberId") Long memberId, + @Valid @RequestBody AdminRedotMemberUpdateRequest request + ) { + return ResponseEntity.ok(adminRedotMemberService.updateRedotMember(memberId, request)); + } + + @DeleteMapping("/{memberId}") + @Override + public ResponseEntity deleteRedotMember(@PathVariable("memberId") Long memberId) { + adminRedotMemberService.deleteRedotMember(memberId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotMemberControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotMemberControllerDocs.java new file mode 100644 index 0000000..017a98f --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotMemberControllerDocs.java @@ -0,0 +1,38 @@ +package redot.redot_server.domain.admin.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.admin.dto.AdminRedotMemberSearchCondition; +import redot.redot_server.domain.admin.dto.request.AdminRedotMemberUpdateRequest; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberResponse; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "Admin Redot Member", description = "관리자 Redot 회원 관리 API") +public interface AdminRedotMemberControllerDocs { + + @Operation(summary = "Redot 회원 목록 조회", description = "`email`, `name`, `socialProvider`, `status` 조건으로 검색하고 `createdAt` 정렬을 ASC/DESC 로 조정할 수 있습니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getRedotMembers( + @ParameterObject AdminRedotMemberSearchCondition searchCondition, + @ParameterObject Pageable pageable); + + @Operation(summary = "Redot 회원 정보 수정", description = "이름, 프로필 이미지, 상태를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = AdminRedotMemberResponse.class))) + ResponseEntity updateRedotMember( + @Parameter(description = "회원 ID", example = "1") Long memberId, + @Valid AdminRedotMemberUpdateRequest request); + + @Operation(summary = "Redot 회원 삭제", description = "회원 계정을 탈퇴 처리합니다.") + @ApiResponse(responseCode = "204", description = "삭제 성공") + ResponseEntity deleteRedotMember(@Parameter(description = "회원 ID", example = "1") Long memberId); +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/AdminRedotMemberSearchCondition.java b/src/main/java/redot/redot_server/domain/admin/dto/AdminRedotMemberSearchCondition.java new file mode 100644 index 0000000..1573259 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/AdminRedotMemberSearchCondition.java @@ -0,0 +1,12 @@ +package redot.redot_server.domain.admin.dto; + +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.entity.SocialProvider; + +public record AdminRedotMemberSearchCondition( + String email, + String name, + SocialProvider socialProvider, + RedotMemberStatus status +) { +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/request/AdminRedotMemberUpdateRequest.java b/src/main/java/redot/redot_server/domain/admin/dto/request/AdminRedotMemberUpdateRequest.java new file mode 100644 index 0000000..82e1b08 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/request/AdminRedotMemberUpdateRequest.java @@ -0,0 +1,14 @@ +package redot.redot_server.domain.admin.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; + +public record AdminRedotMemberUpdateRequest( + @NotBlank + String name, + String profileImageUrl, + @NotNull + RedotMemberStatus status +) { +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjection.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjection.java new file mode 100644 index 0000000..6944c77 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjection.java @@ -0,0 +1,23 @@ +package redot.redot_server.domain.admin.dto.response; + +import java.time.LocalDateTime; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.entity.SocialProvider; + +public interface AdminRedotMemberProjection { + Long getId(); + + String getEmail(); + + String getName(); + + String getProfileImageUrl(); + + SocialProvider getSocialProvider(); + + LocalDateTime getCreatedAt(); + + RedotMemberStatus getStatus(); + + Long getAppCount(); +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjectionImpl.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjectionImpl.java new file mode 100644 index 0000000..7c99afd --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberProjectionImpl.java @@ -0,0 +1,57 @@ +package redot.redot_server.domain.admin.dto.response; + +import java.time.LocalDateTime; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.entity.SocialProvider; + +public record AdminRedotMemberProjectionImpl( + Long id, + String email, + String name, + String profileImageUrl, + SocialProvider socialProvider, + LocalDateTime createdAt, + RedotMemberStatus status, + Long appCount +) implements AdminRedotMemberProjection { + + @Override + public Long getId() { + return id; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getProfileImageUrl() { + return profileImageUrl; + } + + @Override + public SocialProvider getSocialProvider() { + return socialProvider; + } + + @Override + public LocalDateTime getCreatedAt() { + return createdAt; + } + + @Override + public RedotMemberStatus getStatus() { + return status; + } + + @Override + public Long getAppCount() { + return appCount; + } +} diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberResponse.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberResponse.java new file mode 100644 index 0000000..6726217 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminRedotMemberResponse.java @@ -0,0 +1,49 @@ +package redot.redot_server.domain.admin.dto.response; + +import java.time.LocalDateTime; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberProjection; +import redot.redot_server.domain.redot.member.entity.RedotMember; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.entity.SocialProvider; +import redot.redot_server.global.s3.util.ImageUrlResolver; + +public record AdminRedotMemberResponse( + Long id, + String email, + String name, + String profileImageUrl, + SocialProvider socialProvider, + LocalDateTime createdAt, + RedotMemberStatus status, + long appCount +) { + + public static AdminRedotMemberResponse fromProjection(AdminRedotMemberProjection projection, + ImageUrlResolver imageUrlResolver) { + return new AdminRedotMemberResponse( + projection.getId(), + projection.getEmail(), + projection.getName(), + imageUrlResolver.toPublicUrl(projection.getProfileImageUrl()), + projection.getSocialProvider(), + projection.getCreatedAt(), + projection.getStatus(), + projection.getAppCount() == null ? 0L : projection.getAppCount() + ); + } + + public static AdminRedotMemberResponse fromEntity(RedotMember member, + long appCount, + ImageUrlResolver imageUrlResolver) { + return new AdminRedotMemberResponse( + member.getId(), + member.getEmail(), + member.getName(), + imageUrlResolver.toPublicUrl(member.getProfileImageUrl()), + member.getSocialProvider(), + member.getCreatedAt(), + member.getStatus(), + appCount + ); + } +} diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java new file mode 100644 index 0000000..a5cff5b --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java @@ -0,0 +1,79 @@ +package redot.redot_server.domain.admin.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import redot.redot_server.domain.admin.dto.AdminRedotMemberSearchCondition; +import redot.redot_server.domain.admin.dto.request.AdminRedotMemberUpdateRequest; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberProjection; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberResponse; +import redot.redot_server.domain.auth.exception.AuthErrorCode; +import redot.redot_server.domain.auth.exception.AuthException; +import redot.redot_server.domain.redot.app.repository.RedotAppRepository; +import redot.redot_server.domain.redot.member.entity.RedotMember; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; +import redot.redot_server.global.s3.event.ImageDeletionEvent; +import redot.redot_server.global.s3.util.ImageUrlResolver; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminRedotMemberService { + + private final RedotMemberRepository redotMemberRepository; + private final RedotAppRepository redotAppRepository; + private final ApplicationEventPublisher eventPublisher; + private final ImageUrlResolver imageUrlResolver; + + public PageResponse getRedotMembers(AdminRedotMemberSearchCondition searchCondition, + Pageable pageable) { + if (searchCondition != null && searchCondition.status() == RedotMemberStatus.DELETED) { + throw new AuthException(AuthErrorCode.INVALID_MEMBER_STATUS_FILTER); + } + Page summaries = redotMemberRepository.searchAdminMembers(searchCondition, pageable); + Page responsePage = summaries.map(summary -> + AdminRedotMemberResponse.fromProjection(summary, imageUrlResolver)); + return PageResponse.from(responsePage); + } + + @Transactional + public AdminRedotMemberResponse updateRedotMember(Long memberId, AdminRedotMemberUpdateRequest request) { + RedotMember redotMember = getMemberIncludingDeleted(memberId); + + String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); + deleteOldProfileImageUrlIfChanged(newProfileImageUrl, redotMember); + + redotMember.updateInfo(request.name(), newProfileImageUrl); + if (request.status() == RedotMemberStatus.DELETED) { + throw new AuthException(AuthErrorCode.INVALID_MEMBER_STATUS_UPDATE); + } + + redotMember.changeStatus(request.status()); + + return AdminRedotMemberResponse.fromEntity(redotMember, redotAppRepository.countByOwnerId(memberId), imageUrlResolver); + } + + @Transactional + public void deleteRedotMember(Long memberId) { + RedotMember redotMember = getMemberIncludingDeleted(memberId); + redotMember.delete(); + } + + private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, RedotMember member) { + String oldProfileImageUrl = member.getProfileImageUrl(); + if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(newProfileImageUrl)) { + eventPublisher.publishEvent(new ImageDeletionEvent(oldProfileImageUrl)); + } + } + + private RedotMember getMemberIncludingDeleted(Long memberId) { + return redotMemberRepository.findByIdIncludingDeleted(memberId) + .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + } + +} diff --git a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java index 928a1a3..7ab970a 100644 --- a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java +++ b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java @@ -34,7 +34,9 @@ public enum AuthErrorCode implements ErrorCode { INVALID_EMAIL_VERIFICATION_CODE(400, 1024, "이메일 인증 코드가 올바르지 않습니다."), INVALID_EMAIL_VERIFICATION_TOKEN(400, 1025, "이메일 인증 토큰이 유효하지 않습니다."), UNSUPPORTED_EMAIL_VERIFICATION_PURPOSE(400, 1026, "지원하지 않는 이메일 인증 용도입니다."), - EMAIL_NOT_VERIFIED(400, 1027, "이메일 인증이 필요합니다.") + EMAIL_NOT_VERIFIED(400, 1027, "이메일 인증이 필요합니다."), + INVALID_MEMBER_STATUS_UPDATE(400, 1028, "지원하지 않는 회원 상태 변경입니다."), + INVALID_MEMBER_STATUS_FILTER(400, 1029, "지원하지 않는 회원 상태 조회입니다.") ; diff --git a/src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java b/src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java index d8e5e9e..b2bd245 100644 --- a/src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java +++ b/src/main/java/redot/redot_server/domain/redot/app/repository/RedotAppRepository.java @@ -7,4 +7,6 @@ public interface RedotAppRepository extends JpaRepository { Page findByOwnerId(Long ownerId, Pageable pageable); + + long countByOwnerId(Long ownerId); } diff --git a/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMember.java b/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMember.java index e440cfa..09cc15c 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMember.java +++ b/src/main/java/redot/redot_server/domain/redot/member/entity/RedotMember.java @@ -34,7 +34,7 @@ @UniqueConstraint(columnNames = {"social_provider", "social_provider_id"}) } ) -@SQLRestriction("status = 'ACTIVE'") +@SQLRestriction("status <> 'DELETED'") public class RedotMember extends BaseTimeEntity { @Id @@ -103,6 +103,13 @@ public void delete(){ this.deletedAt = LocalDateTime.now(); } + public void changeStatus(RedotMemberStatus status) { + this.status = status; + if (status != RedotMemberStatus.DELETED) { + this.deletedAt = null; + } + } + public void linkSocialAccount(SocialProvider socialProvider, String socialProviderId, String name, diff --git a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java index 057f1cf..c91926b 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java +++ b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java @@ -2,13 +2,18 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.entity.SocialProvider; -public interface RedotMemberRepository extends JpaRepository { +public interface RedotMemberRepository extends JpaRepository, RedotMemberRepositoryCustom { boolean existsByEmail(String email); Optional findByEmail(String email); Optional findBySocialProviderAndSocialProviderId(SocialProvider provider, String socialProviderId); + + @Query(value = "SELECT * FROM redot_members WHERE id = :id", nativeQuery = true) + Optional findByIdIncludingDeleted(@Param("id") Long id); } diff --git a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryCustom.java b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryCustom.java new file mode 100644 index 0000000..66f9bc7 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryCustom.java @@ -0,0 +1,10 @@ +package redot.redot_server.domain.redot.member.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import redot.redot_server.domain.admin.dto.AdminRedotMemberSearchCondition; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberProjection; + +public interface RedotMemberRepositoryCustom { + Page searchAdminMembers(AdminRedotMemberSearchCondition condition, Pageable pageable); +} diff --git a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryImpl.java b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryImpl.java new file mode 100644 index 0000000..17322f4 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepositoryImpl.java @@ -0,0 +1,102 @@ +package redot.redot_server.domain.redot.member.repository; + +import static redot.redot_server.domain.redot.app.entity.QRedotApp.redotApp; +import static redot.redot_server.domain.redot.member.entity.QRedotMember.redotMember; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; +import redot.redot_server.domain.admin.dto.AdminRedotMemberSearchCondition; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberProjection; +import redot.redot_server.domain.admin.dto.response.AdminRedotMemberProjectionImpl; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; +import redot.redot_server.domain.redot.member.entity.SocialProvider; + +@Repository +@RequiredArgsConstructor +public class RedotMemberRepositoryImpl implements RedotMemberRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page searchAdminMembers(AdminRedotMemberSearchCondition condition, Pageable pageable) { + List predicates = buildPredicates(condition); + BooleanExpression[] filters = predicates.toArray(new BooleanExpression[0]); + + List fetched = queryFactory + .select(Projections.constructor(AdminRedotMemberProjectionImpl.class, + redotMember.id, + redotMember.email, + redotMember.name, + redotMember.profileImageUrl, + redotMember.socialProvider, + redotMember.createdAt, + redotMember.status, + redotApp.id.count())) + .from(redotMember) + .leftJoin(redotApp).on(redotApp.owner.eq(redotMember)) + .where(filters) + .groupBy(redotMember.id, + redotMember.email, + redotMember.name, + redotMember.profileImageUrl, + redotMember.socialProvider, + redotMember.createdAt, + redotMember.status) + .orderBy(resolveSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List content = new ArrayList<>(fetched); + + Long totalCount = queryFactory.select(redotMember.count()) + .from(redotMember) + .where(filters) + .fetchOne(); + + long total = totalCount == null ? 0L : totalCount; + + return new PageImpl<>(content, pageable, total); + } + + private List buildPredicates(AdminRedotMemberSearchCondition condition) { + List predicates = new ArrayList<>(); + if (condition == null) { + return predicates; + } + if (StringUtils.hasText(condition.email())) { + predicates.add(redotMember.email.containsIgnoreCase(condition.email())); + } + if (StringUtils.hasText(condition.name())) { + predicates.add(redotMember.name.containsIgnoreCase(condition.name())); + } + SocialProvider socialProvider = condition.socialProvider(); + if (socialProvider != null) { + predicates.add(redotMember.socialProvider.eq(socialProvider)); + } + RedotMemberStatus status = condition.status(); + if (status != null) { + predicates.add(redotMember.status.eq(status)); + } + return predicates; + } + + private OrderSpecifier resolveSort(Pageable pageable) { + Sort.Order createdAtOrder = pageable.getSort().stream() + .filter(order -> order.getProperty().equals("createdAt")) + .findFirst() + .orElseGet(() -> new Sort.Order(Sort.Direction.DESC, "createdAt")); + return createdAtOrder.isAscending() ? redotMember.createdAt.asc() : redotMember.createdAt.desc(); + } +} From 2f414f49db509d2764fb5075364d29e95569abc3 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 21:39:38 +0900 Subject: [PATCH 4/7] feat(redot-member): add BANNED_REDOT_MEMBER error code and implement status validation for member actions --- .../domain/auth/exception/AuthErrorCode.java | 3 +- .../auth/service/RedotMemberAuthService.java | 10 +++++- .../member/service/RedotMemberService.java | 12 +++++-- .../service/RedotMemberStatusValidator.java | 17 ++++++++++ .../RedotMemberJwtAuthenticationFilter.java | 32 +++++++++++++++++-- .../RedotMemberRefreshTokenFilter.java | 10 ++++-- 6 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberStatusValidator.java diff --git a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java index 7ab970a..ec0d446 100644 --- a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java +++ b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java @@ -36,7 +36,8 @@ public enum AuthErrorCode implements ErrorCode { UNSUPPORTED_EMAIL_VERIFICATION_PURPOSE(400, 1026, "지원하지 않는 이메일 인증 용도입니다."), EMAIL_NOT_VERIFIED(400, 1027, "이메일 인증이 필요합니다."), INVALID_MEMBER_STATUS_UPDATE(400, 1028, "지원하지 않는 회원 상태 변경입니다."), - INVALID_MEMBER_STATUS_FILTER(400, 1029, "지원하지 않는 회원 상태 조회입니다.") + INVALID_MEMBER_STATUS_FILTER(400, 1029, "지원하지 않는 회원 상태 조회입니다."), + BANNED_REDOT_MEMBER(403, 1030, "제한된 계정입니다.") ; diff --git a/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java b/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java index 07e679d..f1dd9ed 100644 --- a/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java +++ b/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java @@ -15,6 +15,7 @@ import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse; import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; +import redot.redot_server.domain.redot.member.service.RedotMemberStatusValidator; import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.jwt.token.TokenContext; import redot.redot_server.global.jwt.token.TokenType; @@ -32,6 +33,7 @@ public class RedotMemberAuthService { private final AuthTokenService authTokenService; private final EmailVerificationService emailVerificationService; private final ImageUrlResolver imageUrlResolver; + private final RedotMemberStatusValidator redotMemberStatusValidator; @Transactional public RedotMemberResponse signUp(RedotMemberCreateRequest request) { @@ -68,6 +70,8 @@ public AuthResult signIn(HttpServletRequest request, RedotMemberSignInRequest si throw new AuthException(AuthErrorCode.INVALID_USER_INFO); } + redotMemberStatusValidator.ensureActive(member); + TokenContext context = new TokenContext( member.getId(), TokenType.REDOT_MEMBER, @@ -94,8 +98,9 @@ public AuthResult reissue(HttpServletRequest request) { throw new AuthException(AuthErrorCode.INVALID_TOKEN_SUBJECT); } - redotMemberRepository.findById(memberId) + RedotMember member = redotMemberRepository.findById(memberId) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); return authTokenService.issueTokens(request, new TokenContext( memberId, @@ -108,6 +113,7 @@ public AuthResult reissue(HttpServletRequest request) { public RedotMemberResponse getCurrentMember(Long memberId) { RedotMember member = redotMemberRepository.findById(memberId) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); return RedotMemberResponse.fromEntity(member, imageUrlResolver); } @@ -124,6 +130,8 @@ public void resetPassword(PasswordResetConfirmRequest request) { RedotMember member = redotMemberRepository.findByEmail(normalizedEmail) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); + member.resetPassword(passwordEncoder.encode(request.newPassword())); } } diff --git a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java index e9773fb..6ac5e00 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java +++ b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java @@ -30,6 +30,7 @@ public class RedotMemberService { private final ImageStorageService imageStorageService; private final ApplicationEventPublisher eventPublisher; private final ImageUrlResolver imageUrlResolver; + private final RedotMemberStatusValidator redotMemberStatusValidator; @Transactional public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvider provider) { @@ -38,12 +39,15 @@ public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvide Optional byProvider = redotMemberRepository .findBySocialProviderAndSocialProviderId(provider, profile.providerId()); if (byProvider.isPresent()) { - return byProvider.get(); + RedotMember existing = byProvider.get(); + redotMemberStatusValidator.ensureActive(existing); + return existing; } Optional byEmail = redotMemberRepository.findByEmail(normalizedEmail); if (byEmail.isPresent()) { RedotMember existing = byEmail.get(); + redotMemberStatusValidator.ensureActive(existing); existing.linkSocialAccount(provider, profile.providerId(), profile.name(), profile.profileImageUrl()); return existing; } @@ -60,8 +64,9 @@ public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvide @Transactional public UploadedImageUrlResponse uploadProfileImage(Long memberId, MultipartFile imageFile) { - redotMemberRepository.findById(memberId) + RedotMember member = redotMemberRepository.findById(memberId) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); String imageUrl = imageStorageService.upload(ImageDirectory.REDOT_MEMBER_PROFILE, memberId, imageFile); return new UploadedImageUrlResponse(imageUrl); @@ -72,6 +77,8 @@ public RedotMemberResponse updateRedotMemberInfo(Long id, RedotMemberUpdateReque RedotMember redotMember = redotMemberRepository.findById(id) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(redotMember); + String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); deleteOldProfileImageUrlIfChanged(newProfileImageUrl, redotMember); @@ -92,6 +99,7 @@ private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, RedotM public void deleteRedotMember(Long id) { RedotMember redotMember = redotMemberRepository.findById(id) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(redotMember); redotMember.delete(); } } diff --git a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberStatusValidator.java b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberStatusValidator.java new file mode 100644 index 0000000..0a045a0 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberStatusValidator.java @@ -0,0 +1,17 @@ +package redot.redot_server.domain.redot.member.service; + +import org.springframework.stereotype.Component; +import redot.redot_server.domain.auth.exception.AuthErrorCode; +import redot.redot_server.domain.auth.exception.AuthException; +import redot.redot_server.domain.redot.member.entity.RedotMember; +import redot.redot_server.domain.redot.member.entity.RedotMemberStatus; + +@Component +public class RedotMemberStatusValidator { + + public void ensureActive(RedotMember redotMember) { + if (redotMember.getStatus() == RedotMemberStatus.BANNED) { + throw new AuthException(AuthErrorCode.BANNED_REDOT_MEMBER); + } + } +} diff --git a/src/main/java/redot/redot_server/global/security/filter/jwt/auth/RedotMemberJwtAuthenticationFilter.java b/src/main/java/redot/redot_server/global/security/filter/jwt/auth/RedotMemberJwtAuthenticationFilter.java index ea01287..ba028df 100644 --- a/src/main/java/redot/redot_server/global/security/filter/jwt/auth/RedotMemberJwtAuthenticationFilter.java +++ b/src/main/java/redot/redot_server/global/security/filter/jwt/auth/RedotMemberJwtAuthenticationFilter.java @@ -4,19 +4,47 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import redot.redot_server.domain.auth.exception.AuthErrorCode; +import redot.redot_server.domain.auth.exception.AuthException; +import redot.redot_server.domain.redot.member.entity.RedotMember; +import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; +import redot.redot_server.domain.redot.member.service.RedotMemberStatusValidator; import redot.redot_server.global.jwt.provider.JwtProvider; import redot.redot_server.global.jwt.token.TokenType; @Component public class RedotMemberJwtAuthenticationFilter extends AbstractJwtAuthenticationFilter { + private final RedotMemberRepository redotMemberRepository; + private final RedotMemberStatusValidator redotMemberStatusValidator; + public RedotMemberJwtAuthenticationFilter(JwtProvider jwtProvider, - AuthenticationEntryPoint authenticationEntryPoint) { + AuthenticationEntryPoint authenticationEntryPoint, + RedotMemberRepository redotMemberRepository, + RedotMemberStatusValidator redotMemberStatusValidator) { super(jwtProvider, authenticationEntryPoint, TokenType.REDOT_MEMBER); + this.redotMemberRepository = redotMemberRepository; + this.redotMemberStatusValidator = redotMemberStatusValidator; } @Override protected void validateClaims(Claims claims, HttpServletRequest request) { - // No additional validation for RedotMember access tokens + Long memberId = extractSubjectId(claims); + RedotMember member = redotMemberRepository.findById(memberId) + .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); + } + + private Long extractSubjectId(Claims claims) { + String subject = claims.getSubject(); + if (!StringUtils.hasText(subject)) { + throw new AuthException(AuthErrorCode.INVALID_TOKEN_SUBJECT); + } + try { + return Long.parseLong(subject); + } catch (NumberFormatException ex) { + throw new AuthException(AuthErrorCode.INVALID_TOKEN_SUBJECT, ex); + } } } diff --git a/src/main/java/redot/redot_server/global/security/filter/jwt/refresh/RedotMemberRefreshTokenFilter.java b/src/main/java/redot/redot_server/global/security/filter/jwt/refresh/RedotMemberRefreshTokenFilter.java index 6e4c9ec..e6e9b4e 100644 --- a/src/main/java/redot/redot_server/global/security/filter/jwt/refresh/RedotMemberRefreshTokenFilter.java +++ b/src/main/java/redot/redot_server/global/security/filter/jwt/refresh/RedotMemberRefreshTokenFilter.java @@ -6,7 +6,9 @@ import org.springframework.stereotype.Component; import redot.redot_server.domain.auth.exception.AuthErrorCode; import redot.redot_server.domain.auth.exception.AuthException; +import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; +import redot.redot_server.domain.redot.member.service.RedotMemberStatusValidator; import redot.redot_server.global.jwt.cookie.CookieProvider; import redot.redot_server.global.jwt.provider.JwtProvider; import redot.redot_server.global.jwt.token.TokenType; @@ -15,13 +17,16 @@ public class RedotMemberRefreshTokenFilter extends AbstractRefreshTokenFilter { private final RedotMemberRepository redotMemberRepository; + private final RedotMemberStatusValidator redotMemberStatusValidator; public RedotMemberRefreshTokenFilter(JwtProvider jwtProvider, CookieProvider cookieProvider, AuthenticationEntryPoint authenticationEntryPoint, - RedotMemberRepository redotMemberRepository) { + RedotMemberRepository redotMemberRepository, + RedotMemberStatusValidator redotMemberStatusValidator) { super(jwtProvider, cookieProvider, authenticationEntryPoint); this.redotMemberRepository = redotMemberRepository; + this.redotMemberStatusValidator = redotMemberStatusValidator; } @Override @@ -32,7 +37,8 @@ protected TokenType requiredTokenType() { @Override protected void validateClaims(Claims claims, HttpServletRequest request) { Long memberId = extractSubjectId(claims); - redotMemberRepository.findById(memberId) + RedotMember member = redotMemberRepository.findById(memberId) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + redotMemberStatusValidator.ensureActive(member); } } From f46aa604a879d128a98146bdae7e4b87b84ea5b7 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 23:52:02 +0900 Subject: [PATCH 5/7] feat(redot-member): refactor member retrieval and status update logic in admin service --- .../service/AdminRedotMemberService.java | 18 ++++---- .../repository/RedotMemberRepository.java | 5 --- .../member/service/RedotMemberService.java | 43 ++++++++----------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java index a5cff5b..e97b4fe 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminRedotMemberService.java @@ -43,15 +43,17 @@ public PageResponse getRedotMembers(AdminRedotMemberSe @Transactional public AdminRedotMemberResponse updateRedotMember(Long memberId, AdminRedotMemberUpdateRequest request) { - RedotMember redotMember = getMemberIncludingDeleted(memberId); + RedotMember redotMember = redotMemberRepository.findById(memberId) + .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); + + if (request.status() == RedotMemberStatus.DELETED) { + throw new AuthException(AuthErrorCode.INVALID_MEMBER_STATUS_UPDATE); + } String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); deleteOldProfileImageUrlIfChanged(newProfileImageUrl, redotMember); redotMember.updateInfo(request.name(), newProfileImageUrl); - if (request.status() == RedotMemberStatus.DELETED) { - throw new AuthException(AuthErrorCode.INVALID_MEMBER_STATUS_UPDATE); - } redotMember.changeStatus(request.status()); @@ -60,7 +62,8 @@ public AdminRedotMemberResponse updateRedotMember(Long memberId, AdminRedotMembe @Transactional public void deleteRedotMember(Long memberId) { - RedotMember redotMember = getMemberIncludingDeleted(memberId); + RedotMember redotMember = redotMemberRepository.findById(memberId) + .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); redotMember.delete(); } @@ -71,9 +74,4 @@ private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, RedotM } } - private RedotMember getMemberIncludingDeleted(Long memberId) { - return redotMemberRepository.findByIdIncludingDeleted(memberId) - .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); - } - } diff --git a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java index c91926b..3e0d197 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java +++ b/src/main/java/redot/redot_server/domain/redot/member/repository/RedotMemberRepository.java @@ -2,8 +2,6 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.entity.SocialProvider; @@ -13,7 +11,4 @@ public interface RedotMemberRepository extends JpaRepository, Optional findByEmail(String email); Optional findBySocialProviderAndSocialProviderId(SocialProvider provider, String socialProviderId); - - @Query(value = "SELECT * FROM redot_members WHERE id = :id", nativeQuery = true) - Optional findByIdIncludingDeleted(@Param("id") Long id); } diff --git a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java index 6ac5e00..cec0b08 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java +++ b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java @@ -36,30 +36,25 @@ public class RedotMemberService { public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvider provider) { String normalizedEmail = EmailUtils.normalize(profile.email()); - Optional byProvider = redotMemberRepository - .findBySocialProviderAndSocialProviderId(provider, profile.providerId()); - if (byProvider.isPresent()) { - RedotMember existing = byProvider.get(); - redotMemberStatusValidator.ensureActive(existing); - return existing; - } - - Optional byEmail = redotMemberRepository.findByEmail(normalizedEmail); - if (byEmail.isPresent()) { - RedotMember existing = byEmail.get(); - redotMemberStatusValidator.ensureActive(existing); - existing.linkSocialAccount(provider, profile.providerId(), profile.name(), profile.profileImageUrl()); - return existing; - } - - RedotMember socialMember = RedotMember.createSocialMember( - profile.name(), - normalizedEmail, - profile.profileImageUrl(), - provider, - profile.providerId() - ); - return redotMemberRepository.save(socialMember); + return redotMemberRepository + .findBySocialProviderAndSocialProviderId(provider, profile.providerId()) + .map(existing -> { + redotMemberStatusValidator.ensureActive(existing); + return existing; + }) + .or(() -> redotMemberRepository.findByEmail(normalizedEmail) + .map(existing -> { + redotMemberStatusValidator.ensureActive(existing); + existing.linkSocialAccount(provider, profile.providerId(), profile.name(), profile.profileImageUrl()); + return existing; + })) + .orElseGet(() -> redotMemberRepository.save(RedotMember.createSocialMember( + profile.name(), + normalizedEmail, + profile.profileImageUrl(), + provider, + profile.providerId() + ))); } @Transactional From a24ceb37d4a3c6a1eccd3474be4308af5d27438f Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 23:56:05 +0900 Subject: [PATCH 6/7] feat(redot-member): add SOCIAL_EMAIL_REQUIRED error code and validate email presence for social members --- .../redot_server/domain/auth/exception/AuthErrorCode.java | 2 ++ .../domain/redot/member/service/RedotMemberService.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java index ec0d446..95ed45b 100644 --- a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java +++ b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java @@ -38,6 +38,8 @@ public enum AuthErrorCode implements ErrorCode { INVALID_MEMBER_STATUS_UPDATE(400, 1028, "지원하지 않는 회원 상태 변경입니다."), INVALID_MEMBER_STATUS_FILTER(400, 1029, "지원하지 않는 회원 상태 조회입니다."), BANNED_REDOT_MEMBER(403, 1030, "제한된 계정입니다.") + , + SOCIAL_EMAIL_REQUIRED(400, 1031, "소셜 계정에서 이메일 정보를 제공해야 합니다.") ; diff --git a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java index cec0b08..79141fc 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java +++ b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java @@ -1,10 +1,10 @@ package redot.redot_server.domain.redot.member.service; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import redot.redot_server.domain.auth.exception.AuthErrorCode; import redot.redot_server.domain.auth.exception.AuthException; @@ -35,6 +35,9 @@ public class RedotMemberService { @Transactional public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvider provider) { String normalizedEmail = EmailUtils.normalize(profile.email()); + if (!StringUtils.hasText(normalizedEmail)) { + throw new AuthException(AuthErrorCode.SOCIAL_EMAIL_REQUIRED); + } return redotMemberRepository .findBySocialProviderAndSocialProviderId(provider, profile.providerId()) From bcfcde8381ba9a388071e8f0bcf99e2fde2ab7a6 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 23:58:55 +0900 Subject: [PATCH 7/7] feat(redot-member): fix BANNED_REDOT_MEMBER error code formatting in AuthErrorCode --- .../redot_server/domain/auth/exception/AuthErrorCode.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java index 95ed45b..182cf2f 100644 --- a/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java +++ b/src/main/java/redot/redot_server/domain/auth/exception/AuthErrorCode.java @@ -37,8 +37,7 @@ public enum AuthErrorCode implements ErrorCode { EMAIL_NOT_VERIFIED(400, 1027, "이메일 인증이 필요합니다."), INVALID_MEMBER_STATUS_UPDATE(400, 1028, "지원하지 않는 회원 상태 변경입니다."), INVALID_MEMBER_STATUS_FILTER(400, 1029, "지원하지 않는 회원 상태 조회입니다."), - BANNED_REDOT_MEMBER(403, 1030, "제한된 계정입니다.") - , + BANNED_REDOT_MEMBER(403, 1030, "제한된 계정입니다."), SOCIAL_EMAIL_REQUIRED(400, 1031, "소셜 계정에서 이메일 정보를 제공해야 합니다.") ;