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 @@ -10,7 +10,9 @@
import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingAiService;
import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingAsyncFacadeService;
import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingIngestService;
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.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
Expand All @@ -20,6 +22,7 @@
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.ModelAttribute;
Expand All @@ -37,15 +40,18 @@ public class JobPostingAiController {
private final JobPostingAiService jobPostingAiService;
private final JobPostingIngestService jobPostingIngestService;
private final JobPostingAsyncFacadeService jobPostingAsyncFacadeService;
private final UserService userService;

@Operation(
summary = "채용 공고 정보 추출",
description = "채용 공고 원문 텍스트를 기반으로 회사명, 직무명, 주요 업무, 자격 요건, 우대 사항을 AI로 추출합니다."
)
@PostMapping(value = "/extract", consumes = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<JobPostingExtractResponse> extractJobPostingFromText(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody JobPostingExtractRequest request
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 추출에 성공했습니다.",
jobPostingAiService.extractJobPosting(request.rawText())
Expand All @@ -58,8 +64,10 @@ public ApiResponse<JobPostingExtractResponse> extractJobPostingFromText(
)
@PostMapping(value = "/extract", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<JobPostingExtractResponse> extractJobPostingFromMultipart(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@ModelAttribute JobPostingExtractMultipartRequest request
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 추출에 성공했습니다.",
jobPostingAiService.extractJobPosting(request)
Expand Down Expand Up @@ -194,11 +202,13 @@ public ApiResponse<JobPostingExtractResponse> extractJobPostingFromMultipart(
})
@PostMapping(value = "/ingest", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<JobPostingIngestResponse> ingestJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@ModelAttribute JobPostingIngestMultipartRequest request
) {
var user = validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 추출 및 저장에 성공했습니다.",
jobPostingIngestService.ingestAndCreate(request)
jobPostingIngestService.ingestAndCreate(user, request)
);
}

Expand All @@ -208,11 +218,13 @@ public ApiResponse<JobPostingIngestResponse> ingestJobPosting(
)
@PostMapping(value = "/ingest/async", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<JobPostingAsyncSubmitResponse> submitIngestJobPostingAsync(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@ModelAttribute JobPostingIngestMultipartRequest request
) {
var user = validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 비동기 작업 접수에 성공했습니다.",
jobPostingAsyncFacadeService.submit(request)
jobPostingAsyncFacadeService.submit(user, request)
);
}

Expand All @@ -222,11 +234,17 @@ public ApiResponse<JobPostingAsyncSubmitResponse> submitIngestJobPostingAsync(
)
@GetMapping("/ingest/async/{taskId}")
public ApiResponse<JobPostingAsyncStatusResponse> getIngestJobPostingAsyncStatus(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable String taskId
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 비동기 작업 상태 조회에 성공했습니다.",
jobPostingAsyncFacadeService.getTask(taskId)
);
}

private com.jobdri.jobdri_api.domain.user.entity.User validateAuthenticatedUser(UserDetailsImpl userDetails) {
return userService.validateUser(userDetails == null ? null : userDetails.getUser());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
import com.jobdri.jobdri_api.domain.jobposting.service.MockQuestionCacheService;
import com.jobdri.jobdri_api.domain.jobposting.service.MockJobPostingGenerationService;
import com.jobdri.jobdri_api.domain.jobposting.service.JobPostingService;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -38,12 +41,15 @@ public class JobPostingController {
private final MockJobPostingGenerationService mockJobPostingGenerationService;
private final MockQuestionCacheService mockQuestionCacheService;
private final JobPostingService jobPostingService;
private final UserService userService;

@Operation(summary = "채용 공고 초안 생성", description = "회사 정보와 직무 정보를 바탕으로 AI가 공고 본문 초안을 생성합니다.")
@PostMapping("/generate")
public ApiResponse<JobPostingGenerateResponse> generateJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody JobPostingGenerateRequest request
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 초안 생성에 성공했습니다.",
jobPostingAiService.generateJobPosting(request)
Expand All @@ -56,8 +62,10 @@ public ApiResponse<JobPostingGenerateResponse> generateJobPosting(
)
@PostMapping("/mock/generate")
public ApiResponse<JobPostingMockGenerateResponse> generateMockJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody JobPostingMockGenerateRequest request
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"모의 공고 생성에 성공했습니다.",
mockJobPostingGenerationService.generate(request)
Expand All @@ -70,8 +78,10 @@ public ApiResponse<JobPostingMockGenerateResponse> generateMockJobPosting(
)
@PostMapping("/mock/questions")
public ApiResponse<JobPostingMockQuestionResponse> getMockRecommendedQuestions(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody JobPostingMockGenerateRequest request
) {
validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"모의 공고 추천 질문 조회에 성공했습니다.",
new JobPostingMockQuestionResponse(mockQuestionCacheService.getRecommendedQuestions(request))
Expand All @@ -81,44 +91,58 @@ public ApiResponse<JobPostingMockQuestionResponse> getMockRecommendedQuestions(
@Operation(summary = "채용 공고 저장", description = "생성되었거나 직접 작성한 채용 공고를 DB에 저장합니다.")
@PostMapping
public ApiResponse<JobPostingResponse> createJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody JobPostingCreateRequest request
) {
var user = validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 저장에 성공했습니다.",
jobPostingService.createJobPosting(request)
jobPostingService.createJobPosting(user, request)
);
}

@Operation(summary = "채용 공고 수정", description = "기존 채용 공고를 수정합니다. 회사명이 없으면 회사를 새로 생성합니다.")
@PutMapping("/{jobPostingId}")
public ApiResponse<JobPostingResponse> updateJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long jobPostingId,
@Valid @RequestBody JobPostingUpdateRequest request
) {
var user = validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 수정에 성공했습니다.",
jobPostingService.updateJobPosting(jobPostingId, request)
jobPostingService.updateJobPosting(user, jobPostingId, request)
);
}

@Operation(summary = "채용 공고 단건 조회", description = "채용 공고 ID로 단건 조회합니다.")
@GetMapping("/{jobPostingId}")
public ApiResponse<JobPostingResponse> getJobPosting(@PathVariable Long jobPostingId) {
public ApiResponse<JobPostingResponse> getJobPosting(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long jobPostingId
) {
var user = validateAuthenticatedUser(userDetails);
return ApiResponse.onSuccess(
"채용 공고 조회에 성공했습니다.",
jobPostingService.getJobPosting(jobPostingId)
jobPostingService.getJobPosting(user, jobPostingId)
);
}

@Operation(summary = "채용 공고 목록 조회", description = "전체 공고 또는 회사별 공고 목록을 조회합니다.")
@GetMapping
public ApiResponse<List<JobPostingResponse>> getJobPostings(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestParam(required = false) Long companyId
) {
var user = validateAuthenticatedUser(userDetails);
List<JobPostingResponse> result = companyId == null
? jobPostingService.getAllJobPostings()
: jobPostingService.getJobPostingsByCompany(companyId);
? jobPostingService.getAllJobPostings(user)
: jobPostingService.getJobPostingsByCompany(user, companyId);

return ApiResponse.onSuccess("채용 공고 목록 조회에 성공했습니다.", result);
}

private com.jobdri.jobdri_api.domain.user.entity.User validateAuthenticatedUser(UserDetailsImpl userDetails) {
return userService.validateUser(userDetails == null ? null : userDetails.getUser());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@Builder
public class JobPostingIngestCommand {

private Long userId;
private String rawText;
private String sourceUrl;
private byte[] imageBytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public class JobPostingResponse {

private Long jobPostingId;
private Long userId;
private Long companyId;
private String companyName;
private String companySize;
Expand All @@ -21,6 +22,7 @@ public class JobPostingResponse {
public static JobPostingResponse from(JobPosting jobPosting) {
return JobPostingResponse.builder()
.jobPostingId(jobPosting.getId())
.userId(jobPosting.getUser().getId())
.companyId(jobPosting.getCompany().getId())
.companyName(jobPosting.getCompany().getName())
.companySize(jobPosting.getCompany().getSize().name())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification;
import com.jobdri.jobdri_api.domain.company.entity.Company;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApply;
import com.jobdri.jobdri_api.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;

Expand All @@ -25,6 +26,10 @@ public class JobPosting {
@JoinColumn(name = "company_id", nullable = false)
private Company company;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "detail_classification_id", nullable = false)
private DetailClassification detailClassification;
Expand All @@ -43,13 +48,15 @@ public class JobPosting {
private List<MockApply> mockApplies = new ArrayList<>();

public static JobPosting create(
User user,
Company company,
DetailClassification detailClassification,
String task,
String requirement,
String preferred
) {
return JobPosting.builder()
.user(user)
.company(company)
.detailClassification(detailClassification)
.task(task)
Expand All @@ -59,12 +66,14 @@ public static JobPosting create(
}

public void update(
User user,
Company company,
DetailClassification detailClassification,
String task,
String requirement,
String preferred
) {
this.user = user;
this.company = company;
this.detailClassification = detailClassification;
this.task = task;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

public interface JobPostingRepository extends JpaRepository<JobPosting, Long> {
List<JobPosting> findAllByCompanyId(Long companyId);
List<JobPosting> findAllByUserId(Long userId);
List<JobPosting> findAllByUserIdAndCompanyId(Long userId, Long companyId);
List<JobPosting> findTop5ByDetailClassificationIdOrderByIdDesc(Long detailClassificationId);
List<JobPosting> findTop5ByCompanyIdOrderByIdDesc(Long companyId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.jobdri.jobdri_api.domain.jobposting.dto.request.JobPostingIngestMultipartRequest;
import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingAsyncStatusResponse;
import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingAsyncSubmitResponse;
import com.jobdri.jobdri_api.domain.user.entity.User;
import com.jobdri.jobdri_api.domain.user.service.UserService;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
Expand All @@ -19,10 +21,12 @@ public class JobPostingAsyncFacadeService {

private final JobPostingAsyncTaskService jobPostingAsyncTaskService;
private final JobPostingAsyncProcessor jobPostingAsyncProcessor;
private final UserService userService;

public JobPostingAsyncSubmitResponse submit(JobPostingIngestMultipartRequest request) {
public JobPostingAsyncSubmitResponse submit(User user, JobPostingIngestMultipartRequest request) {
User validatedUser = userService.validateUser(user);
String taskId = jobPostingAsyncTaskService.createPendingTask();
JobPostingIngestCommand command = snapshot(request);
JobPostingIngestCommand command = snapshot(validatedUser, request);

try {
jobPostingAsyncProcessor.process(taskId, command);
Expand All @@ -43,8 +47,9 @@ public JobPostingAsyncStatusResponse getTask(String taskId) {
return jobPostingAsyncTaskService.getTask(taskId);
}

private JobPostingIngestCommand snapshot(JobPostingIngestMultipartRequest request) {
private JobPostingIngestCommand snapshot(User user, JobPostingIngestMultipartRequest request) {
return JobPostingIngestCommand.builder()
.userId(user.getId())
.rawText(request.rawText())
.sourceUrl(request.sourceUrl())
.imageBytes(readBytes(request.image()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import com.jobdri.jobdri_api.domain.jobposting.dto.response.JobPostingGenerateResponse;
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.user.entity.User;
import com.jobdri.jobdri_api.domain.user.service.UserService;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,15 +26,17 @@ public class JobPostingIngestService {

private static final int DEFAULT_CANDIDATE_LIMIT = 10;

@Value("${job-posting.ingest.classification-confidence-threshold}")
@Value("${job-posting.ingest.classification-confidence-threshold:0.65}")
private double classificationConfidenceThreshold;

private final JobPostingAiService jobPostingAiService;
private final JobPostingClassificationService jobPostingClassificationService;
private final JobPostingService jobPostingService;
private final UserService userService;

public JobPostingIngestResponse ingestAndCreate(JobPostingIngestMultipartRequest request) {
public JobPostingIngestResponse ingestAndCreate(User user, JobPostingIngestMultipartRequest request) {
JobPostingIngestCommand command = JobPostingIngestCommand.builder()
.userId(user.getId())
.rawText(request.rawText())
.sourceUrl(request.sourceUrl())
.companySize(request.companySize())
Expand Down Expand Up @@ -92,6 +96,7 @@ public JobPostingIngestResponse ingestAndCreate(JobPostingIngestCommand command)
);

JobPostingResponse saved = jobPostingService.createJobPosting(
resolveUser(command),
new JobPostingCreateRequest(
fallbackCompanyName(extracted.companyName()),
command.getCompanySize(),
Expand Down Expand Up @@ -119,4 +124,11 @@ private String fallbackCompanyName(String companyName) {
}
return companyName;
}

private User resolveUser(JobPostingIngestCommand command) {
if (command.getUserId() == null) {
throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO, "인증 정보가 누락되었습니다.");
}
return userService.getUser(command.getUserId());
}
}
Loading
Loading