diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingExtensionController.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingExtensionController.java new file mode 100644 index 0000000..a3f61dc --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/controller/JobPostingExtensionController.java @@ -0,0 +1,44 @@ +package com.jobdri.jobdri_api.domain.jobposting.controller; + +import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingExtensionIngestRequest; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingExtensionIngestResponse; +import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingExtensionIngestService; +import com.jobdri.jobdri_api.domain.user.service.UserService; +import com.jobdri.jobdri_api.global.apiPayload.ApiResponse; +import com.jobdri.jobdri_api.global.security.UserDetailsImpl; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/job-postings/extension") +@Tag(name = "JobPosting Extension", description = "크롬 익스텐션 채용 공고 연계 API") +public class JobPostingExtensionController { + + private final JobPostingExtensionIngestService jobPostingExtensionIngestService; + private final UserService userService; + + @Operation( + summary = "크롬 익스텐션 공고 수집 및 모의 서류 생성", + description = "크롬 익스텐션이 크롤링한 채용 공고 원문을 기반으로 공고를 저장하고 모의 서류 지원을 생성합니다." + ) + @PostMapping(value = "/ingest", consumes = MediaType.APPLICATION_JSON_VALUE) + public ApiResponse ingestFromExtension( + @AuthenticationPrincipal UserDetailsImpl userDetails, + @Valid @RequestBody JobPostingExtensionIngestRequest request + ) { + var user = userService.validateUser(userDetails == null ? null : userDetails.getUser()); + return ApiResponse.onSuccess( + "크롬 익스텐션 공고 수집에 성공했습니다.", + jobPostingExtensionIngestService.ingest(user, request) + ); + } +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtensionIngestRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtensionIngestRequest.java new file mode 100644 index 0000000..e4e5bfe --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/request/JobPostingExtensionIngestRequest.java @@ -0,0 +1,12 @@ +package com.jobdri.jobdri_api.domain.jobposting.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record JobPostingExtensionIngestRequest( + String sourceUrl, + String sourceSite, + + @NotBlank(message = "크롤링한 공고 내용은 필수입니다.") + String rawText +) { +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtensionIngestResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtensionIngestResponse.java new file mode 100644 index 0000000..987d671 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/dto/response/JobPostingExtensionIngestResponse.java @@ -0,0 +1,28 @@ +package com.jobdri.jobdri_api.domain.jobposting.dto.response; + +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; + +public record JobPostingExtensionIngestResponse( + String sourceUrl, + String sourceSite, + boolean savedToDatabase, + String message, + JobPostingIngestResponse ingest, + MockApplyCreateResponse mockApply +) { + public static JobPostingExtensionIngestResponse of( + String sourceUrl, + String sourceSite, + JobPostingIngestResponse ingest, + MockApplyCreateResponse mockApply + ) { + return new JobPostingExtensionIngestResponse( + sourceUrl, + sourceSite, + ingest.isSavedToDatabase(), + ingest.getMessage(), + ingest, + mockApply + ); + } +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java new file mode 100644 index 0000000..0b695c3 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java @@ -0,0 +1,41 @@ +package com.jobdri.jobdri_api.domain.jobposting.service; + +import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingExtensionIngestRequest; +import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingIngestRequest; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingExtensionIngestResponse; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingIngestResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; +import com.jobdri.jobdri_api.domain.mockapply.service.MockApplyService; +import com.jobdri.jobdri_api.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JobPostingExtensionIngestService { + + private final JobPostingIngestService jobPostingIngestService; + private final MockApplyService mockApplyService; + + public JobPostingExtensionIngestResponse ingest(User user, JobPostingExtensionIngestRequest request) { + JobPostingIngestResponse ingest = jobPostingIngestService.ingestAndCreate( + user, + new JobPostingIngestRequest(request.rawText(), null) + ); + + MockApplyCreateResponse mockApply = null; + if (ingest.isSavedToDatabase() && ingest.getSaved() != null) { + mockApply = mockApplyService.createMockApplyFromJobPosting( + user, + ingest.getSaved().getJobPostingId() + ); + } + + return JobPostingExtensionIngestResponse.of( + request.sourceUrl(), + request.sourceSite(), + ingest, + mockApply + ); + } +} diff --git a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java new file mode 100644 index 0000000..ffe3e8d --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java @@ -0,0 +1,121 @@ +package com.jobdri.jobdri_api.domain.jobposting.service; + +import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingExtensionIngestRequest; +import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingIngestRequest; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingExtensionIngestResponse; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingIngestResponse; +import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingResponse; +import com.jobdri.jobdri_api.domain.mockapply.dto.response.MockApplyCreateResponse; +import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType; +import com.jobdri.jobdri_api.domain.mockapply.service.MockApplyService; +import com.jobdri.jobdri_api.domain.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JobPostingExtensionIngestServiceTest { + + @Mock + private JobPostingIngestService jobPostingIngestService; + + @Mock + private MockApplyService mockApplyService; + + @InjectMocks + private JobPostingExtensionIngestService jobPostingExtensionIngestService; + + private User user; + + @BeforeEach + void setUp() { + user = User.signup("테스트 사용자", "extension@example.com", "encoded-password"); + ReflectionTestUtils.setField(user, "id", 1L); + } + + @Test + @DisplayName("익스텐션 공고 수집 성공 시 공고 저장 후 모의 서류 지원을 생성한다") + void ingestCreatesMockApplyWhenJobPostingSaved() { + JobPostingExtensionIngestRequest request = new JobPostingExtensionIngestRequest( + "https://www.wanted.co.kr/wd/123", + "WANTED", + "채용 공고 원문" + ); + JobPostingResponse saved = JobPostingResponse.builder() + .jobPostingId(10L) + .userId(1L) + .companyId(2L) + .companyName("테스트 회사") + .detailClassificationId(3L) + .detailClassificationName("백엔드 개발") + .task("주요 업무") + .requirement("자격 요건") + .preferred("우대 사항") + .build(); + JobPostingIngestResponse ingest = new JobPostingIngestResponse( + true, + "저장 성공", + null, + null, + null, + null, + saved + ); + MockApplyCreateResponse mockApply = new MockApplyCreateResponse(10L, 20L, ApplyType.MOCK, 1); + + when(jobPostingIngestService.ingestAndCreate(eq(user), org.mockito.ArgumentMatchers.any(JobPostingIngestRequest.class))) + .thenReturn(ingest); + when(mockApplyService.createMockApplyFromJobPosting(user, 10L)) + .thenReturn(mockApply); + + JobPostingExtensionIngestResponse response = jobPostingExtensionIngestService.ingest(user, request); + + ArgumentCaptor ingestRequestCaptor = ArgumentCaptor.forClass(JobPostingIngestRequest.class); + verify(jobPostingIngestService).ingestAndCreate(eq(user), ingestRequestCaptor.capture()); + assertThat(ingestRequestCaptor.getValue().rawText()).isEqualTo("채용 공고 원문"); + assertThat(ingestRequestCaptor.getValue().imageObjectKey()).isNull(); + assertThat(response.sourceUrl()).isEqualTo("https://www.wanted.co.kr/wd/123"); + assertThat(response.sourceSite()).isEqualTo("WANTED"); + assertThat(response.mockApply()).isEqualTo(mockApply); + } + + @Test + @DisplayName("공고 저장이 보류되면 모의 서류 지원을 생성하지 않는다") + void ingestSkipsMockApplyWhenJobPostingNotSaved() { + JobPostingExtensionIngestRequest request = new JobPostingExtensionIngestRequest( + "https://www.wanted.co.kr/wd/123", + "WANTED", + "채용 공고 원문" + ); + JobPostingIngestResponse ingest = new JobPostingIngestResponse( + false, + "저장 보류", + null, + null, + null, + null, + null + ); + + when(jobPostingIngestService.ingestAndCreate(eq(user), org.mockito.ArgumentMatchers.any(JobPostingIngestRequest.class))) + .thenReturn(ingest); + + JobPostingExtensionIngestResponse response = jobPostingExtensionIngestService.ingest(user, request); + + verify(mockApplyService, never()).createMockApplyFromJobPosting(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any()); + assertThat(response.savedToDatabase()).isFalse(); + assertThat(response.mockApply()).isNull(); + } +}