From 8645fd8e7380500a8a37a01cf2e6d79f0e49e214 Mon Sep 17 00:00:00 2001 From: wooh Date: Wed, 27 May 2026 15:55:12 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Fix]=20=EC=9E=AC=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20=EC=97=B0=EA=B2=B0=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재지원 시 새 JobPosting을 복제 생성하지 않고 원본 공고에 새 MockApply만 생성 - MockApply sequence 자동 할당 기준을 user + jobPostingId로 변경 - 같은 회사/직무의 다른 공고는 각각 독립된 sequence를 갖도록 테스트 수정 - 재지원 시 같은 jobPostingId 안에서 sequence가 증가하는지 검증 --- .../controller/MockApplyController.java | 2 +- .../repository/MockApplyRepository.java | 11 ++++++ .../mockapply/service/MockApplyService.java | 38 +++---------------- .../service/MockApplyServiceTest.java | 14 ++++--- 4 files changed, 25 insertions(+), 40 deletions(-) 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 873b89a..48ee421 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 @@ -199,7 +199,7 @@ public ApiResponse createMockApply( } @Operation( - summary = "모의 서류 지원 재도전 - ", + summary = "모의 서류 지원 재도전", description = "기존 모의 서류 지원의 공고와 선택 문항을 복사해 새 회차의 모의 서류 지원을 생성합니다. 답변은 비워진 상태로 자소서 입력 단계부터 다시 진행합니다." ) @ApiResponses(value = { 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 48b7183..cb758ce 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 @@ -25,6 +25,17 @@ public interface MockApplyRepository extends JpaRepository { """) Optional findByIdWithJobPosting(@Param("mockApplyId") Long mockApplyId); + @Query(""" + select coalesce(max(ma.sequence), 0) + from MockApply ma + where ma.user.id = :userId + and ma.jobPosting.id = :jobPostingId + """) + int findMaxSequenceByUserIdAndJobPostingId( + @Param("userId") Long userId, + @Param("jobPostingId") Long jobPostingId + ); + @Query(""" select coalesce(max(ma.sequence), 0) 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 d5838e4..fb6c10d 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 @@ -43,7 +43,6 @@ @Transactional(readOnly = true) public class MockApplyService { private static final int SEQUENCE_SAVE_MAX_RETRY = 5; - private static final int SEQUENCE_ALLOCATE_MAX_RETRY = 5; private static final String SEQUENCE_UNIQUE_CONSTRAINT = "uk_mock_apply_user_posting_sequence"; private static final String UNIQUE_VIOLATION_SQL_STATE = "23505"; @@ -55,7 +54,6 @@ public class MockApplyService { private final JobPostingService jobPostingService; private final UserService userService; private final MockApplyPersistenceService mockApplyPersistenceService; - private final MockApplySequenceService mockApplySequenceService; @Transactional(propagation = Propagation.NOT_SUPPORTED) @AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()") @@ -85,18 +83,9 @@ public MockApplyRetryResponse retryMockApply(User user, Long mockApplyId) { MockApply sourceMockApply = getOwnedMockApplyWithJobPosting(validatedUser, mockApplyId); JobPosting sourceJobPosting = sourceMockApply.getJobPosting(); - JobPosting clonedJobPosting = jobPostingRepository.save(JobPosting.create( - validatedUser, - sourceJobPosting.getCompany(), - sourceJobPosting.getDetailClassification(), - sourceJobPosting.getTask(), - sourceJobPosting.getRequirement(), - sourceJobPosting.getPreferred() - )); - MockApply retryMockApply = saveMockApplyWithSequence( validatedUser, - clonedJobPosting, + sourceJobPosting, sourceMockApply.getApplyType(), null ); @@ -267,27 +256,10 @@ private int resolveSequence(User user, JobPosting jobPosting, Integer requestedS } private int allocateSequence(User user, JobPosting jobPosting) { - for (int attempt = 0; attempt < SEQUENCE_ALLOCATE_MAX_RETRY; attempt++) { - try { - return mockApplySequenceService.allocate( - user.getId(), - jobPosting.getCompany().getId(), - jobPosting.getDetailClassification().getId() - ); - } catch (DataIntegrityViolationException e) { - if (attempt == SEQUENCE_ALLOCATE_MAX_RETRY - 1) { - throw new GeneralException( - GeneralErrorCode.INTERNAL_SERVER_ERROR, - "모의 서류 지원 순번 할당에 실패했습니다." - ); - } - } - } - - throw new GeneralException( - GeneralErrorCode.INTERNAL_SERVER_ERROR, - "모의 서류 지원 순번 할당에 실패했습니다." - ); + return mockApplyRepository.findMaxSequenceByUserIdAndJobPostingId( + user.getId(), + jobPosting.getId() + ) + 1; } private boolean isPositiveSequence(Integer sequence) { 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 39c3bbc..ec8741f 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 @@ -125,8 +125,8 @@ void createActualApplyWithRequestedSequence() { } @Test - @DisplayName("같은 회사와 직무의 다른 공고로 재지원하면 다음 순번을 저장한다") - void createActualApplySequencesAcrossSameCompanyAndDetailJobPostings() { + @DisplayName("같은 회사와 직무여도 다른 공고이면 순번을 따로 계산한다") + void createActualApplySequencesByJobPosting() { User user = saveUser("actual-apply-retry-sequence@example.com"); Company company = saveCompany("재지원 기업 " + UUID.randomUUID(), CompanySize.MEDIUM); DetailClassification detailClassification = saveDetailClassification("백엔드 개발"); @@ -138,8 +138,8 @@ void createActualApplySequencesAcrossSameCompanyAndDetailJobPostings() { MockApply secondMockApply = mockApplyRepository.findById(secondResponse.mockApplyId()).orElseThrow(); assertThat(firstResponse.sequence()).isEqualTo(1); - assertThat(secondResponse.sequence()).isEqualTo(2); - assertThat(secondMockApply.getSequence()).isEqualTo(2); + assertThat(secondResponse.sequence()).isEqualTo(1); + assertThat(secondMockApply.getSequence()).isEqualTo(1); } @Test @@ -236,7 +236,7 @@ void createMockApplyFromJobPosting() { } @Test - @DisplayName("기존 지원의 공고와 문항을 복사해 재도전 지원을 생성한다") + @DisplayName("기존 지원의 공고에 새 회차와 문항을 생성한다") void retryMockApply() { User user = saveUser("retry-mock-apply@example.com"); JobPosting jobPosting = saveJobPosting(user, "백엔드 개발"); @@ -250,12 +250,14 @@ void retryMockApply() { JobPosting retryJobPosting = jobPostingRepository.findById(response.jobPostingId()).orElseThrow(); List retryQuestions = questionRepository.findAllByMockApplyIdOrderByIdAsc(response.mockApplyId()); assertThat(response.sourceMockApplyId()).isEqualTo(sourceMockApply.getId()); + assertThat(response.jobPostingId()).isEqualTo(jobPosting.getId()); assertThat(response.sequence()).isEqualTo(2); assertThat(response.status()).isEqualTo(MockApplyStatus.ANSWER_WRITE); assertThat(retryMockApply.getApplyType()).isEqualTo(ApplyType.MOCK); + assertThat(retryMockApply.getJobPosting().getId()).isEqualTo(jobPosting.getId()); assertThat(retryMockApply.getSequence()).isEqualTo(2); assertThat(retryMockApply.getStatus()).isEqualTo(MockApplyStatus.ANSWER_WRITE); - assertThat(retryJobPosting.getId()).isNotEqualTo(jobPosting.getId()); + assertThat(retryJobPosting.getId()).isEqualTo(jobPosting.getId()); assertThat(retryJobPosting.getCompany().getId()).isEqualTo(jobPosting.getCompany().getId()); assertThat(retryJobPosting.getDetailClassification().getId()).isEqualTo(jobPosting.getDetailClassification().getId()); assertThat(retryJobPosting.getTask()).isEqualTo(jobPosting.getTask()); From 956a21b0c8b49cb1fcd1e6fe44e78f88685b3861 Mon Sep 17 00:00:00 2001 From: wooh Date: Wed, 27 May 2026 16:27:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20=EC=9E=AC=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EC=88=9C=EB=B2=88=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MockApplySequence 키를 userId + jobPostingId 기준으로 변경 - sequence 할당 시 PESSIMISTIC_WRITE 락으로 카운터 row 직렬화 - MockApplyService의 max+1 직접 계산 제거 - 카운터 생성 충돌 시 재시도 후 GeneralException으로 매핑 - 재지원 시 같은 공고 내 sequence가 안정적으로 증가하도록 보완 --- .../mockapply/entity/MockApplySequence.java | 15 ++++-------- .../MockApplySequenceRepository.java | 6 ++--- .../service/MockApplySequenceService.java | 12 ++++------ .../mockapply/service/MockApplyService.java | 23 +++++++++++++++---- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/entity/MockApplySequence.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/entity/MockApplySequence.java index 3245e8d..c443134 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/entity/MockApplySequence.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/entity/MockApplySequence.java @@ -16,7 +16,7 @@ name = "mock_apply_sequences", uniqueConstraints = @UniqueConstraint( name = "uk_mock_apply_sequences_key", - columnNames = {"user_id", "company_id", "detail_classification_id"} + columnNames = {"user_id", "job_posting_id"} ) ) public class MockApplySequence { @@ -28,25 +28,20 @@ public class MockApplySequence { @Column(name = "user_id", nullable = false) private Long userId; - @Column(name = "company_id", nullable = false) - private Long companyId; - - @Column(name = "detail_classification_id", nullable = false) - private Long detailClassificationId; + @Column(name = "job_posting_id", nullable = false) + private Long jobPostingId; @Column(nullable = false) private int lastSequence; public static MockApplySequence create( Long userId, - Long companyId, - Long detailClassificationId, + Long jobPostingId, int lastSequence ) { return MockApplySequence.builder() .userId(userId) - .companyId(companyId) - .detailClassificationId(detailClassificationId) + .jobPostingId(jobPostingId) .lastSequence(lastSequence) .build(); } 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 f519d1a..8a11c41 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 @@ -16,12 +16,10 @@ public interface MockApplySequenceRepository extends JpaRepository findByKeyForUpdate( @Param("userId") Long userId, - @Param("companyId") Long companyId, - @Param("detailClassificationId") Long detailClassificationId + @Param("jobPostingId") Long jobPostingId ); } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplySequenceService.java b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplySequenceService.java index d6cf1a8..54f332f 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplySequenceService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplySequenceService.java @@ -16,18 +16,16 @@ public class MockApplySequenceService { private final MockApplyRepository mockApplyRepository; @Transactional(propagation = Propagation.REQUIRES_NEW) - public int allocate(Long userId, Long companyId, Long detailClassificationId) { + public int allocate(Long userId, Long jobPostingId) { MockApplySequence sequence = mockApplySequenceRepository - .findByKeyForUpdate(userId, companyId, detailClassificationId) + .findByKeyForUpdate(userId, jobPostingId) .orElseGet(() -> mockApplySequenceRepository.saveAndFlush( MockApplySequence.create( userId, - companyId, - detailClassificationId, - mockApplyRepository.findMaxSequenceByUserIdAndCompanyIdAndDetailClassificationId( + jobPostingId, + mockApplyRepository.findMaxSequenceByUserIdAndJobPostingId( userId, - companyId, - detailClassificationId + jobPostingId ) ) )); 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 fb6c10d..dce7cc9 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 @@ -43,6 +43,7 @@ @Transactional(readOnly = true) public class MockApplyService { private static final int SEQUENCE_SAVE_MAX_RETRY = 5; + private static final int SEQUENCE_ALLOCATE_MAX_RETRY = 5; private static final String SEQUENCE_UNIQUE_CONSTRAINT = "uk_mock_apply_user_posting_sequence"; private static final String UNIQUE_VIOLATION_SQL_STATE = "23505"; @@ -54,6 +55,7 @@ public class MockApplyService { private final JobPostingService jobPostingService; private final UserService userService; private final MockApplyPersistenceService mockApplyPersistenceService; + private final MockApplySequenceService mockApplySequenceService; @Transactional(propagation = Propagation.NOT_SUPPORTED) @AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()") @@ -256,10 +258,23 @@ private int resolveSequence(User user, JobPosting jobPosting, Integer requestedS } private int allocateSequence(User user, JobPosting jobPosting) { - return mockApplyRepository.findMaxSequenceByUserIdAndJobPostingId( - user.getId(), - jobPosting.getId() - ) + 1; + for (int attempt = 0; attempt < SEQUENCE_ALLOCATE_MAX_RETRY; attempt++) { + try { + return mockApplySequenceService.allocate(user.getId(), jobPosting.getId()); + } catch (DataIntegrityViolationException e) { + if (attempt == SEQUENCE_ALLOCATE_MAX_RETRY - 1) { + throw new GeneralException( + GeneralErrorCode.INTERNAL_SERVER_ERROR, + "모의 서류 지원 순번 할당에 실패했습니다." + ); + } + } + } + + throw new GeneralException( + GeneralErrorCode.INTERNAL_SERVER_ERROR, + "모의 서류 지원 순번 할당에 실패했습니다." + ); } private boolean isPositiveSequence(Integer sequence) {