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
Expand Up @@ -59,7 +59,7 @@ public ApiResponse<MockApplyHomeResponse> getMyMockApplies(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"ACTUAL\"},\"error\":null}")
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"ACTUAL\",\"sequence\":1},\"error\":null}")
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
Expand Down Expand Up @@ -88,7 +88,7 @@ public ApiResponse<MockApplyCreateResponse> createActualApply(
) {
return ApiResponse.onSuccess(
"모의 서류 지원이 생성되었습니다.",
mockApplyService.createActualApply(userDetails.getUser(), request.jobPostingId())
mockApplyService.createActualApply(userDetails.getUser(), request.jobPostingId(), request.sequence())
);
}

Expand All @@ -103,7 +103,7 @@ public ApiResponse<MockApplyCreateResponse> createActualApply(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"MOCK\"},\"error\":null}")
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"MOCK\",\"sequence\":1},\"error\":null}")
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
Expand Down Expand Up @@ -132,7 +132,11 @@ public ApiResponse<MockApplyCreateResponse> createMockApplyFromJobPosting(
) {
return ApiResponse.onSuccess(
"모의 서류 지원이 생성되었습니다.",
mockApplyService.createMockApplyFromJobPosting(userDetails.getUser(), request.jobPostingId())
mockApplyService.createMockApplyFromJobPosting(
userDetails.getUser(),
request.jobPostingId(),
request.sequence()
)
);
}

Expand All @@ -147,7 +151,7 @@ public ApiResponse<MockApplyCreateResponse> createMockApplyFromJobPosting(
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"MOCK\"},\"error\":null}")
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"모의 서류 지원이 생성되었습니다.\",\"result\":{\"jobPostingId\":1,\"mockApplyId\":10,\"applyType\":\"MOCK\",\"sequence\":1},\"error\":null}")
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.jobdri.jobdri_api.domain.mockapply.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record MockApplyCreateActualRequest(
@NotNull(message = "공고 ID는 필수입니다.")
Long jobPostingId
Long jobPostingId,

@Positive(message = "지원 순번은 1 이상이어야 합니다.")
Integer sequence
) {
public MockApplyCreateActualRequest(Long jobPostingId) {
this(jobPostingId, null);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.jobdri.jobdri_api.domain.mockapply.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record MockApplyCreateMockFromJobPostingRequest(
@NotNull(message = "공고 ID는 필수입니다.")
Long jobPostingId
Long jobPostingId,

@Positive(message = "지원 순번은 1 이상이어야 합니다.")
Integer sequence
) {
public MockApplyCreateMockFromJobPostingRequest(Long jobPostingId) {
this(jobPostingId, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingMockGenerateRequest;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record MockApplyCreateMockRequest(
@NotNull(message = "회사 ID는 필수입니다.")
Expand All @@ -11,8 +12,15 @@ public record MockApplyCreateMockRequest(
Long middleClassificationId,

@NotNull(message = "소분류 ID는 필수입니다.")
Long detailClassificationId
Long detailClassificationId,

@Positive(message = "지원 순번은 1 이상이어야 합니다.")
Integer sequence
) {
public MockApplyCreateMockRequest(Long companyId, Long middleClassificationId, Long detailClassificationId) {
this(companyId, middleClassificationId, detailClassificationId, null);
}

public JobPostingMockGenerateRequest toJobPostingMockGenerateRequest() {
return new JobPostingMockGenerateRequest(companyId, middleClassificationId, detailClassificationId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
public record MockApplyCreateResponse(
Long jobPostingId,
Long mockApplyId,
ApplyType applyType
ApplyType applyType,
int sequence
) {
public static MockApplyCreateResponse from(MockApply mockApply) {
return new MockApplyCreateResponse(
mockApply.getJobPosting().getId(),
mockApply.getId(),
mockApply.getApplyType()
mockApply.getApplyType(),
mockApply.getSequence()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Table(name = "mock_applies")
@Table(
name = "mock_applies",
uniqueConstraints = @UniqueConstraint(
name = "uk_mock_apply_user_posting_sequence",
columnNames = {"user_id", "job_posting_id", "sequence"}
)
)
public class MockApply {

@Id
Expand All @@ -39,6 +45,8 @@ public class MockApply {
@Column(nullable = false)
private MockApplyStatus status;

private Integer sequence;

@Column(nullable = false)
private LocalDateTime createdAt;

Expand All @@ -50,11 +58,16 @@ public class MockApply {
private List<Question> questions = new ArrayList<>();

public static MockApply create(User user, JobPosting jobPosting, ApplyType applyType) {
return create(user, jobPosting, applyType, null);
}

public static MockApply create(User user, JobPosting jobPosting, ApplyType applyType, Integer sequence) {
return MockApply.builder()
.user(user)
.jobPosting(jobPosting)
.applyType(applyType)
.status(MockApplyStatus.APPLICATION_CREATED)
.sequence(sequence)
.createdAt(LocalDateTime.now())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public interface MockApplyRepository extends JpaRepository<MockApply, Long> {
long countByUserIdAndJobPostingId(Long userId, Long jobPostingId);

default int calculateSequence(MockApply mockApply) {
if (mockApply.getSequence() != null && mockApply.getSequence() > 0) {
return mockApply.getSequence();
}

return Math.toIntExact(countSequenceByUserIdAndJobPostingId(
mockApply.getUser().getId(),
mockApply.getJobPosting().getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.jobdri.jobdri_api.domain.mockapply.service;

import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MockApplyPersistenceService {

private final MockApplyRepository mockApplyRepository;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public MockApply saveAndFlush(MockApply mockApply) {
return mockApplyRepository.saveAndFlush(mockApply);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,75 @@
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MockApplyService {
private static final int SEQUENCE_SAVE_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";

private final MockApplyRepository mockApplyRepository;
private final JobPostingRepository jobPostingRepository;
private final CompanyRepository companyRepository;
private final MockJobPostingGenerationService mockJobPostingGenerationService;
private final JobPostingService jobPostingService;
private final UserService userService;
private final MockApplyPersistenceService mockApplyPersistenceService;

@Transactional
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
public MockApplyCreateResponse createActualApply(User user, Long jobPostingId) {
return createActualApply(user, jobPostingId, null);
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
public MockApplyCreateResponse createActualApply(User user, Long jobPostingId, Integer sequence) {
User validatedUser = userService.validateUser(user);
JobPosting jobPosting = jobPostingService.getOwnedJobPosting(validatedUser, jobPostingId);

MockApply mockApply = MockApply.create(validatedUser, jobPosting, ApplyType.ACTUAL);
return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply));
MockApply mockApply = saveMockApplyWithSequence(
validatedUser,
jobPosting,
ApplyType.ACTUAL,
sequence
);
return MockApplyCreateResponse.from(mockApply);
}

@Transactional
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
public MockApplyCreateResponse createMockApplyFromJobPosting(User user, Long jobPostingId) {
return createMockApplyFromJobPosting(user, jobPostingId, null);
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
public MockApplyCreateResponse createMockApplyFromJobPosting(User user, Long jobPostingId, Integer sequence) {
User validatedUser = userService.validateUser(user);
JobPosting jobPosting = jobPostingService.getOwnedJobPosting(validatedUser, jobPostingId);

MockApply mockApply = MockApply.create(validatedUser, jobPosting, ApplyType.MOCK);
return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply));
MockApply mockApply = saveMockApplyWithSequence(
validatedUser,
jobPosting,
ApplyType.MOCK,
sequence
);
return MockApplyCreateResponse.from(mockApply);
}

@Transactional
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
public MockApplyCreateResponse createMockApply(User user, MockApplyCreateMockRequest request) {
User validatedUser = userService.validateUser(user);
Expand Down Expand Up @@ -90,8 +121,13 @@ public MockApplyCreateResponse createMockApply(User user, MockApplyCreateMockReq
"생성된 모의 공고를 찾을 수 없습니다. jobPostingId=" + savedJobPostingId
));

MockApply mockApply = MockApply.create(validatedUser, savedJobPosting, ApplyType.MOCK);
return MockApplyCreateResponse.from(mockApplyRepository.save(mockApply));
MockApply mockApply = saveMockApplyWithSequence(
validatedUser,
savedJobPosting,
ApplyType.MOCK,
request.sequence()
);
return MockApplyCreateResponse.from(mockApply);
}

public JobPostingResponse getMockApplyJobPosting(User user, Long mockApplyId) {
Expand All @@ -109,8 +145,9 @@ public MockApplySequenceResponse getMockApplySequence(User user, Long mockApplyI
mockApplyRepository.countByUserIdAndJobPostingId(validatedUser.getId(), jobPostingId)
);
int sequence = mockApplyRepository.calculateSequence(mockApply);
totalCount = Math.max(totalCount, sequence);

if (sequence < 1 || sequence > totalCount) {
if (sequence < 1) {
throw new GeneralException(
GeneralErrorCode.MOCK_APPLY_NOT_FOUND,
"해당 공고에 연결된 모의 서류 지원 순서를 찾을 수 없습니다. mockApplyId=" + mockApplyId
Expand Down Expand Up @@ -159,4 +196,77 @@ private MockApply getOwnedMockApply(User user, Long mockApplyId) {

return mockApply;
}

private int resolveSequence(User user, JobPosting jobPosting, Integer requestedSequence) {
if (isPositiveSequence(requestedSequence)) {
return requestedSequence;
}
return Math.toIntExact(mockApplyRepository.countByUserIdAndJobPostingId(
user.getId(),
jobPosting.getId()
)) + 1;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private boolean isPositiveSequence(Integer sequence) {
return sequence != null && sequence > 0;
}

private MockApply saveMockApplyWithSequence(
User user,
JobPosting jobPosting,
ApplyType applyType,
Integer requestedSequence
) {
int sequence = resolveSequence(user, jobPosting, requestedSequence);
for (int attempt = 0; attempt < SEQUENCE_SAVE_MAX_RETRY; attempt++) {
try {
return mockApplyPersistenceService.saveAndFlush(MockApply.create(user, jobPosting, applyType, sequence));
} catch (DataIntegrityViolationException e) {
if (!isSequenceUniqueConflict(e)) {
throw e;
}
if (isPositiveSequence(requestedSequence)) {
throw new GeneralException(
GeneralErrorCode.INVALID_PARAMETER,
"이미 사용 중인 지원 순번입니다. sequence=" + requestedSequence
);
}
sequence++;
}
}

throw new GeneralException(
GeneralErrorCode.INTERNAL_SERVER_ERROR,
"모의 서류 지원 순번 생성에 실패했습니다."
);
}

private boolean isSequenceUniqueConflict(DataIntegrityViolationException exception) {
Throwable cause = exception;
while (cause != null) {
if (cause instanceof org.hibernate.exception.ConstraintViolationException constraintViolation
&& isSequenceConstraintName(constraintViolation.getConstraintName())) {
return true;
}
if (cause instanceof SQLException sqlException
&& UNIQUE_VIOLATION_SQL_STATE.equals(sqlException.getSQLState())
&& containsSequenceConstraint(sqlException.getMessage())) {
return true;
}
if (containsSequenceConstraint(cause.getMessage())) {
return true;
}
cause = cause.getCause();
}
return false;
}

private boolean isSequenceConstraintName(String constraintName) {
return containsSequenceConstraint(constraintName);
}

private boolean containsSequenceConstraint(String value) {
return value != null
&& value.toLowerCase(Locale.ROOT).contains(SEQUENCE_UNIQUE_CONSTRAINT);
}
}
Loading
Loading