Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.jobdri.jobdri_api.domain.mockapply.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Table(
name = "mock_apply_sequences",
uniqueConstraints = @UniqueConstraint(
name = "uk_mock_apply_sequences_key",
columnNames = {"user_id", "company_id", "detail_classification_id"}
)
)
public class MockApplySequence {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@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(nullable = false)
private int lastSequence;

public static MockApplySequence create(
Long userId,
Long companyId,
Long detailClassificationId,
int lastSequence
) {
return MockApplySequence.builder()
.userId(userId)
.companyId(companyId)
.detailClassificationId(detailClassificationId)
.lastSequence(lastSequence)
.build();
}

public int incrementAndGet() {
lastSequence++;
return lastSequence;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ public interface MockApplyRepository extends JpaRepository<MockApply, Long> {
List<MockApply> findAllByJobPostingId(Long jobPostingId);
long countByUserIdAndJobPostingId(Long userId, Long jobPostingId);

@Query("""
select coalesce(max(ma.sequence), 0)
from MockApply ma
where ma.user.id = :userId
and ma.jobPosting.company.id = :companyId
and ma.jobPosting.detailClassification.id = :detailClassificationId
""")
int findMaxSequenceByUserIdAndCompanyIdAndDetailClassificationId(
@Param("userId") Long userId,
@Param("companyId") Long companyId,
@Param("detailClassificationId") Long detailClassificationId
);

default int calculateSequence(MockApply mockApply) {
if (mockApply.getSequence() != null && mockApply.getSequence() > 0) {
return mockApply.getSequence();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.jobdri.jobdri_api.domain.mockapply.repository;

import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplySequence;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface MockApplySequenceRepository extends JpaRepository<MockApplySequence, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
select mas
from MockApplySequence mas
where mas.userId = :userId
and mas.companyId = :companyId
and mas.detailClassificationId = :detailClassificationId
""")
Optional<MockApplySequence> findByKeyForUpdate(
@Param("userId") Long userId,
@Param("companyId") Long companyId,
@Param("detailClassificationId") Long detailClassificationId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.jobdri.jobdri_api.domain.mockapply.service;

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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MockApplySequenceService {

private final MockApplySequenceRepository mockApplySequenceRepository;
private final MockApplyRepository mockApplyRepository;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public int allocate(Long userId, Long companyId, Long detailClassificationId) {
MockApplySequence sequence = mockApplySequenceRepository
.findByKeyForUpdate(userId, companyId, detailClassificationId)
.orElseGet(() -> mockApplySequenceRepository.saveAndFlush(
MockApplySequence.create(
userId,
companyId,
detailClassificationId,
mockApplyRepository.findMaxSequenceByUserIdAndCompanyIdAndDetailClassificationId(
userId,
companyId,
detailClassificationId
)
)
));

return sequence.incrementAndGet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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";

Expand All @@ -50,6 +51,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()")
Expand Down Expand Up @@ -201,10 +203,31 @@ private int resolveSequence(User user, JobPosting jobPosting, Integer requestedS
if (isPositiveSequence(requestedSequence)) {
return requestedSequence;
}
return Math.toIntExact(mockApplyRepository.countByUserIdAndJobPostingId(
user.getId(),
jobPosting.getId()
)) + 1;
return allocateSequence(user, jobPosting);
}

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,
"모의 서류 지원 순번 할당에 실패했습니다."
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private boolean isPositiveSequence(Integer sequence) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ void createActualApplyWithRequestedSequence() {
assertThat(sequenceResponse.totalCount()).isEqualTo(3);
}

@Test
@DisplayName("같은 회사와 직무의 다른 공고로 재지원하면 다음 순번을 저장한다")
void createActualApplySequencesAcrossSameCompanyAndDetailJobPostings() {
User user = saveUser("actual-apply-retry-sequence@example.com");
Company company = saveCompany("재지원 기업 " + UUID.randomUUID(), CompanySize.MEDIUM);
DetailClassification detailClassification = saveDetailClassification("백엔드 개발");
JobPosting firstJobPosting = saveJobPosting(user, company, detailClassification, "첫 번째 JD");
JobPosting secondJobPosting = saveJobPosting(user, company, detailClassification, "복제된 JD");

MockApplyCreateResponse firstResponse = mockApplyService.createActualApply(user, firstJobPosting.getId());
MockApplyCreateResponse secondResponse = mockApplyService.createActualApply(user, secondJobPosting.getId());

MockApply secondMockApply = mockApplyRepository.findById(secondResponse.mockApplyId()).orElseThrow();
assertThat(firstResponse.sequence()).isEqualTo(1);
assertThat(secondResponse.sequence()).isEqualTo(2);
assertThat(secondMockApply.getSequence()).isEqualTo(2);
}

@Test
@DisplayName("요청 순번이 0 이하이면 다음 유효 순번을 저장한다")
void createActualApplyIgnoresNonPositiveRequestedSequence() {
Expand Down Expand Up @@ -391,6 +409,22 @@ private JobPosting saveJobPosting(User user, String detailName) {
});
}

private JobPosting saveJobPosting(
User user,
Company company,
DetailClassification detailClassification,
String task
) {
return inNewTransaction(() -> jobPostingRepository.save(JobPosting.create(
userRepository.findById(user.getId()).orElseThrow(),
companyRepository.findById(company.getId()).orElseThrow(),
detailClassificationRepository.findById(detailClassification.getId()).orElseThrow(),
task,
"자격 요건",
"우대 사항"
)));
}

private DetailClassification saveDetailClassification(String detailName) {
return inNewTransaction(() -> saveDetailClassificationInCurrentTransaction(detailName));
}
Expand Down
Loading