diff --git a/server_config b/server_config index 9d8e8cb..8ebfd17 160000 --- a/server_config +++ b/server_config @@ -1 +1 @@ -Subproject commit 9d8e8cb7005e8702706736f5834fd3b83928fc22 +Subproject commit 8ebfd17396fe86f86651a7523877e2784735aea4 diff --git a/src/main/generated/com/chwipoClova/interview/entity/QInterview.java b/src/main/generated/com/chwipoClova/interview/entity/QInterview.java new file mode 100644 index 0000000..27c9501 --- /dev/null +++ b/src/main/generated/com/chwipoClova/interview/entity/QInterview.java @@ -0,0 +1,61 @@ +package com.chwipoClova.interview.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QInterview is a Querydsl query type for Interview + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QInterview extends EntityPathBase { + + private static final long serialVersionUID = 1729798915L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QInterview interview = new QInterview("interview"); + + public final NumberPath interviewId = createNumber("interviewId", Long.class); + + public final DateTimePath modifyDate = createDateTime("modifyDate", java.util.Date.class); + + public final StringPath recruitSummary = createString("recruitSummary"); + + public final DateTimePath regDate = createDateTime("regDate", java.util.Date.class); + + public final StringPath resumeSummary = createString("resumeSummary"); + + public final StringPath title = createString("title"); + + public final com.chwipoClova.user.entity.QUser user; + + public QInterview(String variable) { + this(Interview.class, forVariable(variable), INITS); + } + + public QInterview(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QInterview(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QInterview(PathMetadata metadata, PathInits inits) { + this(Interview.class, metadata, inits); + } + + public QInterview(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.user = inits.isInitialized("user") ? new com.chwipoClova.user.entity.QUser(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/com/chwipoClova/qa/entity/QQa.java b/src/main/generated/com/chwipoClova/qa/entity/QQa.java new file mode 100644 index 0000000..389f5b4 --- /dev/null +++ b/src/main/generated/com/chwipoClova/qa/entity/QQa.java @@ -0,0 +1,61 @@ +package com.chwipoClova.qa.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QQa is a Querydsl query type for Qa + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QQa extends EntityPathBase { + + private static final long serialVersionUID = 24461021L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QQa qa = new QQa("qa"); + + public final StringPath aiAnswer = createString("aiAnswer"); + + public final StringPath answer = createString("answer"); + + public final com.chwipoClova.interview.entity.QInterview interview; + + public final DateTimePath modifyDate = createDateTime("modifyDate", java.util.Date.class); + + public final NumberPath qaId = createNumber("qaId", Long.class); + + public final StringPath question = createString("question"); + + public final DateTimePath regDate = createDateTime("regDate", java.util.Date.class); + + public QQa(String variable) { + this(Qa.class, forVariable(variable), INITS); + } + + public QQa(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QQa(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QQa(PathMetadata metadata, PathInits inits) { + this(Qa.class, metadata, inits); + } + + public QQa(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.interview = inits.isInitialized("interview") ? new com.chwipoClova.interview.entity.QInterview(forProperty("interview"), inits.get("interview")) : null; + } + +} + diff --git a/src/main/generated/com/chwipoClova/recruit/entity/QRecruit.java b/src/main/generated/com/chwipoClova/recruit/entity/QRecruit.java new file mode 100644 index 0000000..7b333dc --- /dev/null +++ b/src/main/generated/com/chwipoClova/recruit/entity/QRecruit.java @@ -0,0 +1,67 @@ +package com.chwipoClova.recruit.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QRecruit is a Querydsl query type for Recruit + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QRecruit extends EntityPathBase { + + private static final long serialVersionUID = 1252659683L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QRecruit recruit = new QRecruit("recruit"); + + public final StringPath content = createString("content"); + + public final StringPath fileName = createString("fileName"); + + public final StringPath filePath = createString("filePath"); + + public final NumberPath fileSize = createNumber("fileSize", Long.class); + + public final StringPath originalFileName = createString("originalFileName"); + + public final NumberPath recruitId = createNumber("recruitId", Long.class); + + public final DateTimePath regDate = createDateTime("regDate", java.util.Date.class); + + public final StringPath summary = createString("summary"); + + public final StringPath title = createString("title"); + + public final com.chwipoClova.user.entity.QUser user; + + public QRecruit(String variable) { + this(Recruit.class, forVariable(variable), INITS); + } + + public QRecruit(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QRecruit(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QRecruit(PathMetadata metadata, PathInits inits) { + this(Recruit.class, metadata, inits); + } + + public QRecruit(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.user = inits.isInitialized("user") ? new com.chwipoClova.user.entity.QUser(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/com/chwipoClova/resume/entity/QResume.java b/src/main/generated/com/chwipoClova/resume/entity/QResume.java index 1b8da58..373a73e 100644 --- a/src/main/generated/com/chwipoClova/resume/entity/QResume.java +++ b/src/main/generated/com/chwipoClova/resume/entity/QResume.java @@ -28,7 +28,7 @@ public class QResume extends EntityPathBase { public final NumberPath fileSize = createNumber("fileSize", Long.class); - public final StringPath orginalFileName = createString("orginalFileName"); + public final StringPath originalFileName = createString("originalFileName"); public final DateTimePath regDate = createDateTime("regDate", java.util.Date.class); diff --git a/src/main/java/com/chwipoClova/common/config/MultipartJackson2HttpMessageConverter.java b/src/main/java/com/chwipoClova/common/config/MultipartJackson2HttpMessageConverter.java new file mode 100644 index 0000000..2ca547c --- /dev/null +++ b/src/main/java/com/chwipoClova/common/config/MultipartJackson2HttpMessageConverter.java @@ -0,0 +1,34 @@ +package com.chwipoClova.common.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; + +@Component +public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * Converter for support http request with header Content-Type: multipart/form-data + */ + public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } +} diff --git a/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java b/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java index c26da9e..6a9e075 100644 --- a/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java +++ b/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java @@ -23,11 +23,15 @@ public enum ExceptionCode { USER_NULL("801", "유저 정보가 올바르지 않습니다."), - FILE_EXT("850", "PDF 파일 형식이 아닙니다."), + FILE_EXT_PDF("850", "PDF 파일 형식이 아닙니다."), - FILE_SIZE("851", "파일 업로드 최대 크기는 50M 입니다."), + FILE_EXT_IMAGE("851", "IMAGE 파일 형식이 아닙니다."), - RESUME_NULL("860", "이력서 정보가 올바르지 않습니다.") + FILE_SIZE("860", "파일 업로드 최대 크기는 50M 입니다."), + + RESUME_NULL("870", "이력서 정보가 올바르지 않습니다."), + + RECRUIT_CONTENT_NULL("880", "채용공고 정보가 올바르지 않습니다.") ; diff --git a/src/main/java/com/chwipoClova/interview/controller/InterviewController.java b/src/main/java/com/chwipoClova/interview/controller/InterviewController.java new file mode 100644 index 0000000..0814a46 --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/controller/InterviewController.java @@ -0,0 +1,43 @@ +package com.chwipoClova.interview.controller; + +import com.chwipoClova.interview.request.InterviewInsertReq; +import com.chwipoClova.interview.response.InterviewInsertRes; +import com.chwipoClova.interview.service.InterviewService; +import com.chwipoClova.resume.response.ResumeUploadRes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Encoding; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "Interview", description = "면접 API") +@RequestMapping("interview") +public class InterviewController { + + private final InterviewService interviewService; + + @Operation(summary = "면접 등록", description = "면접 등록") + @PostMapping(path = "/insertInterview", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK") + } + ) + public InterviewInsertRes insertInterview( + @RequestPart(value = "interviewData") InterviewInsertReq interviewInsertReq, + @RequestPart(value = "file", required = false) MultipartFile file + ) throws Exception { + return interviewService.insertInterview(interviewInsertReq, file); + } + +} diff --git a/src/main/java/com/chwipoClova/interview/entity/Interview.java b/src/main/java/com/chwipoClova/interview/entity/Interview.java new file mode 100644 index 0000000..d732e09 --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/entity/Interview.java @@ -0,0 +1,67 @@ +package com.chwipoClova.interview.entity; + +import com.chwipoClova.user.entity.User; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import java.util.Date; + +@Entity(name = "Interview") +@Table(name = "Interview") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties() +@DynamicInsert +@Builder +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class Interview { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "interviewId") + @Schema(description = "면접ID") + private Long interviewId; + + @Column(name = "title") + @Schema(description = "제목") + private String title; + + @Column(name = "resumeSummary") + @Schema(description = "이력서 요약") + private String resumeSummary; + + @Column(name = "recruitSummary") + @Schema(description = "채용공고 요약") + private String recruitSummary; + + @Column(name = "regDate") + @Schema(description = "등록일") + private Date regDate; + + @Column(name = "modifyDate") + @Schema(description = "수정일") + private Date modifyDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + private User user; + + // @PrePersist 메서드 정의 (최초 등록시 호출) + @PrePersist + public void prePersist() { + this.regDate = new Date(); // 현재 날짜와 시간으로 등록일 설정 + } + + // @PreUpdate 메서드 정의 (업데이트 시 호출) + @PreUpdate + public void preUpdate() { + this.modifyDate = new Date(); // 현재 날짜와 시간으로 수정일 업데이트 + } + +} diff --git a/src/main/java/com/chwipoClova/interview/repository/InterviewRepository.java b/src/main/java/com/chwipoClova/interview/repository/InterviewRepository.java new file mode 100644 index 0000000..63d70dc --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/repository/InterviewRepository.java @@ -0,0 +1,7 @@ +package com.chwipoClova.interview.repository; + +import com.chwipoClova.interview.entity.Interview; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterviewRepository extends JpaRepository { +} diff --git a/src/main/java/com/chwipoClova/interview/request/InterviewInsertReq.java b/src/main/java/com/chwipoClova/interview/request/InterviewInsertReq.java new file mode 100644 index 0000000..c5240bb --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/request/InterviewInsertReq.java @@ -0,0 +1,19 @@ +package com.chwipoClova.interview.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class InterviewInsertReq { + + @Schema(description = "유저 ID", example = "1", name = "userId") + private Long userId; + + @Schema(description = "이력서 ID", example = "1", name = "resumeId") + private Long resumeId; + + @Schema(description = "채용공고내용", example = "삼성채용", name = "recruitContent") + private String recruitContent; + +} diff --git a/src/main/java/com/chwipoClova/interview/response/InterviewInsertRes.java b/src/main/java/com/chwipoClova/interview/response/InterviewInsertRes.java new file mode 100644 index 0000000..c83c430 --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/response/InterviewInsertRes.java @@ -0,0 +1,34 @@ +package com.chwipoClova.interview.response; + +import com.chwipoClova.qa.response.QaQuestionInsertRes; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +@Data +@Builder +public class InterviewInsertRes { + + @Schema(description = "면접 ID", example = "1", name = "interviewId") + private Long interviewId; + + @Schema(description = "유저 ID", example = "1", name = "userId") + private Long userId; + + @Schema(description = "면접 제목", example = "삼성채용", name = "title") + private String title; + + @Schema(description = "등록일", example = "2023-12-09T10:13:17.838+00:00", name = "regDate") + private Date regDate; + + @Schema(description = "수정일", example = "2023-12-09T10:13:17.838+00:00", name = "modifyDate") + private Date modifyDate; + + @Schema(description = "질문데이터", name = "questionData") + private List questionData; + +} diff --git a/src/main/java/com/chwipoClova/interview/service/InterviewService.java b/src/main/java/com/chwipoClova/interview/service/InterviewService.java new file mode 100644 index 0000000..4519c13 --- /dev/null +++ b/src/main/java/com/chwipoClova/interview/service/InterviewService.java @@ -0,0 +1,105 @@ +package com.chwipoClova.interview.service; + +import com.chwipoClova.common.exception.CommonException; +import com.chwipoClova.common.exception.ExceptionCode; +import com.chwipoClova.interview.entity.Interview; +import com.chwipoClova.interview.repository.InterviewRepository; +import com.chwipoClova.interview.request.InterviewInsertReq; +import com.chwipoClova.interview.response.InterviewInsertRes; +import com.chwipoClova.qa.request.QaQuestionInsertReq; +import com.chwipoClova.qa.response.QaQuestionInsertRes; +import com.chwipoClova.qa.service.QaService; +import com.chwipoClova.recruit.request.RecruitInsertReq; +import com.chwipoClova.recruit.response.RecruitInsertRes; +import com.chwipoClova.recruit.service.RecruitService; +import com.chwipoClova.resume.entity.Resume; +import com.chwipoClova.resume.repository.ResumeRepository; +import com.chwipoClova.user.entity.User; +import com.chwipoClova.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +@Slf4j +public class InterviewService { + + private final ResumeRepository resumeRepository; + + private final UserRepository userRepository; + + private final RecruitService recruitService; + + private final InterviewRepository interviewRepository; + + private final QaService qaService; + + @Transactional + public InterviewInsertRes insertInterview(InterviewInsertReq interviewInsertReq, MultipartFile file) throws IOException { + Long userId = interviewInsertReq.getUserId(); + Long resumeId = interviewInsertReq.getResumeId(); + String recruitContent = interviewInsertReq.getRecruitContent(); + + User user = userRepository.findById(userId).orElseThrow(() -> new CommonException(ExceptionCode.USER_NULL.getMessage(), ExceptionCode.USER_NULL.getCode())); + Resume resume = resumeRepository.findByUserUserIdAndResumeId(userId, resumeId).orElseThrow(() -> new CommonException(ExceptionCode.RESUME_NULL.getMessage(), ExceptionCode.RESUME_NULL.getCode())); + + // 채용 공고 등록 및 조회 + RecruitInsertReq recruitInsertReq = new RecruitInsertReq(); + recruitInsertReq.setUserId(userId); + recruitInsertReq.setRecruitContent(recruitContent); + recruitInsertReq.setFile(file); + RecruitInsertRes recruitInsertRes = recruitService.insertRecruit(recruitInsertReq); + + String title = recruitInsertRes.getTitle(); + String recruitSummary = recruitInsertRes.getSummary(); + String resumeSummary = resume.getSummary(); + + // TODO 이력서 요약과 채용공고 요약을 이용해서 질문, AI 답변 생성 + String q1 = "질문1입니다."; + String qAi1 = "질문1 AI 답변입니다."; + + String q2 = "질문2입니다."; + String qAi2 = "질문2 AI 답변입니다."; + + // 면접 저장 + Interview interview = Interview.builder() + .title(title) + .recruitSummary(recruitSummary) + .resumeSummary(resumeSummary) + .user(user) + .build(); + Interview interviewRst = interviewRepository.save(interview); + + // 질문 답변 저장 + List qaQuestionInsertReqList = new ArrayList<>(); + QaQuestionInsertReq qaQuestionInsertReq1 = new QaQuestionInsertReq(); + qaQuestionInsertReq1.setInterview(interview); + qaQuestionInsertReq1.setQuestion(q1); + qaQuestionInsertReq1.setAiAnswer(qAi1); + qaQuestionInsertReqList.add(qaQuestionInsertReq1); + + QaQuestionInsertReq qaQuestionInsertReq2 = new QaQuestionInsertReq(); + qaQuestionInsertReq2.setInterview(interview); + qaQuestionInsertReq2.setQuestion(q2); + qaQuestionInsertReq2.setAiAnswer(qAi2); + qaQuestionInsertReqList.add(qaQuestionInsertReq2); + List questionData = qaService.insertQaQuestionList(qaQuestionInsertReqList); + + return InterviewInsertRes.builder() + .interviewId(interviewRst.getInterviewId()) + .userId(userId) + .title(interviewRst.getTitle()) + .regDate(interviewRst.getRegDate()) + .modifyDate(interviewRst.getModifyDate()) + .questionData(questionData) + .build(); + } + +} diff --git a/src/main/java/com/chwipoClova/qa/controller/QaController.java b/src/main/java/com/chwipoClova/qa/controller/QaController.java new file mode 100644 index 0000000..441a5ad --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/controller/QaController.java @@ -0,0 +1,4 @@ +package com.chwipoClova.qa.controller; + +public class QaController { +} diff --git a/src/main/java/com/chwipoClova/qa/entity/Qa.java b/src/main/java/com/chwipoClova/qa/entity/Qa.java new file mode 100644 index 0000000..b2e14db --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/entity/Qa.java @@ -0,0 +1,65 @@ +package com.chwipoClova.qa.entity; + +import com.chwipoClova.interview.entity.Interview; +import com.chwipoClova.user.entity.User; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import java.util.Date; + +@Entity(name = "Qa") +@Table(name = "Qa") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties() +@DynamicInsert +@Builder +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class Qa { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "qaId") + @Schema(description = "질문답변 ID") + private Long qaId; + + @Column(name = "question") + @Schema(description = "질문") + private String question; + + @Column(name = "answer") + @Schema(description = "답변") + private String answer; + + @Column(name = "aiAnswer") + @Schema(description = "AI답변") + private String aiAnswer; + + @Column(name = "regDate") + @Schema(description = "등록일") + private Date regDate; + + @Column(name = "modifyDate") + @Schema(description = "수정일") + private Date modifyDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "interviewId") + private Interview interview; + + @PrePersist + public void prePersist() { + this.regDate = new Date(); // 현재 날짜와 시간으로 등록일 설정 + } + + // @PreUpdate 메서드 정의 (업데이트 시 호출) + @PreUpdate + public void preUpdate() { + this.modifyDate = new Date(); // 현재 날짜와 시간으로 수정일 업데이트 + } +} diff --git a/src/main/java/com/chwipoClova/qa/repository/QaRepository.java b/src/main/java/com/chwipoClova/qa/repository/QaRepository.java new file mode 100644 index 0000000..e511738 --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/repository/QaRepository.java @@ -0,0 +1,7 @@ +package com.chwipoClova.qa.repository; + +import com.chwipoClova.qa.entity.Qa; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QaRepository extends JpaRepository { +} diff --git a/src/main/java/com/chwipoClova/qa/request/QaQuestionInsertReq.java b/src/main/java/com/chwipoClova/qa/request/QaQuestionInsertReq.java new file mode 100644 index 0000000..3b1953f --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/request/QaQuestionInsertReq.java @@ -0,0 +1,20 @@ +package com.chwipoClova.qa.request; + + +import com.chwipoClova.interview.entity.Interview; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class QaQuestionInsertReq { + + @Schema(description = "면접객체", name = "interview") + private Interview interview; + + @Schema(description = "질문", example = "질문1", name = "question") + private String question; + + @Schema(description = "AI 답변", example = "AI답변1", name = "aiAnswer") + private String aiAnswer; + +} diff --git a/src/main/java/com/chwipoClova/qa/response/QaQuestionInsertRes.java b/src/main/java/com/chwipoClova/qa/response/QaQuestionInsertRes.java new file mode 100644 index 0000000..efeff28 --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/response/QaQuestionInsertRes.java @@ -0,0 +1,30 @@ +package com.chwipoClova.qa.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; + +@Data +@Builder +public class QaQuestionInsertRes { + + @Schema(description = "면접 ID", example = "1", name = "interviewId") + private Long interviewId; + + @Schema(description = "질문 ID", example = "1", name = "qaId") + private Long qaId; + + @Schema(description = "질문", example = "질문1", name = "question") + private String question; + + @Schema(description = "AI 답변", example = "AI답변1", name = "aiAnswer") + private String aiAnswer; + + @Schema(description = "등록일", example = "2023-12-09T10:13:17.838+00:00", name = "regDate") + private Date regDate; + + @Schema(description = "수정일", example = "2023-12-09T10:13:17.838+00:00", name = "modifyDate") + private Date modifyDate; +} diff --git a/src/main/java/com/chwipoClova/qa/service/QaService.java b/src/main/java/com/chwipoClova/qa/service/QaService.java new file mode 100644 index 0000000..a798da1 --- /dev/null +++ b/src/main/java/com/chwipoClova/qa/service/QaService.java @@ -0,0 +1,56 @@ +package com.chwipoClova.qa.service; + +import com.chwipoClova.interview.entity.Interview; +import com.chwipoClova.interview.repository.InterviewRepository; +import com.chwipoClova.qa.entity.Qa; +import com.chwipoClova.qa.repository.QaRepository; +import com.chwipoClova.qa.request.QaQuestionInsertReq; +import com.chwipoClova.qa.response.QaQuestionInsertRes; +import com.chwipoClova.recruit.request.RecruitInsertReq; +import com.chwipoClova.recruit.response.RecruitInsertRes; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +@Slf4j +public class QaService { + + private final QaRepository qaRepository; + + @Transactional + public List insertQaQuestionList(List qaQuestionInsertReqList) throws IOException { + List qaList = new ArrayList<>(); + qaQuestionInsertReqList.stream().forEach(qaQuestionInsertReq -> { + Qa qa = Qa.builder() + .question(qaQuestionInsertReq.getQuestion()) + .aiAnswer(qaQuestionInsertReq.getAiAnswer()) + .interview(qaQuestionInsertReq.getInterview()) + .build(); + qaList.add(qa); + }); + List qaListRst = qaRepository.saveAll(qaList); + + List qaQuestionInsertResList = new ArrayList<>(); + + qaListRst.stream().forEach(qa -> { + QaQuestionInsertRes qaQuestionInsertRes = QaQuestionInsertRes.builder() + .qaId(qa.getQaId()) + .question(qa.getQuestion()) + .aiAnswer(qa.getAiAnswer()) + .interviewId(qa.getInterview().getInterviewId()) + .regDate(qa.getRegDate()) + .modifyDate(qa.getModifyDate()) + .build(); + qaQuestionInsertResList.add(qaQuestionInsertRes); + }); + + return qaQuestionInsertResList; + } +} diff --git a/src/main/java/com/chwipoClova/recruit/entity/Recruit.java b/src/main/java/com/chwipoClova/recruit/entity/Recruit.java new file mode 100644 index 0000000..b6d9d68 --- /dev/null +++ b/src/main/java/com/chwipoClova/recruit/entity/Recruit.java @@ -0,0 +1,72 @@ +package com.chwipoClova.recruit.entity; + +import com.chwipoClova.user.entity.User; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import java.util.Date; + +@Entity(name = "Recruit") +@Table(name = "Recruit") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties() +@DynamicInsert +@Builder +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "채용공고 정보 VO") +public class Recruit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "recruitId") + @Schema(description = "채용공고 ID") + private Long recruitId; + + @Column(name = "title") + @Schema(description = "채용공고제목") + private String title; + + @Column(name = "content") + @Schema(description = "채용공고텍스트") + private String content; + + @Column(name = "fileName") + @Schema(description = "파일이름") + private String fileName; + + @Column(name = "filePath") + @Schema(description = "파일경로") + private String filePath; + + @Column(name = "fileSize") + @Schema(description = "파일크기") + private Long fileSize; + + @Column(name = "originalFileName") + @Schema(description = "원본파일이름") + private String originalFileName; + + @Column(name = "summary") + @Schema(description = "요약") + private String summary; + + @Column(name = "regDate") + @Schema(description = "등록일") + private Date regDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + private User user; + + @PrePersist + public void prePersist() { + this.regDate = new Date(); // 현재 날짜와 시간으로 등록일 설정 + } +} diff --git a/src/main/java/com/chwipoClova/recruit/repository/RecruitRepository.java b/src/main/java/com/chwipoClova/recruit/repository/RecruitRepository.java new file mode 100644 index 0000000..0776446 --- /dev/null +++ b/src/main/java/com/chwipoClova/recruit/repository/RecruitRepository.java @@ -0,0 +1,7 @@ +package com.chwipoClova.recruit.repository; + +import com.chwipoClova.recruit.entity.Recruit; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecruitRepository extends JpaRepository { +} diff --git a/src/main/java/com/chwipoClova/recruit/request/RecruitInsertReq.java b/src/main/java/com/chwipoClova/recruit/request/RecruitInsertReq.java new file mode 100644 index 0000000..d7c2e6c --- /dev/null +++ b/src/main/java/com/chwipoClova/recruit/request/RecruitInsertReq.java @@ -0,0 +1,18 @@ +package com.chwipoClova.recruit.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Data +public class RecruitInsertReq { + + @Schema(description = "유저 ID", example = "1", name = "userId") + private Long userId; + + @Schema(description = "채용공고 내용", example = "1", name = "recruitContent") + private String recruitContent; + + private MultipartFile file; + +} diff --git a/src/main/java/com/chwipoClova/recruit/response/RecruitInsertRes.java b/src/main/java/com/chwipoClova/recruit/response/RecruitInsertRes.java new file mode 100644 index 0000000..3881914 --- /dev/null +++ b/src/main/java/com/chwipoClova/recruit/response/RecruitInsertRes.java @@ -0,0 +1,20 @@ +package com.chwipoClova.recruit.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class RecruitInsertRes { + + @Schema(description = "채용공고 번호", example = "1", name = "recruitId") + private Long recruitId; + + @Schema(description = "채용공고 제목", example = "삼성채용공고", name = "title") + private String title; + + @Schema(description = "채용공고 요약", example = "삼성모집", name = "summary") + private String summary; + +} diff --git a/src/main/java/com/chwipoClova/recruit/service/RecruitService.java b/src/main/java/com/chwipoClova/recruit/service/RecruitService.java new file mode 100644 index 0000000..6a5b096 --- /dev/null +++ b/src/main/java/com/chwipoClova/recruit/service/RecruitService.java @@ -0,0 +1,153 @@ +package com.chwipoClova.recruit.service; + + +import com.chwipoClova.common.exception.CommonException; +import com.chwipoClova.common.exception.ExceptionCode; +import com.chwipoClova.recruit.entity.Recruit; +import com.chwipoClova.recruit.repository.RecruitRepository; +import com.chwipoClova.recruit.request.RecruitInsertReq; +import com.chwipoClova.recruit.response.RecruitInsertRes; +import com.chwipoClova.user.entity.User; +import com.chwipoClova.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +@Slf4j +public class RecruitService { + + @Value("${file.upload.recruit.path}") + private String uploadPath; + + @Value("${file.upload.recruit.max-size}") + private Long uploadMaxSize; + + @Value("${file.upload.recruit.type}") + private String uploadType; + + private final UserRepository userRepository; + + private final RecruitRepository recruitRepository; + + @Transactional + public RecruitInsertRes insertRecruit(RecruitInsertReq recruitInsertReq) throws IOException { + Long userId = recruitInsertReq.getUserId(); + String recruitContent = recruitInsertReq.getRecruitContent(); + MultipartFile file = recruitInsertReq.getFile(); + + User user = userRepository.findById(userId).orElseThrow(() -> new CommonException(ExceptionCode.USER_NULL.getMessage(), ExceptionCode.USER_NULL.getCode())); + + if (org.apache.commons.lang3.StringUtils.isBlank(recruitContent) && file == null) { + throw new CommonException(ExceptionCode.RECRUIT_CONTENT_NULL.getMessage(), ExceptionCode.RECRUIT_CONTENT_NULL.getCode()); + } + + Recruit recruit; + + if (org.apache.commons.lang3.StringUtils.isNotBlank(recruitContent)) { + // TODO 채용 공고 텍스트 요약 + String summary = recruitContent + "요약"; + + // TODO 채용 공고 제목 추출 + String title = recruitContent + "제목"; + + recruit = Recruit.builder() + .title(title) + .content(recruitContent) + .summary(summary) + .user(user) + .build(); + } else { + String contentType = file.getContentType(); + assert contentType != null; + + if (contentType.toLowerCase().indexOf(uploadType) == -1) { + throw new CommonException(ExceptionCode.FILE_EXT_IMAGE.getMessage(), ExceptionCode.FILE_EXT_IMAGE.getCode()); + } + + String originalName = file.getOriginalFilename(); + assert originalName != null; + + String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); + + // 날짜 폴더 생성 + String folderPath = makeFolder(); + + // UUID + String uuid = UUID.randomUUID().toString(); + + long currentTimeMills = Timestamp.valueOf(LocalDateTime.now()).getTime(); + + String filePath = uploadPath + File.separator + folderPath + File.separator; + String fileName = uuid + "_" + currentTimeMills + "." +extension; + Long fileSize = file.getSize(); + + if (fileSize > uploadMaxSize) { + new CommonException(ExceptionCode.FILE_SIZE.getMessage(), ExceptionCode.FILE_SIZE.getCode()); + } + + // 저장할 파일 이름 중간에 "_"를 이용해서 구현 + String saveName = filePath + fileName; + Path savePath = Paths.get(saveName); + file.transferTo(savePath); + + // TODO 채용 공고 이미지 요약 + String summary = originalName + "요약"; + + // TODO 채용 공고 제목 추출 + String title = originalName + "제목"; + + recruit = Recruit.builder() + .title(title) + .fileName(fileName) + .filePath(filePath) + .fileSize(fileSize) + .originalFileName(originalName) + .user(user) + .summary(summary) + .build(); + } + + Recruit recruitRst = recruitRepository.save(recruit); + + RecruitInsertRes recruitInsertRes = RecruitInsertRes.builder() + .recruitId(recruitRst.getRecruitId()) + .title(recruitRst.getTitle()) + .summary(recruitRst.getSummary()) + .build(); + + return recruitInsertRes; + } + private String makeFolder() { + + String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + + String folderPath = str.replace("/", File.separator); + + // make folder -------- + File uploadPathFolder = new File(uploadPath, folderPath); + + if(!uploadPathFolder.exists()) { + boolean mkdirs = uploadPathFolder.mkdirs(); + log.info("-------------------makeFolder------------------"); + log.info("uploadPathFolder.exists() : {}", uploadPathFolder.exists()); + log.info("mkdirs : {}", mkdirs); + } + return folderPath; + } +} diff --git a/src/main/java/com/chwipoClova/resume/controller/ResumeController.java b/src/main/java/com/chwipoClova/resume/controller/ResumeController.java index 01f4988..e938fc3 100644 --- a/src/main/java/com/chwipoClova/resume/controller/ResumeController.java +++ b/src/main/java/com/chwipoClova/resume/controller/ResumeController.java @@ -7,6 +7,7 @@ import com.chwipoClova.resume.response.ResumeUploadRes; import com.chwipoClova.resume.service.ResumeService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -30,17 +31,17 @@ public class ResumeController { private final ResumeService resumeService; @Operation(summary = "이력서 업로드", description = "이력서 업로드") - @PostMapping(path = "/resumeUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(path = "/uploadResume", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK") } ) - public ResumeUploadRes resumeUpload( + public ResumeUploadRes uploadResume( @Schema(description = "userId", example = "1", name = "userId") - @RequestParam(value = "userId") Long userId, + @RequestPart(value = "userId") Long userId, @RequestPart(value = "file") MultipartFile file ) throws Exception { - return resumeService.resumeUpload(userId, file); + return resumeService.uploadResume(userId, file); } @Operation(summary = "이력서 조회", description = "이력서 조회") diff --git a/src/main/java/com/chwipoClova/resume/entity/Resume.java b/src/main/java/com/chwipoClova/resume/entity/Resume.java index 8dbd586..e1dcca9 100644 --- a/src/main/java/com/chwipoClova/resume/entity/Resume.java +++ b/src/main/java/com/chwipoClova/resume/entity/Resume.java @@ -41,9 +41,9 @@ public class Resume { @Schema(description = "파일크기") private Long fileSize; - @Column(name = "orginalFileName") + @Column(name = "originalFileName") @Schema(description = "원본파일이름") - private String orginalFileName; + private String originalFileName; @Column(name = "summary") @Schema(description = "요약") @@ -53,7 +53,7 @@ public class Resume { @Schema(description = "등록일") private Date regDate; - @OneToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "userId") private User user; diff --git a/src/main/java/com/chwipoClova/resume/request/ResumeUploadReq.java b/src/main/java/com/chwipoClova/resume/request/ResumeUploadReq.java index 0735e17..f4e3371 100644 --- a/src/main/java/com/chwipoClova/resume/request/ResumeUploadReq.java +++ b/src/main/java/com/chwipoClova/resume/request/ResumeUploadReq.java @@ -9,6 +9,7 @@ @Data public class ResumeUploadReq { - private MultipartFile file; + @Schema(description = "유저 Id", example = "1", name = "userId") + private Long userId; } diff --git a/src/main/java/com/chwipoClova/resume/service/ResumeService.java b/src/main/java/com/chwipoClova/resume/service/ResumeService.java index 9b0d32d..378665c 100644 --- a/src/main/java/com/chwipoClova/resume/service/ResumeService.java +++ b/src/main/java/com/chwipoClova/resume/service/ResumeService.java @@ -30,7 +30,6 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.UUID; @RequiredArgsConstructor @@ -38,13 +37,13 @@ @Slf4j public class ResumeService { - @Value("${file.upload.path}") + @Value("${file.upload.resume.path}") private String uploadPath; - @Value("${file.upload.max-size}") + @Value("${file.upload.resume.max-size}") private Long uploadMaxSize; - @Value("${file.upload.type}") + @Value("${file.upload.resume.type}") private String uploadType; private final ResumeRepository resumeRepository; @@ -52,18 +51,18 @@ public class ResumeService { private final UserRepository userRepository; @Transactional - public ResumeUploadRes resumeUpload(Long userId, MultipartFile file) throws IOException { + public ResumeUploadRes uploadResume(Long userId, MultipartFile file) throws IOException { User user = userRepository.findById(userId).orElseThrow(() -> new CommonException(ExceptionCode.USER_NULL.getMessage(), ExceptionCode.USER_NULL.getCode())); String contentType = file.getContentType(); assert contentType != null; if (contentType.toLowerCase().indexOf(uploadType) == -1) { - throw new CommonException(ExceptionCode.FILE_EXT.getMessage(), ExceptionCode.FILE_EXT.getCode()); + throw new CommonException(ExceptionCode.FILE_EXT_PDF.getMessage(), ExceptionCode.FILE_EXT_PDF.getCode()); } - String orginalName = file.getOriginalFilename(); - assert orginalName != null; + String originalName = file.getOriginalFilename(); + assert originalName != null; String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); @@ -89,6 +88,7 @@ public ResumeUploadRes resumeUpload(Long userId, MultipartFile file) throws IOEx file.transferTo(savePath); // TODO 업로드 성공 후 요약 저장 + String summary = originalName + "요약"; // TODO 등록 전 개수 제한 필요 @@ -97,7 +97,8 @@ public ResumeUploadRes resumeUpload(Long userId, MultipartFile file) throws IOEx .fileName(fileName) .filePath(filePath) .fileSize(fileSize) - .orginalFileName(orginalName) + .originalFileName(originalName) + .summary(summary) .user(user) .build(); @@ -136,7 +137,7 @@ public List selectResumeList(Long userId) { resumeList.stream().forEach(resume -> { ResumeListRes resumeListRes = ResumeListRes.builder() .resumeId(resume.getResumeId()) - .fileName(resume.getOrginalFileName()) + .fileName(resume.getOriginalFileName()) .regDate(resume.getRegDate()) .build(); resumeListResList.add(resumeListRes);