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 cdca783..b5bdac6 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 @@ -11,6 +11,13 @@ public interface AnalysisRepository extends JpaRepository { Optional findByMockApplyId(Long mockApplyId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from Analysis a + where a.mockApply.id = :mockApplyId + """) + void deleteByMockApplyId(@Param("mockApplyId") Long mockApplyId); + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" delete from Analysis a 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 0fef233..0e19624 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 @@ -14,6 +14,22 @@ public interface QuestionAnalysisRepository extends JpaRepository 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.id = :mockApplyId + ) + or qa.question.id in ( + select q.id + from Question q + where q.mockApply.id = :mockApplyId + ) + """) + void deleteAllByMockApplyId(@Param("mockApplyId") Long mockApplyId); + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" delete from QuestionAnalysis qa diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java index 48ee421..b984713 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.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.PostMapping; @@ -49,6 +50,57 @@ public ApiResponse getMyMockApplies( ); } + @Operation( + summary = "모의 서류 지원 단건 삭제", + description = "mockApplyId에 해당하는 모의 서류 지원과 연결된 문항, 분석 결과만 삭제합니다. 같은 공고의 다른 모의 지원은 유지됩니다." + ) + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "모의 서류 지원 삭제 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 삭제되었습니다.\",\"result\":null,\"error\":null}") + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 정보 누락", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":false,\"code\":\"AUTH_4011\",\"message\":\"인증 정보가 누락되었습니다.\",\"result\":null,\"error\":\"인증 정보가 누락되었습니다.\"}") + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "다른 사용자의 모의 서류 지원 접근", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":false,\"code\":\"AUTH_4031\",\"message\":\"해당 모의 서류 지원에 접근할 수 없습니다.\",\"result\":null,\"error\":\"해당 모의 서류 지원에 접근할 수 없습니다.\"}") + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "모의 서류 지원 없음", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject(value = "{\"isSuccess\":false,\"code\":\"MOCK_APPLY_4041\",\"message\":\"해당 모의 서류 지원을 찾을 수 없습니다. mockApplyId=999\",\"result\":null,\"error\":\"해당 모의 서류 지원을 찾을 수 없습니다. mockApplyId=999\"}") + ) + ) + }) + @DeleteMapping("/{mockApplyId}") + public ApiResponse deleteMockApply( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @PathVariable Long mockApplyId + ) { + mockApplyService.deleteMockApply(userDetails.getUser(), mockApplyId); + return ApiResponse.onSuccess("모의 서류 지원이 삭제되었습니다.", null); + } + @Operation( summary = "실제 공고 기반 모의 서류 지원 생성", description = "공고 텍스트/URL 추출, 공고 저장, 사용자 확인 및 수정이 선행된 뒤 저장된 채용 공고 ID를 기준으로 로그인 사용자의 ACTUAL 타입 모의 서류 지원을 생성합니다." 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 c57e594..8e5623a 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 @@ -22,6 +22,13 @@ public interface MockApplyRepository extends JpaRepository { """) void deleteAllByJobPostingId(Long jobPostingId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + delete from MockApply ma + where ma.id = :mockApplyId + """) + void deleteByMockApplyId(@Param("mockApplyId") Long mockApplyId); + @Query(""" select ma from MockApply ma diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java index dce7cc9..fc0a8bc 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java @@ -1,6 +1,8 @@ package com.jobdri.jobdri_api.domain.mockapply.service; import com.jobdri.jobdri_api.domain.analysis.entity.Question; +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.company.entity.Company; import com.jobdri.jobdri_api.domain.company.repository.CompanyRepository; @@ -48,6 +50,8 @@ public class MockApplyService { private static final String UNIQUE_VIOLATION_SQL_STATE = "23505"; private final MockApplyRepository mockApplyRepository; + private final QuestionAnalysisRepository questionAnalysisRepository; + private final AnalysisRepository analysisRepository; private final JobPostingRepository jobPostingRepository; private final CompanyRepository companyRepository; private final QuestionRepository questionRepository; @@ -213,6 +217,18 @@ public MockApplyHomeResponse getMyMockApplies(User user) { ); } + @Transactional + @AuditLogEvent(action = "MOCK_APPLY_DELETE", targetType = "MOCK_APPLY", targetId = "#arg1") + public void deleteMockApply(User user, Long mockApplyId) { + User validatedUser = userService.validateUser(user); + getOwnedMockApply(validatedUser, mockApplyId); + + questionAnalysisRepository.deleteAllByMockApplyId(mockApplyId); + analysisRepository.deleteByMockApplyId(mockApplyId); + questionRepository.deleteAllByMockApplyId(mockApplyId); + mockApplyRepository.deleteByMockApplyId(mockApplyId); + } + private List filterByCompletion( List items, boolean completed diff --git a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java index ec8741f..e463153 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java @@ -2,7 +2,10 @@ 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; @@ -65,6 +68,9 @@ class MockApplyServiceTest { @Autowired private AnalysisRepository analysisRepository; + @Autowired + private QuestionAnalysisRepository questionAnalysisRepository; + @Autowired private QuestionRepository questionRepository; @@ -349,6 +355,34 @@ void getMyMockApplies() { assertThat(response.completed().get(0).resumePath()).isEqualTo("/mock-applies/" + completed.getId() + "/analysis"); } + @Test + @DisplayName("모의 서류 지원을 삭제하면 해당 지원의 문항과 분석만 삭제한다") + void deleteMockApplyDeletesOnlyTargetMockApplyResults() { + User user = saveUser("mock-apply-delete@example.com"); + JobPosting jobPosting = saveJobPosting(user, "백엔드 개발"); + MockApply target = saveMockApply(user, jobPosting, ApplyType.MOCK, 1); + MockApply remaining = saveMockApply(user, jobPosting, ApplyType.MOCK, 2); + Question targetQuestion = saveQuestion(target, "삭제 대상 문항", 1000, "삭제 대상 답변"); + Question remainingQuestion = saveQuestion(remaining, "유지 대상 문항", 1000, "유지 대상 답변"); + Analysis targetAnalysis = saveAnalysis(target, 70); + Analysis remainingAnalysis = saveAnalysis(remaining, 80); + QuestionAnalysis targetQuestionAnalysis = saveQuestionAnalysis(targetQuestion, targetAnalysis); + QuestionAnalysis remainingQuestionAnalysis = saveQuestionAnalysis(remainingQuestion, remainingAnalysis); + + mockApplyService.deleteMockApply(user, target.getId()); + mockApplyRepository.flush(); + + assertThat(mockApplyRepository.findById(target.getId())).isEmpty(); + assertThat(questionRepository.findById(targetQuestion.getId())).isEmpty(); + assertThat(analysisRepository.findById(targetAnalysis.getId())).isEmpty(); + assertThat(questionAnalysisRepository.findById(targetQuestionAnalysis.getId())).isEmpty(); + assertThat(jobPostingRepository.findById(jobPosting.getId())).isPresent(); + assertThat(mockApplyRepository.findById(remaining.getId())).isPresent(); + assertThat(questionRepository.findById(remainingQuestion.getId())).isPresent(); + assertThat(analysisRepository.findById(remainingAnalysis.getId())).isPresent(); + assertThat(questionAnalysisRepository.findById(remainingQuestionAnalysis.getId())).isPresent(); + } + @Test @DisplayName("존재하지 않는 공고 ID로 ACTUAL 타입 지원 생성 시 예외를 던진다") void createActualApplyThrowsWhenJobPostingNotFound() { @@ -436,6 +470,22 @@ void getMockApplyJobPostingThrowsWhenForbidden() { .isEqualTo(GeneralErrorCode.FORBIDDEN); } + @Test + @DisplayName("다른 사용자의 모의 서류 지원은 삭제할 수 없다") + void deleteMockApplyThrowsWhenForbidden() { + User owner = saveUser("delete-owner@example.com"); + User otherUser = saveUser("delete-other@example.com"); + JobPosting jobPosting = saveJobPosting(owner, "데이터 분석"); + MockApply mockApply = saveMockApply(owner, jobPosting, ApplyType.MOCK, 1); + + assertThatThrownBy(() -> mockApplyService.deleteMockApply(otherUser, mockApply.getId())) + .isInstanceOf(GeneralException.class) + .extracting("code") + .isEqualTo(GeneralErrorCode.FORBIDDEN); + + assertThat(mockApplyRepository.findById(mockApply.getId())).isPresent(); + } + private User saveUser(String email) { return inNewTransaction(() -> userRepository.save(User.signup("테스트 사용자", email, "encoded-password"))); } @@ -502,6 +552,30 @@ private Question saveQuestion(MockApply mockApply, String content, int limit, St ))); } + private Analysis saveAnalysis(MockApply mockApply, int score) { + return inNewTransaction(() -> analysisRepository.save(Analysis.create( + mockApplyRepository.findById(mockApply.getId()).orElseThrow(), + score, + score, + score, + score, + "분석 결과입니다." + ))); + } + + private QuestionAnalysis saveQuestionAnalysis(Question question, Analysis analysis) { + return inNewTransaction(() -> questionAnalysisRepository.save(QuestionAnalysis.create( + questionRepository.findById(question.getId()).orElseThrow(), + analysisRepository.findById(analysis.getId()).orElseThrow(), + "답변입니다.", + "근거가 부족합니다.", + "구체적인 성과를 포함해 답변했습니다.", + QuestionAnalysisStatus.MENTIONED, + 0, + 5 + ))); + } + private T inNewTransaction(Supplier action) { TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);