diff --git a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandLikeRepository.java b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandLikeRepository.java index e25e24da..91fb150d 100644 --- a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandLikeRepository.java +++ b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandLikeRepository.java @@ -24,4 +24,6 @@ public interface BrandLikeRepository extends JpaRepository { void deleteByUserIdAndBrandId(Long userId, Long brandId); long countByBrandId(Long brandId); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandRepository.java b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandRepository.java index 58d4ed0f..9a3c3ddd 100644 --- a/src/main/java/com/example/RealMatch/brand/domain/repository/BrandRepository.java +++ b/src/main/java/com/example/RealMatch/brand/domain/repository/BrandRepository.java @@ -45,4 +45,6 @@ public interface BrandRepository extends JpaRepository { where b.brandName like %:keyword% """) List findIdsByBrandNameContaining(@Param("keyword") String keyword); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignApplyRepository.java b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignApplyRepository.java index 4cb32f72..d3452c71 100644 --- a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignApplyRepository.java +++ b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignApplyRepository.java @@ -81,5 +81,5 @@ List findMyAppliedCollaborationsForSearch( @Param("brandIds") List brandIds ); - + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java index b62f3618..70d3cbf8 100644 --- a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java +++ b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -59,4 +60,9 @@ List findReceivedProposalIds( "WHERE p.id IN :ids") List findAllByIdWithDetails(@Param("ids") List ids); + // User가 연관된 Proposal 모두 삭제 (보낸/받은) + // 쿼리 최적화를 위해 @Modifying 어노테이션과 JPQL 사용 + @Modifying + @Query("DELETE FROM CampaignProposal cp WHERE cp.senderUserId = :userId OR cp.receiverUserId = :userId") + void deleteByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/com/example/RealMatch/campaign/domain/repository/CampaignLikeRepository.java b/src/main/java/com/example/RealMatch/campaign/domain/repository/CampaignLikeRepository.java index 71804c98..b1a0f40f 100644 --- a/src/main/java/com/example/RealMatch/campaign/domain/repository/CampaignLikeRepository.java +++ b/src/main/java/com/example/RealMatch/campaign/domain/repository/CampaignLikeRepository.java @@ -42,4 +42,6 @@ select cl.campaign.id, count(cl) group by cl.campaign.id """) List countByCampaignIdIn(@Param("campaignIds") List campaignIds); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java index 6faef45b..d2a7a622 100644 --- a/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java +++ b/src/main/java/com/example/RealMatch/match/domain/repository/MatchBrandHistoryRepository.java @@ -31,4 +31,6 @@ public interface MatchBrandHistoryRepository extends JpaRepository findByUserIdWithCursor( Long cursor, Pageable pageable ); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/user/application/service/UserDeleteService.java b/src/main/java/com/example/RealMatch/user/application/service/UserDeleteService.java new file mode 100644 index 00000000..874250f2 --- /dev/null +++ b/src/main/java/com/example/RealMatch/user/application/service/UserDeleteService.java @@ -0,0 +1,69 @@ +package com.example.RealMatch.user.application.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.RealMatch.brand.domain.repository.BrandLikeRepository; +import com.example.RealMatch.brand.domain.repository.BrandRepository; +import com.example.RealMatch.business.domain.repository.CampaignApplyRepository; +import com.example.RealMatch.business.domain.repository.CampaignProposalRepository; +import com.example.RealMatch.campaign.domain.repository.CampaignLikeRepository; +import com.example.RealMatch.global.exception.CustomException; +import com.example.RealMatch.match.domain.repository.MatchBrandHistoryRepository; +import com.example.RealMatch.match.domain.repository.MatchCampaignHistoryRepository; +import com.example.RealMatch.tag.domain.repository.TagUserRepository; +import com.example.RealMatch.user.domain.entity.User; +import com.example.RealMatch.user.domain.repository.AuthenticationMethodRepository; +import com.example.RealMatch.user.domain.repository.NotificationSettingRepository; +import com.example.RealMatch.user.domain.repository.UserContentCategoryRepository; +import com.example.RealMatch.user.domain.repository.UserRepository; +import com.example.RealMatch.user.domain.repository.UserSignupPurposeRepository; +import com.example.RealMatch.user.domain.repository.UserTermRepository; +import com.example.RealMatch.user.presentation.code.UserErrorCode; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserDeleteService { + + private final UserRepository userRepository; + private final AuthenticationMethodRepository authenticationMethodRepository; + private final BrandRepository brandRepository; + private final BrandLikeRepository brandLikeRepository; + private final CampaignApplyRepository campaignApplyRepository; + private final CampaignLikeRepository campaignLikeRepository; + private final CampaignProposalRepository campaignProposalRepository; + private final MatchBrandHistoryRepository matchBrandHistoryRepository; + private final MatchCampaignHistoryRepository matchCampaignHistoryRepository; + private final NotificationSettingRepository notificationSettingRepository; + private final TagUserRepository tagUserRepository; + private final UserContentCategoryRepository userContentCategoryRepository; + private final UserSignupPurposeRepository userSignupPurposeRepository; + private final UserTermRepository userTermRepository; + + @Transactional + public void deleteUserImmediately(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + + deleteRelatedData(userId); + userRepository.delete(user); + } + + private void deleteRelatedData(Long userId) { + tagUserRepository.deleteByUserId(userId); + userContentCategoryRepository.deleteByUserId(userId); + userSignupPurposeRepository.deleteByUserId(userId); + userTermRepository.deleteByUserId(userId); + notificationSettingRepository.deleteByUserId(userId); + matchBrandHistoryRepository.deleteByUserId(userId); + matchCampaignHistoryRepository.deleteByUserId(userId); + brandLikeRepository.deleteByUserId(userId); + campaignLikeRepository.deleteByUserId(userId); + campaignApplyRepository.deleteByUserId(userId); + campaignProposalRepository.deleteByUserId(userId); + brandRepository.deleteByUserId(userId); + authenticationMethodRepository.deleteByUserId(userId); + } +} diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/AuthenticationMethodRepository.java b/src/main/java/com/example/RealMatch/user/domain/repository/AuthenticationMethodRepository.java index 9bc4ec63..18c5b3ee 100644 --- a/src/main/java/com/example/RealMatch/user/domain/repository/AuthenticationMethodRepository.java +++ b/src/main/java/com/example/RealMatch/user/domain/repository/AuthenticationMethodRepository.java @@ -16,5 +16,5 @@ public interface AuthenticationMethodRepository extends JpaRepository findByUserId(Long userId); Optional findOneByUserId(Long userId); + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/UserContentCategoryRepository.java b/src/main/java/com/example/RealMatch/user/domain/repository/UserContentCategoryRepository.java index dac55678..c8ee440f 100644 --- a/src/main/java/com/example/RealMatch/user/domain/repository/UserContentCategoryRepository.java +++ b/src/main/java/com/example/RealMatch/user/domain/repository/UserContentCategoryRepository.java @@ -18,4 +18,6 @@ public interface UserContentCategoryRepository where ucc.user.id = :userId """) List findByUserId(@Param("userId") Long userId); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/UserTermRepository.java b/src/main/java/com/example/RealMatch/user/domain/repository/UserTermRepository.java index 1b95a619..47048636 100644 --- a/src/main/java/com/example/RealMatch/user/domain/repository/UserTermRepository.java +++ b/src/main/java/com/example/RealMatch/user/domain/repository/UserTermRepository.java @@ -17,4 +17,6 @@ public interface UserTermRepository extends JpaRepository { List findByUserIdAndIsAgreed(Long userId, boolean isAgreed); Optional findByUserIdAndTermName(Long userId, TermName termName); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/example/RealMatch/user/presentation/controller/UserController.java b/src/main/java/com/example/RealMatch/user/presentation/controller/UserController.java index da4936a4..ead3e320 100644 --- a/src/main/java/com/example/RealMatch/user/presentation/controller/UserController.java +++ b/src/main/java/com/example/RealMatch/user/presentation/controller/UserController.java @@ -15,6 +15,7 @@ import com.example.RealMatch.match.domain.entity.enums.BrandSortType; import com.example.RealMatch.match.domain.entity.enums.CampaignSortType; import com.example.RealMatch.match.presentation.dto.request.MatchRequestDto; +import com.example.RealMatch.user.application.service.UserDeleteService; import com.example.RealMatch.user.application.service.UserFavoriteService; import com.example.RealMatch.user.application.service.UserFeatureService; import com.example.RealMatch.user.application.service.UserService; @@ -47,6 +48,7 @@ public class UserController implements UserSwagger { private final UserFeatureService userFeatureService; private final UserWithdrawService userWithdrawService; private final UserFavoriteService userFavoriteService; + private final UserDeleteService userDeleteService; @Override @GetMapping("/me") @@ -190,4 +192,13 @@ public CustomResponse updateMySns( userService.updateSns(userDetails.getUserId(), request) ); } + + @Override + @DeleteMapping("/me/delete-immediately") + public CustomResponse deleteUserImmediately( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + userDeleteService.deleteUserImmediately(userDetails.getUserId()); + return CustomResponse.ok(null); + } } diff --git a/src/main/java/com/example/RealMatch/user/presentation/swagger/UserSwagger.java b/src/main/java/com/example/RealMatch/user/presentation/swagger/UserSwagger.java index ce052db4..00602d5a 100644 --- a/src/main/java/com/example/RealMatch/user/presentation/swagger/UserSwagger.java +++ b/src/main/java/com/example/RealMatch/user/presentation/swagger/UserSwagger.java @@ -1,6 +1,7 @@ package com.example.RealMatch.user.presentation.swagger; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -241,4 +242,27 @@ CustomResponse updateMySns( @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails, @Valid @RequestBody MyInstagramUpdateRequestDto request ); + + @Operation( + summary = "회원 즉시 삭제 API By 고경수", + description = """ + 로그인한 사용자의 데이터를 즉시 물리 삭제합니다. (복구 불가) + + 삭제 순서: + - 유저 관련 자식 데이터 삭제 후 + - users 물리 삭제 + + 주의: + - 브랜드 오너/참조 FK가 존재하는 경우 DB 제약조건에 따라 실패할 수 있습니다. + """ + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "즉시 삭제 성공"), + @ApiResponse(responseCode = "404", description = "USER_NOT_FOUND"), + @ApiResponse(responseCode = "500", description = "연관 데이터 FK 제약으로 삭제 실패 가능") + }) + @DeleteMapping("/me/delete-immediately") + CustomResponse deleteUserImmediately( + @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails + ); }