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
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom 'io.awspring.cloud:spring-cloud-aws-dependencies:3.4.2'
}
}

dependencies {
//redis
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand Down Expand Up @@ -49,6 +55,9 @@ dependencies {
//openai
implementation 'com.openai:openai-java:4.35.0'

//S3
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'


compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Expand Down
17 changes: 17 additions & 0 deletions ops/s3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# S3 setup for job posting images

## 1. Apply bucket CORS

```bash
aws s3api put-bucket-cors \
--bucket "$S3_BUCKET" \
--cors-configuration file://ops/s3/job-posting-image-cors.json
```

## 2. Apply lifecycle policy

```bash
aws s3api put-bucket-lifecycle-configuration \
--bucket "$S3_BUCKET" \
--lifecycle-configuration file://ops/s3/job-posting-image-lifecycle.json
```
25 changes: 25 additions & 0 deletions ops/s3/job-posting-image-cors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"GET",
"HEAD"
],
"AllowedOrigins": [
"https://jobdri.site",
"https://www.jobdri.site",
"https://job-dri.vercel.app",
"http://localhost:3000",
"http://localhost:5173"
],
"ExposeHeaders": [
"ETag",
"x-amz-request-id",
"x-amz-id-2"
],
"MaxAgeSeconds": 3000
}
]
17 changes: 17 additions & 0 deletions ops/s3/job-posting-image-lifecycle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Rules": [
{
"ID": "DeleteTemporaryJobPostingImages",
"Status": "Enabled",
"Filter": {
"Prefix": "job-postings/tmp/"
},
"Expiration": {
"Days": 1
},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 1
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.jobdri.jobdri_api.domain.analysis.controller;

import com.jobdri.jobdri_api.domain.analysis.dto.response.AnalysisResponse;
import com.jobdri.jobdri_api.domain.analysis.service.AnalysisService;
import com.jobdri.jobdri_api.global.apiPayload.ApiResponse;
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
import com.jobdri.jobdri_api.global.security.UserDetailsImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/mock-applies/{mockApplyId}/analysis")
@Tag(name = "Analysis", description = "자소서 분석 API")
public class AnalysisController {

private final AnalysisService analysisService;

@Operation(summary = "자소서 분석 실행", description = "저장된 문항 답변과 공고 정보를 기반으로 자소서를 분석하고 결과를 저장합니다.")
@PostMapping
public ApiResponse<AnalysisResponse> analyze(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long mockApplyId
) {
return ApiResponse.onSuccess(
"자소서 분석이 완료되었습니다.",
analysisService.analyze(getAuthenticatedUser(userDetails), mockApplyId)
);
}

@Operation(summary = "자소서 분석 결과 조회", description = "저장된 자소서 분석 결과를 조회합니다.")
@GetMapping
public ApiResponse<AnalysisResponse> getAnalysis(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long mockApplyId
) {
return ApiResponse.onSuccess(
"자소서 분석 결과 조회에 성공했습니다.",
analysisService.getAnalysis(getAuthenticatedUser(userDetails), mockApplyId)
);
}

private com.jobdri.jobdri_api.domain.user.entity.User getAuthenticatedUser(UserDetailsImpl userDetails) {
if (userDetails == null || userDetails.getUser() == null) {
throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO, "인증 정보가 누락되었습니다.");
}
return userDetails.getUser();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.jobdri.jobdri_api.domain.analysis.dto.llm;

import java.util.List;

public record AnalysisLlmResponse(
Integer score,
Integer jobFit,
Integer impact,
Integer completeness,
String feedback,
List<QuestionAnalysisItem> questionAnalyses
) {
public record QuestionAnalysisItem(
Long questionId,
String sentence,
String status,
String reason,
String improvement
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.jobdri.jobdri_api.domain.analysis.dto.response;

import com.jobdri.jobdri_api.domain.analysis.entity.Question;

import java.util.List;

public record AnalysisQuestionResponse(
Long questionId,
String questionContent,
String answer,
List<QuestionAnalysisResponse> analyses
) {
public static AnalysisQuestionResponse of(
Question question,
List<QuestionAnalysisResponse> analyses
) {
return new AnalysisQuestionResponse(
question.getId(),
question.getContent(),
question.getAnswer(),
analyses
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.jobdri.jobdri_api.domain.analysis.dto.response;

import com.jobdri.jobdri_api.domain.analysis.entity.Analysis;
import com.jobdri.jobdri_api.domain.mockapply.entity.MockApplyStatus;

import java.util.List;

public record AnalysisResponse(
Long mockApplyId,
Long analysisId,
MockApplyStatus status,
int score,
int jobFit,
int impact,
int completeness,
String feedback,
List<AnalysisQuestionResponse> questions
) {
public static AnalysisResponse of(
Analysis analysis,
MockApplyStatus status,
List<AnalysisQuestionResponse> questions
) {
return new AnalysisResponse(
analysis.getMockApply().getId(),
analysis.getId(),
status,
analysis.getScore(),
analysis.getJobFit(),
analysis.getImpact(),
analysis.getCompleteness(),
analysis.getFeedback(),
questions
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.jobdri.jobdri_api.domain.analysis.dto.response;

import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis;
import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysisStatus;

public record QuestionAnalysisResponse(
Long questionAnalysisId,
String sentence,
String status,
String reason,
String improvement,
int start,
int end
) {
public static QuestionAnalysisResponse from(QuestionAnalysis questionAnalysis) {
return new QuestionAnalysisResponse(
questionAnalysis.getId(),
questionAnalysis.getSentence(),
statusValue(questionAnalysis.getStatus()),
questionAnalysis.getReason(),
questionAnalysis.getImprovement(),
questionAnalysis.getStart(),
questionAnalysis.getEnd()
);
}

private static String statusValue(QuestionAnalysisStatus status) {
if (status == null) {
return QuestionAnalysisStatus.MENTIONED.name().toLowerCase();
}
return status.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jobdri.jobdri_api.domain.analysis.dto.response;

public record QuestionCandidateResponse(
Long questionId,
String content,
int charLimit,
boolean selected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,13 @@ public static Analysis create(
int completeness,
String feedback
) {
Analysis analysis = Analysis.builder()
return Analysis.builder()
.mockApply(mockApply)
.score(score)
.jobFit(jobFit)
.impact(impact)
.completeness(completeness)
.feedback(feedback)
.build();
mockApply.assignAnalysis(analysis);
return analysis;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public class QuestionAnalysis {
@Column(nullable = false, columnDefinition = "TEXT")
private String improvement;

@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "varchar(255) default 'MENTIONED'")
@Builder.Default
private QuestionAnalysisStatus status = QuestionAnalysisStatus.MENTIONED;

@Column(name = "start_index", nullable = false)
private int start;

Expand All @@ -44,6 +49,7 @@ public static QuestionAnalysis create(
String sentence,
String reason,
String improvement,
QuestionAnalysisStatus status,
int start,
int end
) {
Expand All @@ -53,6 +59,7 @@ public static QuestionAnalysis create(
.sentence(sentence)
.reason(reason)
.improvement(improvement)
.status(status)
.start(start)
.end(end)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.jobdri.jobdri_api.domain.analysis.entity;

public enum QuestionAnalysisStatus {
PROVEN,
MENTIONED,
MISSING,
FABRICATED
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface QuestionAnalysisRepository extends JpaRepository<QuestionAnalysis, Long> {
List<QuestionAnalysis> findAllByQuestionId(Long questionId);
List<QuestionAnalysis> findAllByAnalysisId(Long analysisId);
List<QuestionAnalysis> findAllByAnalysisIdOrderByQuestionIdAscIdAsc(Long analysisId);
void deleteAllByAnalysisId(Long analysisId);
}
Loading
Loading