diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisRepository.java index 3b2cff5..cdca783 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/AnalysisRepository.java @@ -2,9 +2,19 @@ import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; 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 java.util.Optional; public interface AnalysisRepository extends JpaRepository { Optional findByMockApplyId(Long mockApplyId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from Analysis a + where a.mockApply.jobPosting.id = :jobPostingId + """) + void deleteAllByJobPostingId(@Param("jobPostingId") Long jobPostingId); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionAnalysisRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionAnalysisRepository.java index b32013f..0fef233 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionAnalysisRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionAnalysisRepository.java @@ -2,6 +2,9 @@ import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis; 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 java.util.List; @@ -10,4 +13,15 @@ public interface QuestionAnalysisRepository extends JpaRepository findAllByAnalysisId(Long analysisId); List findAllByAnalysisIdOrderByQuestionIdAscIdAsc(Long analysisId); void deleteAllByAnalysisId(Long analysisId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from QuestionAnalysis qa + where qa.analysis.id in ( + select a.id + from Analysis a + where a.mockApply.jobPosting.id = :jobPostingId + ) + """) + void deleteAllByJobPostingId(@Param("jobPostingId") Long jobPostingId); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java index de95563..317d8d0 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/repository/QuestionRepository.java @@ -2,6 +2,9 @@ import com.jobdri.jobdri_api.domain.analysis.entity.Question; 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 java.util.List; @@ -10,4 +13,11 @@ public interface QuestionRepository extends JpaRepository { List findAllByMockApplyIdOrderByIdAsc(Long mockApplyId); boolean existsByMockApplyIdAndContent(Long mockApplyId, String content); void deleteAllByMockApplyId(Long mockApplyId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from Question q + where q.mockApply.jobPosting.id = :jobPostingId + """) + void deleteAllByJobPostingId(@Param("jobPostingId") Long jobPostingId); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java index e2673ba..a693cf4 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingController.java @@ -20,6 +20,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.PatchMapping; @@ -139,6 +140,20 @@ public ApiResponse> getMyJobPostings( ); } + @Operation( + summary = "채용 공고 및 모의 서류 결과 전체 삭제", + description = "채용 공고와 연결된 모의 서류 지원, 문항, 분석 결과를 함께 삭제합니다." + ) + @DeleteMapping("/{jobPostingId}") + public ApiResponse deleteJobPosting( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long jobPostingId + ) { + var user = validateAuthenticatedUser(userDetails); + jobPostingService.deleteJobPosting(user, jobPostingId); + return ApiResponse.onSuccess("채용 공고와 모의 서류 결과가 삭제되었습니다.", null); + } + private com.jobdri.jobdri_api.domain.user.entity.User validateAuthenticatedUser(UserDetailsImpl userDetails) { return userService.validateUser(userDetails == null ? null : userDetails.getUser()); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java index 9305a3d..cf3af7e 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingService.java @@ -1,5 +1,8 @@ package com.jobdri.jobdri_api.domain.jobposting.service; +import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; +import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository; +import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; import com.jobdri.jobdri_api.domain.classification.repository.DetailClassificationRepository; import com.jobdri.jobdri_api.domain.audit.annotation.AuditLogEvent; @@ -10,6 +13,8 @@ import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse; import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; import com.jobdri.jobdri_api.domain.jobposting.repository.JobPostingRepository; +import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository; +import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplySequenceRepository; import com.jobdri.jobdri_api.domain.user.entity.User; import com.jobdri.jobdri_api.domain.user.service.UserService; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; @@ -29,6 +34,11 @@ public class JobPostingService { private final CompanyRepository companyRepository; private final DetailClassificationRepository detailClassificationRepository; private final UserService userService; + private final MockApplySequenceRepository mockApplySequenceRepository; + private final MockApplyRepository mockApplyRepository; + private final QuestionRepository questionRepository; + private final AnalysisRepository analysisRepository; + private final QuestionAnalysisRepository questionAnalysisRepository; @Transactional @AuditLogEvent(action = "JOB_POSTING_CREATE", targetType = "JOB_POSTING", targetId = "#result.getJobPostingId()") @@ -89,6 +99,20 @@ public List getJobPostingsByCompany(User user, Long companyI .toList(); } + @Transactional + @AuditLogEvent(action = "JOB_POSTING_DELETE", targetType = "JOB_POSTING", targetId = "#arg1") + public void deleteJobPosting(User user, Long jobPostingId) { + User validatedUser = userService.validateUser(user); + JobPosting jobPosting = getOwnedJobPosting(validatedUser, jobPostingId); + + questionAnalysisRepository.deleteAllByJobPostingId(jobPostingId); + questionRepository.deleteAllByJobPostingId(jobPostingId); + analysisRepository.deleteAllByJobPostingId(jobPostingId); + mockApplyRepository.deleteAllByJobPostingId(jobPostingId); + mockApplySequenceRepository.deleteAllByUserIdAndJobPostingId(validatedUser.getId(), jobPostingId); + jobPostingRepository.delete(jobPosting); + } + public JobPosting getOwnedJobPosting(User user, Long jobPostingId) { JobPosting jobPosting = jobPostingRepository.findById(jobPostingId) .orElseThrow(() -> new GeneralException( diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java index cb758ce..c57e594 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java @@ -2,6 +2,7 @@ import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; 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; @@ -14,6 +15,13 @@ public interface MockApplyRepository extends JpaRepository { List findAllByUserIdAndJobPostingIdOrderByIdAsc(Long userId, Long jobPostingId); long countByUserIdAndJobPostingId(Long userId, Long jobPostingId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from MockApply ma + where ma.jobPosting.id = :jobPostingId + """) + void deleteAllByJobPostingId(Long jobPostingId); + @Query(""" select ma from MockApply ma diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplySequenceRepository.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplySequenceRepository.java index 8a11c41..93bea8d 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplySequenceRepository.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplySequenceRepository.java @@ -22,4 +22,6 @@ Optional findByKeyForUpdate( @Param("userId") Long userId, @Param("jobPostingId") Long jobPostingId ); + + void deleteAllByUserIdAndJobPostingId(Long userId, Long jobPostingId); } diff --git a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingServiceTest.java new file mode 100644 index 0000000..df8a609 --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingServiceTest.java @@ -0,0 +1,149 @@ +package com.jobdri.jobdri_api.domain.jobposting.service; + +import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; +import com.jobdri.jobdri_api.domain.analysis.entity.Question; +import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis; +import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysisStatus; +import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; +import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository; +import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; +import com.jobdri.jobdri_api.domain.classification.entity.Classification; +import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; +import com.jobdri.jobdri_api.domain.classification.entity.MiddleClassification; +import com.jobdri.jobdri_api.domain.classification.repository.ClassificationRepository; +import com.jobdri.jobdri_api.domain.classification.repository.DetailClassificationRepository; +import com.jobdri.jobdri_api.domain.company.entity.Company; +import com.jobdri.jobdri_api.domain.company.entity.CompanySize; +import com.jobdri.jobdri_api.domain.company.repository.CompanyRepository; +import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting; +import com.jobdri.jobdri_api.domain.jobposting.repository.JobPostingRepository; +import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType; +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply; +import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplySequence; +import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository; +import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplySequenceRepository; +import com.jobdri.jobdri_api.domain.user.entity.User; +import com.jobdri.jobdri_api.domain.user.repository.UserRepository; +import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; +import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class JobPostingServiceTest { + + @Autowired + private JobPostingService jobPostingService; + + @Autowired + private JobPostingRepository jobPostingRepository; + + @Autowired + private MockApplyRepository mockApplyRepository; + + @Autowired + private MockApplySequenceRepository mockApplySequenceRepository; + + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private AnalysisRepository analysisRepository; + + @Autowired + private QuestionAnalysisRepository questionAnalysisRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private CompanyRepository companyRepository; + + @Autowired + private ClassificationRepository classificationRepository; + + @Autowired + private DetailClassificationRepository detailClassificationRepository; + + @Test + @DisplayName("채용 공고를 삭제하면 연결된 모의 서류 결과도 함께 삭제한다") + void deleteJobPostingDeletesMockApplyResults() { + User user = saveUser("job-posting-delete@example.com"); + JobPosting jobPosting = saveJobPosting(user); + MockApply mockApply = mockApplyRepository.save(MockApply.create(user, jobPosting, ApplyType.MOCK, 1)); + Question question = questionRepository.save(Question.create(mockApply, "지원 동기", 1000, "답변입니다.")); + Analysis analysis = analysisRepository.save(Analysis.create(mockApply, 80, 81, 82, 83, "분석 결과입니다.")); + QuestionAnalysis questionAnalysis = questionAnalysisRepository.save(QuestionAnalysis.create( + question, + analysis, + "답변입니다.", + "근거가 부족합니다.", + "구체적인 성과를 포함해 답변했습니다.", + QuestionAnalysisStatus.MENTIONED, + 0, + 5 + )); + mockApplySequenceRepository.save(MockApplySequence.create(user.getId(), jobPosting.getId(), 1)); + + jobPostingService.deleteJobPosting(user, jobPosting.getId()); + jobPostingRepository.flush(); + + assertThat(jobPostingRepository.findById(jobPosting.getId())).isEmpty(); + assertThat(mockApplyRepository.findById(mockApply.getId())).isEmpty(); + assertThat(questionRepository.findById(question.getId())).isEmpty(); + assertThat(analysisRepository.findById(analysis.getId())).isEmpty(); + assertThat(questionAnalysisRepository.findById(questionAnalysis.getId())).isEmpty(); + assertThat(mockApplySequenceRepository.findByKeyForUpdate(user.getId(), jobPosting.getId())).isEmpty(); + } + + @Test + @DisplayName("다른 사용자의 채용 공고는 삭제할 수 없다") + void deleteJobPostingThrowsWhenUserDoesNotOwnJobPosting() { + User owner = saveUser("job-posting-delete-owner@example.com"); + User other = saveUser("job-posting-delete-other@example.com"); + JobPosting jobPosting = saveJobPosting(owner); + + assertThatThrownBy(() -> jobPostingService.deleteJobPosting(other, jobPosting.getId())) + .isInstanceOf(GeneralException.class) + .extracting("code") + .isEqualTo(GeneralErrorCode.FORBIDDEN); + + assertThat(jobPostingRepository.findById(jobPosting.getId())).isPresent(); + } + + private User saveUser(String email) { + return userRepository.save(User.signup("테스트 사용자", email, "encoded-password")); + } + + private JobPosting saveJobPosting(User user) { + Company company = companyRepository.save(Company.create("삭제 테스트 기업 " + UUID.randomUUID(), CompanySize.MEDIUM)); + DetailClassification detailClassification = saveDetailClassification(); + return jobPostingRepository.save(JobPosting.create( + user, + company, + detailClassification, + "주요 업무", + "자격 요건", + "우대 사항" + )); + } + + private DetailClassification saveDetailClassification() { + Classification classification = Classification.create("삭제 테스트 대분류 " + UUID.randomUUID()); + MiddleClassification middleClassification = classification.addMiddleClassification("삭제 테스트 중분류"); + DetailClassification detailClassification = middleClassification.addDetailClassification("삭제 테스트 소분류"); + classificationRepository.save(classification); + return detailClassificationRepository.findById(detailClassification.getId()).orElseThrow(); + } +}