-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#26 mock jobposting entity #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.controller; | ||
|
|
||
| import com.jobdri.jobdri_api.domain.corpus.dto.request.CorpusEmbeddingSyncRequest; | ||
| import com.jobdri.jobdri_api.domain.corpus.dto.request.CorpusImportRequest; | ||
| import com.jobdri.jobdri_api.domain.corpus.dto.response.CorpusEmbeddingSyncResponse; | ||
| import com.jobdri.jobdri_api.domain.corpus.service.CorpusEmbeddingSyncService; | ||
| import com.jobdri.jobdri_api.domain.corpus.service.CorpusImportResult; | ||
| import com.jobdri.jobdri_api.domain.corpus.service.CorpusImportService; | ||
| import com.jobdri.jobdri_api.global.apiPayload.ApiResponse; | ||
| 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.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; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/admin/corpus") | ||
| @Tag(name = "CorpusAdmin", description = "관리자용 corpus 적재/임베딩 API") | ||
| public class CorpusAdminController { | ||
|
|
||
| private final CorpusImportService corpusImportService; | ||
| private final CorpusEmbeddingSyncService corpusEmbeddingSyncService; | ||
|
|
||
| @Operation(summary = "corpus 엑셀 적재", description = "관리자가 xlsx 파일 경로를 넘겨 corpus 원본 테이블에 적재합니다.") | ||
| @PostMapping("/import") | ||
| public ApiResponse<CorpusImportResult> importCorpus( | ||
| @Valid @RequestBody CorpusImportRequest request | ||
| ) throws IOException { | ||
| return ApiResponse.onSuccess( | ||
| "corpus 엑셀 적재에 성공했습니다.", | ||
| corpusImportService.importFromXlsx(Path.of(request.filePath())) | ||
| ); | ||
|
Comment on lines
+33
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constrain import file paths to an approved base directory. Line 38 accepts a raw server filesystem path from the request; this creates an arbitrary local file access surface for admin tokens. Enforce normalization and allowlist a configured import root. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Operation(summary = "corpus 임베딩 동기화", description = "유효한 corpus 데이터를 읽어 pgvector 테이블에 임베딩을 저장합니다.") | ||
| @PostMapping("/embeddings/sync") | ||
| public ApiResponse<CorpusEmbeddingSyncResponse> syncEmbeddings( | ||
| @Valid @RequestBody CorpusEmbeddingSyncRequest request | ||
| ) { | ||
| return ApiResponse.onSuccess( | ||
| "corpus 임베딩 동기화에 성공했습니다.", | ||
| corpusEmbeddingSyncService.syncAll(request.limit()) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.Positive; | ||
|
|
||
| public record CorpusEmbeddingSyncRequest( | ||
| @Positive(message = "limit는 1 이상이어야 합니다.") | ||
| Integer limit | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record CorpusImportRequest( | ||
| @NotBlank(message = "엑셀 파일 경로는 필수입니다.") | ||
| String filePath | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.dto.response; | ||
|
|
||
| public record CorpusEmbeddingSyncResponse( | ||
| int jobPostingEmbeddingsUpserted, | ||
| int questionEmbeddingsUpserted, | ||
| String embeddingModel | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.entity; | ||
|
|
||
| import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; | ||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder(access = AccessLevel.PRIVATE) | ||
| @Table( | ||
| name = "corpus_classification_mappings", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint( | ||
| name = "uk_corpus_classification_mapping_source_triplet", | ||
| columnNames = {"source_job_group_l1", "source_job_family_l2", "source_role_l3"} | ||
| ) | ||
| } | ||
| ) | ||
| public class CorpusClassificationMapping { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "source_job_group_l1", nullable = false, columnDefinition = "TEXT") | ||
| private String sourceJobGroupL1; | ||
|
|
||
| @Column(name = "source_job_family_l2", nullable = false, columnDefinition = "TEXT") | ||
| private String sourceJobFamilyL2; | ||
|
|
||
| @Column(name = "source_role_l3", nullable = false, columnDefinition = "TEXT") | ||
| private String sourceRoleL3; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "detail_classification_id", nullable = false) | ||
| private DetailClassification detailClassification; | ||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| public static CorpusClassificationMapping create( | ||
| String sourceJobGroupL1, | ||
| String sourceJobFamilyL2, | ||
| String sourceRoleL3, | ||
| DetailClassification detailClassification | ||
| ) { | ||
| return CorpusClassificationMapping.builder() | ||
| .sourceJobGroupL1(sourceJobGroupL1) | ||
| .sourceJobFamilyL2(sourceJobFamilyL2) | ||
| .sourceRoleL3(sourceRoleL3) | ||
| .detailClassification(detailClassification) | ||
| .createdAt(LocalDateTime.now()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package com.jobdri.jobdri_api.domain.corpus.entity; | ||
|
|
||
| import com.jobdri.jobdri_api.domain.company.entity.Company; | ||
| import com.jobdri.jobdri_api.domain.classification.entity.DetailClassification; | ||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder(access = AccessLevel.PRIVATE) | ||
| @Table( | ||
| name = "mock_job_posting_corpus", | ||
| indexes = { | ||
| @Index(name = "idx_mock_job_posting_corpus_source_analysis", columnList = "source_analysis_id"), | ||
| @Index(name = "idx_mock_job_posting_corpus_company", columnList = "company_id"), | ||
| @Index(name = "idx_mock_job_posting_corpus_classification", columnList = "job_group_l1, job_family_l2, role_l3") | ||
| } | ||
| ) | ||
| public class MockJobPostingCorpus { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(name = "source_analysis_id", nullable = false, length = 100, unique = true) | ||
| private String sourceAnalysisId; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "company_id") | ||
| private Company company; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "detail_classification_id") | ||
| private DetailClassification detailClassification; | ||
|
|
||
| @Column(name = "company_name", columnDefinition = "TEXT") | ||
| private String companyName; | ||
|
|
||
| @Column(columnDefinition = "TEXT") | ||
| private String industry; | ||
|
|
||
| @Column(name = "job_group_l1", columnDefinition = "TEXT") | ||
| private String jobGroupL1; | ||
|
|
||
| @Column(name = "job_family_l2", columnDefinition = "TEXT") | ||
| private String jobFamilyL2; | ||
|
|
||
| @Column(name = "role_l3", columnDefinition = "TEXT") | ||
| private String roleL3; | ||
|
|
||
| @Column(columnDefinition = "TEXT") | ||
| private String skills; | ||
|
|
||
| @Column(name = "responsibilities", columnDefinition = "TEXT") | ||
| private String responsibilities; | ||
|
|
||
| @Column(name = "requirements", columnDefinition = "TEXT") | ||
| private String requirements; | ||
|
|
||
| @Column(name = "preferred", columnDefinition = "TEXT") | ||
| private String preferred; | ||
|
|
||
| @Column(name = "embedding_text", nullable = false, columnDefinition = "TEXT") | ||
| private String embeddingText; | ||
|
|
||
| @Column(name = "is_valid_for_embedding", nullable = false) | ||
| private boolean validForEmbedding; | ||
|
|
||
| @Column(name = "invalid_reason", columnDefinition = "TEXT") | ||
| private String invalidReason; | ||
|
|
||
| @Column(name = "created_at", nullable = false) | ||
| private LocalDateTime createdAt; | ||
|
|
||
| public static MockJobPostingCorpus create( | ||
| String sourceAnalysisId, | ||
| Company company, | ||
| DetailClassification detailClassification, | ||
| String companyName, | ||
| String industry, | ||
| String jobGroupL1, | ||
| String jobFamilyL2, | ||
| String roleL3, | ||
| String skills, | ||
| String responsibilities, | ||
| String requirements, | ||
| String preferred, | ||
| String embeddingText, | ||
| boolean validForEmbedding, | ||
| String invalidReason | ||
| ) { | ||
| return MockJobPostingCorpus.builder() | ||
| .sourceAnalysisId(sourceAnalysisId) | ||
| .company(company) | ||
| .detailClassification(detailClassification) | ||
| .companyName(companyName) | ||
| .industry(industry) | ||
| .jobGroupL1(jobGroupL1) | ||
| .jobFamilyL2(jobFamilyL2) | ||
| .roleL3(roleL3) | ||
| .skills(skills) | ||
| .responsibilities(responsibilities) | ||
| .requirements(requirements) | ||
| .preferred(preferred) | ||
| .embeddingText(embeddingText) | ||
| .validForEmbedding(validForEmbedding) | ||
| .invalidReason(invalidReason) | ||
| .createdAt(LocalDateTime.now()) | ||
| .build(); | ||
| } | ||
|
|
||
| public void assignCompany(Company company) { | ||
| this.company = company; | ||
| } | ||
|
|
||
| public void assignDetailClassification(DetailClassification detailClassification) { | ||
| this.detailClassification = detailClassification; | ||
| } | ||
|
|
||
| public void updateFromImport( | ||
| Company company, | ||
| DetailClassification detailClassification, | ||
| String companyName, | ||
| String industry, | ||
| String jobGroupL1, | ||
| String jobFamilyL2, | ||
| String roleL3, | ||
| String skills, | ||
| String responsibilities, | ||
| String requirements, | ||
| String preferred, | ||
| String embeddingText, | ||
| boolean validForEmbedding, | ||
| String invalidReason | ||
| ) { | ||
| this.company = company; | ||
| this.detailClassification = detailClassification; | ||
| this.companyName = companyName; | ||
| this.industry = industry; | ||
| this.jobGroupL1 = jobGroupL1; | ||
| this.jobFamilyL2 = jobFamilyL2; | ||
| this.roleL3 = roleL3; | ||
| this.skills = skills; | ||
| this.responsibilities = responsibilities; | ||
| this.requirements = requirements; | ||
| this.preferred = preferred; | ||
| this.embeddingText = embeddingText; | ||
| this.validForEmbedding = validForEmbedding; | ||
| this.invalidReason = invalidReason; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: JobDri-Developer/BackEnd
Length of output: 175
🏁 Script executed:
Repository: JobDri-Developer/BackEnd
Length of output: 162
🏁 Script executed:
Repository: JobDri-Developer/BackEnd
Length of output: 1908
🏁 Script executed:
Repository: JobDri-Developer/BackEnd
Length of output: 2841
🏁 Script executed:
Repository: JobDri-Developer/BackEnd
Length of output: 4267
Use case-insensitive detail-name methods for fallback resolution.
countByDetailNameat line 15 andfindByDetailNameat line 14 are case-sensitive derived queries. InCorpusClassificationResolver(lines 57-59), this causes the fallback resolution to incorrectly skip a unique match when only casing differs (e.g.,Software Engineerstored butsoftware engineersearched). Thenormalize()method only trims whitespace, not case.Replace with case-insensitive variants:
Suggested fix
🤖 Prompt for AI Agents