Skip to content

Commit ccc0d74

Browse files
authored
Merge pull request #85 from JobDri-Developer/feat/#38-mock-apply-retry
[Feat] 모의 서류 재도전 API 구현
2 parents b470b0c + febda50 commit ccc0d74

7 files changed

Lines changed: 189 additions & 0 deletions

File tree

src/main/java/com/jobdri/jobdri_api/domain/mockapply/controller/MockApplyController.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockRequest;
66
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse;
77
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse;
8+
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyRetryResponse;
89
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse;
910
import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse;
1011
import com.jobdri.jobdri_api.domain.mockapply.service.MockApplyService;
@@ -197,6 +198,32 @@ public ApiResponse<MockApplyCreateResponse> createMockApply(
197198
);
198199
}
199200

201+
@Operation(
202+
summary = "모의 서류 지원 재도전",
203+
description = "기존 모의 서류 지원의 공고와 선택 문항을 복사해 새 회차의 모의 서류 지원을 생성합니다. 답변은 비워진 상태로 자소서 입력 단계부터 다시 진행합니다."
204+
)
205+
@ApiResponses(value = {
206+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
207+
responseCode = "200",
208+
description = "재도전 모의 서류 지원 생성 성공",
209+
content = @Content(
210+
mediaType = "application/json",
211+
schema = @Schema(implementation = ApiResponse.class),
212+
examples = @ExampleObject(value = "{\"isSuccess\":true,\"code\":\"COMMON2000\",\"message\":\"재도전 모의 서류 지원이 생성되었습니다.\",\"result\":{\"sourceMockApplyId\":10,\"jobPostingId\":2,\"mockApplyId\":11,\"applyType\":\"MOCK\",\"status\":\"ANSWER_WRITE\",\"sequence\":2},\"error\":null}")
213+
)
214+
)
215+
})
216+
@PostMapping("/{mockApplyId}/retry")
217+
public ApiResponse<MockApplyRetryResponse> retryMockApply(
218+
@AuthenticationPrincipal UserDetailsImpl userDetails,
219+
@PathVariable Long mockApplyId
220+
) {
221+
return ApiResponse.onSuccess(
222+
"재도전 모의 서류 지원이 생성되었습니다.",
223+
mockApplyService.retryMockApply(userDetails.getUser(), mockApplyId)
224+
);
225+
}
226+
200227
@Operation(
201228
summary = "모의 서류 지원의 생성 공고 조회",
202229
description = "mockApplyId에 연결된 생성 공고를 조회합니다."

src/main/java/com/jobdri/jobdri_api/domain/mockapply/dto/response/MockApplyHomeItemResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public record MockApplyHomeItemResponse(
1212
Long mockApplyId,
1313
String resumePath,
1414
Long jobPostingId,
15+
int sequence,
1516
MockApplyStatus status,
1617
String companyName,
1718
String detailClassificationName,
@@ -29,6 +30,7 @@ public static MockApplyHomeItemResponse from(MockApply mockApply) {
2930
mockApply.getId(),
3031
resumePath(mockApply),
3132
jobPosting.getId(),
33+
mockApply.getSequence() == null ? 1 : mockApply.getSequence(),
3234
mockApply.getStatus(),
3335
jobPosting.getCompany().getName(),
3436
detailClassificationName,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.jobdri.jobdri_api.domain.mockapply.dto.response;
2+
3+
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
4+
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
5+
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus;
6+
7+
public record MockApplyRetryResponse(
8+
Long sourceMockApplyId,
9+
Long jobPostingId,
10+
Long mockApplyId,
11+
ApplyType applyType,
12+
MockApplyStatus status,
13+
int sequence
14+
) {
15+
public static MockApplyRetryResponse of(Long sourceMockApplyId, MockApply mockApply) {
16+
return new MockApplyRetryResponse(
17+
sourceMockApplyId,
18+
mockApply.getJobPosting().getId(),
19+
mockApply.getId(),
20+
mockApply.getApplyType(),
21+
mockApply.getStatus(),
22+
mockApply.getSequence()
23+
);
24+
}
25+
}

src/main/java/com/jobdri/jobdri_api/domain/mockapply/repository/MockApplyRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@
66
import org.springframework.data.repository.query.Param;
77

88
import java.util.List;
9+
import java.util.Optional;
910

1011
public interface MockApplyRepository extends JpaRepository<MockApply, Long> {
1112
List<MockApply> findAllByUserId(Long userId);
1213
List<MockApply> findAllByJobPostingId(Long jobPostingId);
1314
long countByUserIdAndJobPostingId(Long userId, Long jobPostingId);
1415

16+
@Query("""
17+
select ma
18+
from MockApply ma
19+
join fetch ma.user
20+
join fetch ma.jobPosting jp
21+
join fetch jp.company
22+
join fetch jp.detailClassification
23+
where ma.id = :mockApplyId
24+
""")
25+
Optional<MockApply> findByIdWithJobPosting(@Param("mockApplyId") Long mockApplyId);
26+
1527
@Query("""
1628
select coalesce(max(ma.sequence), 0)
1729
from MockApply ma
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package com.jobdri.jobdri_api.domain.mockapply.service;
22

3+
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
4+
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
35
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
46
import com.jobdri.jobdri_api.domain.mockapply.repository.MockApplyRepository;
57
import lombok.RequiredArgsConstructor;
68
import org.springframework.stereotype.Service;
79
import org.springframework.transaction.annotation.Propagation;
810
import org.springframework.transaction.annotation.Transactional;
911

12+
import java.util.List;
13+
1014
@Service
1115
@RequiredArgsConstructor
1216
public class MockApplyPersistenceService {
1317

1418
private final MockApplyRepository mockApplyRepository;
19+
private final QuestionRepository questionRepository;
1520

1621
@Transactional(propagation = Propagation.REQUIRES_NEW)
1722
public MockApply saveAndFlush(MockApply mockApply) {
1823
return mockApplyRepository.saveAndFlush(mockApply);
1924
}
25+
26+
@Transactional(propagation = Propagation.REQUIRES_NEW)
27+
public List<Question> saveQuestions(List<Question> questions) {
28+
return questionRepository.saveAll(questions);
29+
}
2030
}

src/main/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyService.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.jobdri.jobdri_api.domain.mockapply.service;
22

3+
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
4+
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
35
import com.jobdri.jobdri_api.domain.company.entity.Company;
46
import com.jobdri.jobdri_api.domain.company.repository.CompanyRepository;
57
import com.jobdri.jobdri_api.domain.audit.annotation.AuditLogEvent;
@@ -15,6 +17,7 @@
1517
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse;
1618
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeItemResponse;
1719
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse;
20+
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyRetryResponse;
1821
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse;
1922
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
2023
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
@@ -47,6 +50,7 @@ public class MockApplyService {
4750
private final MockApplyRepository mockApplyRepository;
4851
private final JobPostingRepository jobPostingRepository;
4952
private final CompanyRepository companyRepository;
53+
private final QuestionRepository questionRepository;
5054
private final MockJobPostingGenerationService mockJobPostingGenerationService;
5155
private final JobPostingService jobPostingService;
5256
private final UserService userService;
@@ -74,6 +78,48 @@ public MockApplyCreateResponse createActualApply(User user, Long jobPostingId, I
7478
return MockApplyCreateResponse.from(mockApply);
7579
}
7680

81+
@Transactional(propagation = Propagation.NOT_SUPPORTED)
82+
@AuditLogEvent(action = "MOCK_APPLY_RETRY", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
83+
public MockApplyRetryResponse retryMockApply(User user, Long mockApplyId) {
84+
User validatedUser = userService.validateUser(user);
85+
MockApply sourceMockApply = getOwnedMockApplyWithJobPosting(validatedUser, mockApplyId);
86+
JobPosting sourceJobPosting = sourceMockApply.getJobPosting();
87+
88+
JobPosting clonedJobPosting = jobPostingRepository.save(JobPosting.create(
89+
validatedUser,
90+
sourceJobPosting.getCompany(),
91+
sourceJobPosting.getDetailClassification(),
92+
sourceJobPosting.getTask(),
93+
sourceJobPosting.getRequirement(),
94+
sourceJobPosting.getPreferred()
95+
));
96+
97+
MockApply retryMockApply = saveMockApplyWithSequence(
98+
validatedUser,
99+
clonedJobPosting,
100+
sourceMockApply.getApplyType(),
101+
null
102+
);
103+
104+
List<Question> sourceQuestions = questionRepository.findAllByMockApplyIdOrderByIdAsc(sourceMockApply.getId());
105+
if (!sourceQuestions.isEmpty()) {
106+
MockApply targetMockApply = retryMockApply;
107+
List<Question> retryQuestions = sourceQuestions.stream()
108+
.map(question -> Question.create(
109+
targetMockApply,
110+
question.getContent(),
111+
question.getLimit(),
112+
""
113+
))
114+
.toList();
115+
mockApplyPersistenceService.saveQuestions(retryQuestions);
116+
retryMockApply.updateStatus(MockApplyStatus.ANSWER_WRITE);
117+
retryMockApply = mockApplyPersistenceService.saveAndFlush(retryMockApply);
118+
}
119+
120+
return MockApplyRetryResponse.of(sourceMockApply.getId(), retryMockApply);
121+
}
122+
77123
@Transactional(propagation = Propagation.NOT_SUPPORTED)
78124
@AuditLogEvent(action = "MOCK_APPLY_CREATE", targetType = "MOCK_APPLY", targetId = "#result.mockApplyId()")
79125
public MockApplyCreateResponse createMockApplyFromJobPosting(User user, Long jobPostingId) {
@@ -199,6 +245,20 @@ private MockApply getOwnedMockApply(User user, Long mockApplyId) {
199245
return mockApply;
200246
}
201247

248+
private MockApply getOwnedMockApplyWithJobPosting(User user, Long mockApplyId) {
249+
MockApply mockApply = mockApplyRepository.findByIdWithJobPosting(mockApplyId)
250+
.orElseThrow(() -> new GeneralException(
251+
GeneralErrorCode.MOCK_APPLY_NOT_FOUND,
252+
"해당 모의 서류 지원을 찾을 수 없습니다. mockApplyId=" + mockApplyId
253+
));
254+
255+
if (!mockApply.getUser().getId().equals(user.getId())) {
256+
throw new GeneralException(GeneralErrorCode.FORBIDDEN, "해당 모의 서류 지원에 접근할 수 없습니다.");
257+
}
258+
259+
return mockApply;
260+
}
261+
202262
private int resolveSequence(User user, JobPosting jobPosting, Integer requestedSequence) {
203263
if (isPositiveSequence(requestedSequence)) {
204264
return requestedSequence;

src/test/java/com/jobdri/jobdri_api/domain/mockapply/service/MockApplyServiceTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.jobdri.jobdri_api.domain.mockapply.service;
22

33
import com.jobdri.jobdri_api.domain.analysis.entity.Analysis;
4+
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
45
import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository;
6+
import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository;
57
import com.jobdri.jobdri_api.domain.classification.entity.Classification;
68
import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification;
79
import com.jobdri.jobdri_api.domain.classification.entity.MiddleClassification;
@@ -17,6 +19,7 @@
1719
import com.jobdri.jobdri_api.domain.mockapply.dto.request.MockApplyCreateMockRequest;
1820
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse;
1921
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyHomeResponse;
22+
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyRetryResponse;
2023
import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplySequenceResponse;
2124
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
2225
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
@@ -62,6 +65,9 @@ class MockApplyServiceTest {
6265
@Autowired
6366
private AnalysisRepository analysisRepository;
6467

68+
@Autowired
69+
private QuestionRepository questionRepository;
70+
6571
@Autowired
6672
private JobPostingRepository jobPostingRepository;
6773

@@ -229,6 +235,42 @@ void createMockApplyFromJobPosting() {
229235
assertThat(mockApply.getSequence()).isEqualTo(1);
230236
}
231237

238+
@Test
239+
@DisplayName("기존 지원의 공고와 문항을 복사해 재도전 지원을 생성한다")
240+
void retryMockApply() {
241+
User user = saveUser("retry-mock-apply@example.com");
242+
JobPosting jobPosting = saveJobPosting(user, "백엔드 개발");
243+
MockApply sourceMockApply = saveMockApply(user, jobPosting, ApplyType.MOCK, 1);
244+
saveQuestion(sourceMockApply, "지원 동기와 입사 후 목표를 작성해주세요.", 700, "기존 답변");
245+
saveQuestion(sourceMockApply, "직접 추가한 문항입니다.", 1000, "기존 직접 추가 답변");
246+
247+
MockApplyRetryResponse response = mockApplyService.retryMockApply(user, sourceMockApply.getId());
248+
249+
MockApply retryMockApply = mockApplyRepository.findById(response.mockApplyId()).orElseThrow();
250+
JobPosting retryJobPosting = jobPostingRepository.findById(response.jobPostingId()).orElseThrow();
251+
List<Question> retryQuestions = questionRepository.findAllByMockApplyIdOrderByIdAsc(response.mockApplyId());
252+
assertThat(response.sourceMockApplyId()).isEqualTo(sourceMockApply.getId());
253+
assertThat(response.sequence()).isEqualTo(2);
254+
assertThat(response.status()).isEqualTo(MockApplyStatus.ANSWER_WRITE);
255+
assertThat(retryMockApply.getApplyType()).isEqualTo(ApplyType.MOCK);
256+
assertThat(retryMockApply.getSequence()).isEqualTo(2);
257+
assertThat(retryMockApply.getStatus()).isEqualTo(MockApplyStatus.ANSWER_WRITE);
258+
assertThat(retryJobPosting.getId()).isNotEqualTo(jobPosting.getId());
259+
assertThat(retryJobPosting.getCompany().getId()).isEqualTo(jobPosting.getCompany().getId());
260+
assertThat(retryJobPosting.getDetailClassification().getId()).isEqualTo(jobPosting.getDetailClassification().getId());
261+
assertThat(retryJobPosting.getTask()).isEqualTo(jobPosting.getTask());
262+
assertThat(retryQuestions).hasSize(2);
263+
assertThat(retryQuestions)
264+
.extracting(Question::getContent)
265+
.containsExactly(
266+
"지원 동기와 입사 후 목표를 작성해주세요.",
267+
"직접 추가한 문항입니다."
268+
);
269+
assertThat(retryQuestions)
270+
.extracting(Question::getAnswer)
271+
.containsExactly("", "");
272+
}
273+
232274
@Test
233275
@DisplayName("mockApplyId로 생성된 모의 공고를 조회한다")
234276
void getMockApplyJobPosting() {
@@ -290,6 +332,7 @@ void getMyMockApplies() {
290332
assertThat(response.completed()).hasSize(1);
291333
assertThat(response.inProgress().get(0).mockApplyId()).isEqualTo(inProgress.getId());
292334
assertThat(response.inProgress().get(0).jobPostingId()).isEqualTo(backendPosting.getId());
335+
assertThat(response.inProgress().get(0).sequence()).isEqualTo(1);
293336
assertThat(response.inProgress().get(0).status()).isEqualTo(MockApplyStatus.ANSWER_WRITE);
294337
assertThat(response.inProgress().get(0).companyName()).isEqualTo("테스트 기업");
295338
assertThat(response.inProgress().get(0).detailClassificationName()).isEqualTo("백엔드 개발");
@@ -298,6 +341,7 @@ void getMyMockApplies() {
298341
assertThat(response.inProgress().get(0).score()).isNull();
299342
assertThat(response.inProgress().get(0).resumePath()).isEqualTo("/mock-applies/" + inProgress.getId() + "/answers");
300343
assertThat(response.completed().get(0).mockApplyId()).isEqualTo(completed.getId());
344+
assertThat(response.completed().get(0).sequence()).isEqualTo(1);
301345
assertThat(response.completed().get(0).score()).isEqualTo(71);
302346
assertThat(response.completed().get(0).applyType()).isEqualTo(ApplyType.MOCK);
303347
assertThat(response.completed().get(0).resumePath()).isEqualTo("/mock-applies/" + completed.getId() + "/analysis");
@@ -447,6 +491,15 @@ private MockApply saveMockApply(User user, JobPosting jobPosting, ApplyType appl
447491
));
448492
}
449493

494+
private Question saveQuestion(MockApply mockApply, String content, int limit, String answer) {
495+
return inNewTransaction(() -> questionRepository.save(Question.create(
496+
mockApplyRepository.findById(mockApply.getId()).orElseThrow(),
497+
content,
498+
limit,
499+
answer
500+
)));
501+
}
502+
450503
private <T> T inNewTransaction(Supplier<T> action) {
451504
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
452505
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

0 commit comments

Comments
 (0)