From 3da2ce5e11006a7e6586b1029b85ca717205f23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Wed, 11 Feb 2026 01:01:20 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(#365):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A6=89=EC=8B=9C=20=EC=82=AD=EC=A0=9C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/BrandLikeRepository.java | 2 + .../domain/repository/BrandRepository.java | 2 + .../repository/CampaignApplyRepository.java | 2 +- .../CampaignProposalRepository.java | 8 +++ .../repository/CampaignLikeRepository.java | 2 + .../MatchBrandHistoryRepository.java | 2 + .../MatchCampaignHistoryRepository.java | 2 + .../service/UserDeleteService.java | 69 +++++++++++++++++++ .../AuthenticationMethodRepository.java | 2 +- .../NotificationSettingRepository.java | 1 + .../UserContentCategoryRepository.java | 2 + .../domain/repository/UserTermRepository.java | 2 + .../controller/UserController.java | 11 +++ .../presentation/swagger/UserSwagger.java | 24 +++++++ 14 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/RealMatch/user/application/service/UserDeleteService.java 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..c59b1285 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,8 +5,10 @@ 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; +import org.springframework.transaction.annotation.Transactional; import com.example.RealMatch.business.domain.entity.CampaignProposal; import com.example.RealMatch.business.domain.enums.ProposalStatus; @@ -59,4 +61,10 @@ List findReceivedProposalIds( "WHERE p.id IN :ids") List findAllByIdWithDetails(@Param("ids") List ids); + // User가 연관된 Proposal 모두 삭제 (보낸/받은) + // 쿼리 최적화를 위해 @Modifying 어노테이션과 JPQL 사용 + @Modifying + @Transactional + @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 + ); } From 4609b6f05fd4414a7c5b98f2871962b72a390bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Wed, 11 Feb 2026 04:06:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(#365):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A6=89=EC=8B=9C=20=EC=82=AD=EC=A0=9C=20API=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/domain/repository/CampaignProposalRepository.java | 1 - 1 file changed, 1 deletion(-) 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 c59b1285..5c179f39 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 @@ -64,7 +64,6 @@ List findReceivedProposalIds( // User가 연관된 Proposal 모두 삭제 (보낸/받은) // 쿼리 최적화를 위해 @Modifying 어노테이션과 JPQL 사용 @Modifying - @Transactional @Query("DELETE FROM CampaignProposal cp WHERE cp.senderUserId = :userId OR cp.receiverUserId = :userId") void deleteByUserId(@Param("userId") Long userId); } From 95b1e5c37e9c8d5a1331bbae667dfd0492960d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B2=BD=EC=88=98?= Date: Wed, 11 Feb 2026 04:13:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(#365):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A6=89=EC=8B=9C=20=EC=82=AD=EC=A0=9C=20API=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=8A=A4=ED=83=80=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/domain/repository/CampaignProposalRepository.java | 1 - 1 file changed, 1 deletion(-) 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 5c179f39..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 @@ -8,7 +8,6 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.springframework.transaction.annotation.Transactional; import com.example.RealMatch.business.domain.entity.CampaignProposal; import com.example.RealMatch.business.domain.enums.ProposalStatus;